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.
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.
- artinet SDK
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
andworking
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
- 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.
- Then wrap it in an actual Express Server with the
- Protocol Compliance: Implements the complete A2A specification (v0.3.0) with support for any kind of transport layer (tRPC, WebSockets, etc).
npm install @artinet/sdk
- Node.js (v22.0.0 or higher recommended)
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);
npm test
Interact with A2A-compliant agents using the A2AClient
. See examples/
for more.
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);
}
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.");
}
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" });
Use createAgentServer()
to embed your Agents in an Express App.
The SDK provides a variety of options for creating complex (AgentEngines) or simple agents (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
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",
});
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 beginsOnUpdate
/update
: Fired on each state update during executionOnError
/error
: Fired when an error occurs during executionOnComplete
/complete
: Fired when agent execution completes successfullyOnCancel
/cancel
: Fired when agent execution is cancelled
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,
},
...
});
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
andtasks/resubscribe
- Agent card endpoints at
/.well-known/agent-card.json
- Proper error handling and JSON-RPC compliance
- CORS headers if needed for web clients
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 agentget-task
: Retrieve tasks by IDcancel-task
: Cancel a running taskagent://card
: Retrieve the AgentCardsend-streaming-message
,task-resubscribe
&push-notifications
etc are currently not supported by default.- Leverage the A2A Zod Schemas to implement them manually.
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
).
This project is licensed under the Apache License 2.0 - see the LICENSE
file for details.
This SDK builds upon the concepts and specifications of the Agent2Agent (A2A) Protocol.
Libraries used include:
- Express.js for the server framework.
- EventSource Parser for SSE streaming.