Skip to content

Express for AI Agents. Wrap your Agents in the Agent2Agent Protocol and allow them to communicate with any agent no matter the runtime or the ecosystem. All without any of the boilerplate. A Typescript SDK for Agentic Communication.

License

Notifications You must be signed in to change notification settings

the-artinet-project/artinet-sdk

Repository files navigation

Website npm version npm downloads Apache License Known Vulnerabilities GitHub stars Discord

artinet SDK

The artinet SDK is a TypeScript library designed for Agentic Communication. It's written for node.js and aims to simplify the creation of interoperable AI agents. Learn more at the artinet project.

This SDK leverages a service-oriented architecture for building AI agents allowing developers to easily create agents as simple processes or seamlessly embed them within a dedicated server.

Quick Start

Use the create-agent command:

npx @artinet/create-agent@latest

It has serveral template projects that you can use to jump right into agent building.

Table of Contents

Breaking Changes in v0.5.8

  • Pino has been removed and replaced with console for better portability and is set to silent by default.
  • The default handler for streamMessage no longer automatically emits an initial submitted and working event.
  • Agent Registration, Bundling and Deployment utils have been removed (email us: [email protected] for support).
  • @artinet/metadata-validator has been removed due to build issues.
  • getTask now correctly takes TaskQueryParams as an argument vs TaskIdParams in accordance with the A2A spec.
  • AgentBuilder now returns a unique messageId for each status update instead of the original user provided messageId.
  • AgentBuilder now prefers the contextId & taskId from the calling context.
  • In a future release the following packages will be set as peer dependancies to reduce the size of the build: @modelcontextprotocol/sdk, @trpc/server, cors, express

Features

  • Modular Design: Everything you need to get started building collaborative agents while remaining flexible enough for advanced configuration.
  • Express Style Agent API: Similar to scaffolding an Express Server, you can use the AgentBuilder to create an Agent2Agent API.
    • Then wrap it in an actual Express Server with the createAgentServer() function. It handles all of the transport-layer complexity, adds A2A <-> JSON-RPC middleware, and manages Server-Sent Events (SSE) automatically.
  • Protocol Compliance: Implements the complete A2A specification (v0.3.0) with support for any kind of transport layer (tRPC, WebSockets, etc).

Installation

npm install @artinet/sdk

Requirements

  • Node.js (v22.0.0 or higher recommended)

Example

A basic A2A server and client interaction (For simple agents see the AgentBuilder section). For more detailed examples, see the examples/ directory.

1. Server (quick-server.ts)

import {
  createAgentServer,
  AgentBuilder,
  getContent,
} from "@artinet/sdk";

//Define your Agents Behaviour
const quickAgentEngine: AgentEngine = AgentBuilder()
  .text(async ({ command, context }) => {
    const task = context.State();
    ...
    const userInput = getContent(command.message);
    return `You said: ${userInput}`;
  })
  .createAgentEngine();

// Create an agent server
const { app, agent } = createAgentServer({
  agent: {
    engine: quickAgentEngine,
    agentCard: {
      name: "QuickStart Agent",
      url: "http://localhost:4000/a2a",
      version: "0.1.0",
      capabilities: { streaming: true },
      skills: [{ id: "echo", name: "Echo Skill" }],
      ...
    },
    tasks: new TaskManager(),
    ...
  },
  basePath: "/a2a",
  agentCardPath: "/.well-known/agent.json"
  ...
});

app.listen(4000, () => {
  console.log("A2A Server running at http://localhost:4000/a2a");
});

2. Client (quick-client.ts)

import { A2AClient, TaskStatusUpdateEvent } from "@artinet/sdk";

async function runClient() {
  const client = new A2AClient("http://localhost:4000/a2a");

   const message = {
    messageId: "test-message-id",
    kind: "message",
    role: "user",
    parts: [{ kind: "text", text: "Hello World!" }],
    ...
  };

  const stream = client.sendStreamingMessage({ message });

  for await (const update of stream) {
    // process the updates
    ...
  }
  console.log("Stream finished.");
}

await runClient().catch(console.error);

Running Tests

npm test

Usage

Client

Interact with A2A-compliant agents using the A2AClient. See examples/ for more.

Basic Client Usage

Send a message using message/send.

import { A2AClient, Message } from "@artinet/sdk";

async function runBasicTask() {
  const client = new A2AClient("https://your-a2a-server.com/a2a");
  const message: Message = {
    messageId: "test-message",
    kind: "message",
    role: "user",
    parts: [{ kind: "text", text: "What is the capital of France?" }],
    ...
  };

  const task = await client.sendMessage({ message });
  console.log("Task Completed:", task);
}

Streaming Updates

Receive real-time updates via SSE using message/stream (recommended).

import {
  A2AClient,
  Message,
  TaskStatusUpdateEvent,
  TaskArtifactUpdateEvent,
} from "@artinet/sdk";

async function runStreamingTask() {
  const client = new A2AClient("https://your-a2a-server.com/a2a");
  const message: Message = {
    role: "user",
    parts: [{ kind: "text", text: "Tell me a short story." }],
    ...
  };

  const stream = client.sendStreamingMessage({
    message,
  });

  for await (const update of stream) {
    if ((update as TaskStatusUpdateEvent).status) {
      console.log("Status:", (update as TaskStatusUpdateEvent).status.state);
    } else if ((update as TaskArtifactUpdateEvent).artifact) {
      console.log(
        "Artifact:",
        (update as TaskArtifactUpdateEvent).artifact.name
      );
    }
  }
  console.log("Stream finished.");
}

Authentication

Add headers using addHeader or setHeaders.

import { A2AClient } from "@artinet/sdk";

const client = new A2AClient("https://your-secure-a2a-server.com/a2a");

// Add a single header
client.addHeader("Authorization", "Bearer your-api-token");

// Set multiple headers (overwrites existing)
client.setHeaders({ Authorization: "Bearer ...", "X-Custom": "value" });

Server

Use createAgentServer() to embed your Agents in an Express App.

Implementing an A2A Agent

The SDK provides a variety of options for creating complex (AgentEngines) or simple agents (AgentBuilder).

AgentBuilder

Option 1: Using the AgentBuilder (Recommended for Simple Workflows)

For simple agents with one or more processing steps, use the AgentBuilder pattern:

import { createAgentServer, AgentBuilder, TaskManager } from "@artinet/sdk";

//create a simple agent
const simpleAgent = AgentBuilder().text(() => "hello world!");

//or design a powerful multi-step agent
const { app, agent } = createAgentServer({
  agent: AgentBuilder()
    .text(({ command, context }) => {
      const userMessage =
        command.message.parts[0]?.kind === "text"
          ? command.message.parts[0].text
          : "";
      return {
        parts: [`Processing: ${userMessage}`], //parts are immediately sent back to the caller as TaskStatusUpdateEvents
        args: [userMessage], //args are passed to the next step
      };
    })
    .file(({ command, args }) => {
      const processedText = args[0];
      return {
        parts: [
          {
            name: "result.txt",
            mimeType: "text/plain",
            bytes: `Processed result: ${processedText}`,
          },
        ],
        args: ["file-generated"],
      };
    })
    .text(({ command, args }) => {
      const status = args[0];
      return `Task completed. Status: ${status}`;
    }) //A final Task is returned to the caller which contains all of the emitted parts.
    .createAgent({
      agentCard: {
        name: "Multi-Step Agent",
        url: "http://localhost:3000/a2a",
        version: "1.0.0",
        capabilities: { streaming: true },
        skills: [{ id: "multi-processor", name: "Multi-Step Processor" }],
      },
      tasks: new TaskManager(),
    }),
  basePath: "/a2a",
});

app.listen(3000, () => {
  console.log("Multi-Step A2A Server running on http://localhost:3000/a2a");
});

The AgentBuilder approach is particularly useful when you need:

  • Step-by-step processing: Break down complex tasks into discrete, manageable steps
  • Data flow between steps: Pass results from one step to the next using the args parameter
  • Different content types: Mix text, file, and data processing in a single flow
  • Reusable components: Build modular agents that can be easily edited or extended
AgentEngine

Option 2: Direct AgentEngine Implementation

When you need full control over the execution flow, implement an AgentEngine directly:

import {
  createAgentServer,
  Context,
  AgentEngine,
  TaskManager,
} from "@artinet/sdk";

const myAgent: AgentEngine = async function* (context: Context) {
  const task: TaskAndHistory = context.events.getState();
  yield {
    state: "working",
    message: {
      ...
      role: "agent",
      parts: [{ kind: "text", text: "Processing..." }],
    },
    ...
  };
  yield {
    ...
    name: "result.txt",
    mimeType: "text/plain",
    parts: [{ kind: "text", text: "Report data" }],
  };

  yield {
    ...
    state: "completed",
    message: {
      kind: "message"
      role: "agent",
      parts: [{ kind: "text", text: "Finished processing." }],
      ...
    },
  };
};

const { app, agent } = createAgentServer({
  agent: {
    engine: myAgent,
    agentCard: {
      name: "Example Agent",
      url: "http://localhost:3000/a2a",
      version: "1.0.0",
      capabilities: { streaming: true },
      skills: [{ id: "processor", name: "Text Processor" }],
      ...
    },
    tasks: new TaskManager(),
  },
  basePath: "/a2a",
  agentCardPath: "/.well-known/agent-card.json",
});

Event Handling/Monitoring & Message Streaming

The SDK provides comprehensive event handling & message streaming capabilities that allow you to modify agent execution, subscribe to events, stream commands, and respond to state changes in real-time.

Override Event Behaviour

When using the service layer, you can provide your own Event Handlers:

import { createAgent, TaskManager, ContextManager, Command, SendCommandInterface } from "@artinet/sdk";

const customContextManager = new ContextManger();
const agent = createAgent({
  engine: (context: Context){
    context.events.on("update", (currentState, nextState) => {
      //allow other processes to subscribe to your agent
    })
    ...
    //handle command streams directly within an agent
    for await (const command of context.command) {
      console.log("new command recieved: ", command);
      //will continue polling until the command stream is closed by calling command.close();
    }
    //or process them asynchronously
    context.command.on("send", (command) => {
      ...
    });
  },
  agentCard: {
    name: "Event-Monitored Agent",
    url: "http://localhost:3000/a2a",
    version: "1.0.0",
    capabilities: { streaming: true },
    skills: [{ id: "monitor", name: "Monitored Agent" }],
  },
  contexts: customContextManager,
  tasks: new TaskManager(),
  eventOverrides: { //for even greater control create your own Event Handlers
    onStart: async (context) => {
      ...
      return currentState;
    },
    onUpdate: async (currentState, nextState) => {
      ...
      return currentState;
    },
    ...
  },
});

const result = await agent.sendMessage({
  contextId: "123"
  ...
});

const currentContext = customContextManager.getContext("123");

//subscribe to the events from a specific context
currentContext.events.on("complete", () {
  ...
  //errors thrown here will be triggered in the original context
});

//stream new commands into a running context
(currentContext.command as SendCommandInterface<Command>).send({
  ...
});

currentContext.command.close();

Available Event Types

The EventManager supports the following event types:

  • OnStart/start: Fired when agent execution begins
  • OnUpdate/update: Fired on each state update during execution
  • OnError/error: Fired when an error occurs during execution
  • OnComplete/complete: Fired when agent execution completes successfully
  • OnCancel/cancel: Fired when agent execution is cancelled

Persistent Storage

For storage, use our out of the box storage providers like FileStore. Or implement the Store interface to create your own.

import path from "path";
import fs from "fs";
import { FileStore } from "@artinet/sdk";

//make sure the directory exists
const dataDir = path.join(process.cwd(), "a2a-data");
if (!fs.existsSync(dataDir)) {
  fs.mkdirSync(dataDir, { recursive: true });
}

const myStore = new FileStore(dataDir);

const { app, agent } = createAgentServer({
  agent: {
    engine: myAgent,
    agentCard: {
      ...
    },
    tasks: myStore,
  },
  ...
});

Advanced Server Customization

Our new architecture provides multiple ways to customize your agent server:

1. Using createAgentServer: Easily spin up an A2A Express app createAgentServer():

const initialApp = express();

// custom middleware
initialApp.use((req, res, next) => {
  ...
  next();
});

const { app, agent } = createAgentServer({
  app: initialApp
  agent: {
    ...
  },
});

// more custom middleware
app.use("/custom", (req, res, next) => {
  ...
});

2. Use the JSON-RPC Middleware: Directly import our preconfigured JSON-RPC middleware:

import express from "express";
import { createAgent, jsonRPCMiddleware, errorHandler, InMemoryTaskStore } from "@artinet/sdk";

const customApp = express();

const agent = createAgent({
  engine: myAgentLogic,
  agentCard: {
    ...
  },
  tasks: new InMemoryTaskStore(),
});

customApp.use("/auth", yourAuthMiddleware);
customApp.use("/metrics", yourMetricsMiddleware);
customApp.use(express.json());

// Add the A2A middleware
customApp.post("/", async (req, res, next) => {
  return await jsonRPCMiddleware(agent, req, res, next);
});
// Dont forget to add error handling*
customApp.use(errorHandler);

// Serve the agent card
customApp.get("/.well-known/agent-card.json", (req, res) => {
  res.json(agent.agentCard);
});

// Start your custom server
const server = customApp.listen(3000, () => {
  console.log("Custom A2A server running on port 3000");
});

3. Using Custom Transport Layers: Use our preconfigured TRPC router, or create your own integration with WebSockets & other protocols:

import { createAgentRouter } from "@artinet/sdk";

const agentRouter = createAgentRouter();

Use the Agent: Directly invoke the agent to use it locally:

import { createAgent } from "@artinet/sdk";

const agent = createAgent({
  engine: myAgentLogic,
  agentCard: {
    ...
  },
  tasks: new InMemoryTaskStore(),
});

// Wrap these calls in your desired transport logic
const result = await agent.sendMessage({
  ...
});

// Directly process streams
const stream = agent.streamMessage({
  ...
});

for await (const update of stream) {
  ...
}

Important: When using custom implementations, ensure you handle:

  • Server-Sent Events (SSE) for message/stream and tasks/resubscribe
  • Agent card endpoints at /.well-known/agent-card.json
  • Proper error handling and JSON-RPC compliance
  • CORS headers if needed for web clients

Cross Protocol Support

MCP (Model Context Protocol) Integration

The SDK provides a Model Context Protocol (MCP) <-> A2A compatability layer.

Use createMCPAgent to expose your agent via MCP:

import { createMCPAgent, createAgent } from "@artinet/sdk";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

// Wrap your agent in an MCP Server
const mcpAgent = createMCPAgent({
  serverInfo: {
    name: "My MCP Agent",
    version: "1.0.0",
  },
  options: {
    ...
  },
  agent: createAgent({
    engine: myAgentEngine,
    agentCard: {
      name: "My Agent",
      url: "http://localhost:3000/a2a",
      version: "1.0.0",
      capabilities: { streaming: true },
      skills: [{ id: "helper", name: "Helper Agent" }],
    },
  }),
  agentCardUri: "agent://card", //customize the URI for your AgentCard
});

// The MCPAgent is a fully compliant MCP Server so you can use it as you normally would.
mcpAgent.registerTool({
  ...
});

await mcpAgent.connect(new StdioServerTransport());

Use an MCP Client to interact with an mcpAgent:

...
// Access the AgentCard as a Resource
const agentCard = await client.readResource({ uri: "agent://card" });

// or send messages via Tool Calling
const result = await client.callTool({
  name: "send-message",
  arguments: {
    ...
    message: {
      ...
      parts: [{ kind: "text", text: "Hello from MCP!" }],
    },
  },
});

MCP Tools & Resources:

  • send-message: Send messages to the A2A agent
  • get-task: Retrieve tasks by ID
  • cancel-task: Cancel a running task
  • agent://card: Retrieve the AgentCard
  • send-streaming-message, task-resubscribe & push-notifications etc are currently not supported by default.
    • Leverage the A2A Zod Schemas to implement them manually.

Contributing

Contributions are welcome! Please open an issue or submit a Pull Request on GitHub.

Ensure code adheres to the project style and passes linting (npm run lint) and tests (npm test).

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Acknowledgements

This SDK builds upon the concepts and specifications of the Agent2Agent (A2A) Protocol.

Libraries used include:

About

Express for AI Agents. Wrap your Agents in the Agent2Agent Protocol and allow them to communicate with any agent no matter the runtime or the ecosystem. All without any of the boilerplate. A Typescript SDK for Agentic Communication.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •