a2a-nextjs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @adriancooney/a2a-nextjs
2
+
3
+ A2A protocol handler for Next.js. Translates AI SDK streams to A2A events.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @adriancooney/a2a-nextjs @a2a-js/sdk
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Create an agent
14
+
15
+ ```typescript
16
+ // lib/agents/my-agent.ts
17
+ import { createAgent, InMemoryTaskStore } from "@adriancooney/a2a-nextjs"
18
+
19
+ export const myAgent = createAgent({
20
+ card: {
21
+ name: "my-agent",
22
+ description: "A helpful assistant",
23
+ skills: [
24
+ { id: "chat", name: "Chat", description: "General conversation" },
25
+ ],
26
+ },
27
+ taskStore: new InMemoryTaskStore(),
28
+ execute: async (prompt) => {
29
+ // Return a ReadableStream<UIMessageChunk>
30
+ },
31
+ })
32
+ ```
33
+
34
+ ### Create routes
35
+
36
+ ```typescript
37
+ // app/api/agent/route.ts
38
+ import { myAgent } from "@/lib/agents/my-agent"
39
+
40
+ export const POST = myAgent
41
+ ```
42
+
43
+ ```typescript
44
+ // app/.well-known/agent.json/route.ts
45
+ import { myAgent } from "@/lib/agents/my-agent"
46
+
47
+ export const GET = () => Response.json(myAgent.buildCard({ url: "/api/agent" }))
48
+ ```
49
+
50
+ ## With Workflow + DurableAgent
51
+
52
+ ```typescript
53
+ // lib/agents/my-agent.ts
54
+ import { createAgent, InMemoryTaskStore } from "@adriancooney/a2a-nextjs"
55
+ import { start, getRun } from "workflow/api"
56
+ import { runAgentWorkflow } from "@/lib/agent"
57
+
58
+ export const myAgent = createAgent({
59
+ card: {
60
+ name: "my-agent",
61
+ description: "A helpful assistant",
62
+ skills: [],
63
+ },
64
+ taskStore: new InMemoryTaskStore(),
65
+ execute: async (prompt) => {
66
+ const run = await start(runAgentWorkflow, [{ prompt }])
67
+ return getRun(run.runId).getReadable()
68
+ },
69
+ })
70
+ ```
71
+
72
+ ```typescript
73
+ // lib/agent.ts
74
+ import { DurableAgent } from "@workflow/ai/agent"
75
+ import { getWritable } from "workflow"
76
+ import type { UIMessageChunk } from "ai"
77
+
78
+ export async function runAgentWorkflow({ prompt }: { prompt: string }) {
79
+ "use workflow"
80
+
81
+ const writable = getWritable<UIMessageChunk>()
82
+
83
+ const agent = new DurableAgent({
84
+ model: "anthropic/claude-sonnet-4",
85
+ system: "You are a helpful assistant.",
86
+ tools: {},
87
+ })
88
+
89
+ await agent.stream({
90
+ messages: [{ role: "user", content: prompt }],
91
+ writable,
92
+ })
93
+ }
94
+ ```
95
+
96
+ ## API
97
+
98
+ ### `createAgent(config): AgentHandler`
99
+
100
+ Creates an agent handler.
101
+
102
+ **Config:**
103
+
104
+ - `card` - Agent metadata (name, description, skills)
105
+ - `taskStore` - Task persistence (`InMemoryTaskStore` or custom)
106
+ - `execute` - Returns `ReadableStream<UIMessageChunk>`
107
+
108
+ **Returns** a callable handler with:
109
+
110
+ - `(request: Request) => Promise<Response>` - POST handler
111
+ - `.buildCard({ url })` - Build full AgentCard
112
+
113
+ ### Types
114
+
115
+ ```typescript
116
+ interface AgentCardConfig {
117
+ name: string
118
+ description: string
119
+ version?: string
120
+ protocolVersion?: string
121
+ skills?: AgentSkillConfig[]
122
+ capabilities?: Record<string, unknown>
123
+ defaultInputModes?: string[]
124
+ defaultOutputModes?: string[]
125
+ }
126
+
127
+ interface UIMessageChunk {
128
+ type: string
129
+ delta?: string
130
+ toolCallId?: string
131
+ toolName?: string
132
+ output?: unknown
133
+ errorText?: string
134
+ }
135
+ ```
@@ -0,0 +1,43 @@
1
+ import { TaskStore } from '@a2a-js/sdk/server';
2
+ export { InMemoryTaskStore, TaskStore } from '@a2a-js/sdk/server';
3
+ import { AgentCard } from '@a2a-js/sdk';
4
+ export { AgentCard } from '@a2a-js/sdk';
5
+
6
+ interface UIMessageChunk {
7
+ type: string;
8
+ delta?: string;
9
+ toolCallId?: string;
10
+ toolName?: string;
11
+ output?: unknown;
12
+ errorText?: string;
13
+ }
14
+ interface AgentSkillConfig {
15
+ id: string;
16
+ name: string;
17
+ description?: string;
18
+ tags?: string[];
19
+ }
20
+ interface AgentCardConfig {
21
+ name: string;
22
+ description: string;
23
+ version?: string;
24
+ protocolVersion?: string;
25
+ skills?: AgentSkillConfig[];
26
+ capabilities?: Record<string, unknown>;
27
+ defaultInputModes?: string[];
28
+ defaultOutputModes?: string[];
29
+ }
30
+ interface AgentConfig {
31
+ card: AgentCardConfig;
32
+ taskStore: TaskStore;
33
+ execute: (prompt: string) => Promise<ReadableStream<UIMessageChunk>>;
34
+ }
35
+ interface AgentHandler {
36
+ (request: Request): Promise<Response>;
37
+ buildCard(config: {
38
+ url: string;
39
+ }): AgentCard;
40
+ }
41
+ declare function createAgent(config: AgentConfig): AgentHandler;
42
+
43
+ export { type AgentCardConfig, type AgentConfig, type AgentHandler, type AgentSkillConfig, type UIMessageChunk, createAgent };
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ // src/index.ts
2
+ import crypto from "crypto";
3
+ import {
4
+ DefaultRequestHandler,
5
+ JsonRpcTransportHandler
6
+ } from "@a2a-js/sdk/server";
7
+ import { InMemoryTaskStore } from "@a2a-js/sdk/server";
8
+ function statusUpdate(taskId, contextId, state, message, final) {
9
+ return {
10
+ kind: "status-update",
11
+ taskId,
12
+ contextId,
13
+ status: {
14
+ state,
15
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16
+ ...message && {
17
+ message: {
18
+ kind: "message",
19
+ role: "agent",
20
+ messageId: crypto.randomUUID(),
21
+ parts: [{ kind: "text", text: message }]
22
+ }
23
+ }
24
+ },
25
+ final: final ?? false
26
+ };
27
+ }
28
+ function artifactUpdate(taskId, contextId, artifact) {
29
+ return {
30
+ kind: "artifact-update",
31
+ taskId,
32
+ contextId,
33
+ artifact
34
+ };
35
+ }
36
+ async function translateAIStreamToA2A(stream, taskId, contextId, eventBus) {
37
+ const reader = stream.getReader();
38
+ let textBuffer = "";
39
+ const toolNames = /* @__PURE__ */ new Map();
40
+ let artifactIndex = 0;
41
+ try {
42
+ while (true) {
43
+ const { done, value } = await reader.read();
44
+ if (done) break;
45
+ const chunk = value;
46
+ switch (chunk.type) {
47
+ case "text-delta":
48
+ textBuffer += chunk.delta;
49
+ break;
50
+ case "tool-input-available":
51
+ toolNames.set(chunk.toolCallId, chunk.toolName);
52
+ eventBus.publish(
53
+ statusUpdate(taskId, contextId, "working", `Calling ${chunk.toolName}`)
54
+ );
55
+ break;
56
+ case "tool-output-available": {
57
+ const toolName = toolNames.get(chunk.toolCallId) || "tool";
58
+ let outputText;
59
+ if (typeof chunk.output === "string") {
60
+ outputText = chunk.output;
61
+ } else if (chunk.output?.output?.value) {
62
+ const value2 = chunk.output.output.value;
63
+ try {
64
+ const parsed = JSON.parse(value2);
65
+ outputText = parsed.output ?? value2;
66
+ } catch {
67
+ outputText = value2;
68
+ }
69
+ } else if (chunk.output?.output) {
70
+ const output = chunk.output.output;
71
+ outputText = typeof output === "string" ? output : JSON.stringify(output, null, 2);
72
+ } else {
73
+ outputText = JSON.stringify(chunk.output, null, 2);
74
+ }
75
+ eventBus.publish(
76
+ artifactUpdate(taskId, contextId, {
77
+ artifactId: `artifact_${chunk.toolCallId}`,
78
+ name: toolName,
79
+ description: `Output from ${toolName}`,
80
+ parts: [{ kind: "text", text: outputText }]
81
+ })
82
+ );
83
+ artifactIndex++;
84
+ break;
85
+ }
86
+ case "tool-output-error":
87
+ eventBus.publish(
88
+ artifactUpdate(taskId, contextId, {
89
+ artifactId: `artifact_error_${chunk.toolCallId}`,
90
+ name: "error",
91
+ description: "Tool execution error",
92
+ parts: [{ kind: "text", text: chunk.errorText || "Unknown error" }]
93
+ })
94
+ );
95
+ artifactIndex++;
96
+ break;
97
+ case "error":
98
+ eventBus.publish(statusUpdate(taskId, contextId, "failed", chunk.errorText, true));
99
+ eventBus.finished();
100
+ return;
101
+ }
102
+ }
103
+ reader.releaseLock();
104
+ if (textBuffer) {
105
+ eventBus.publish(
106
+ artifactUpdate(taskId, contextId, {
107
+ artifactId: `artifact_response_${artifactIndex}`,
108
+ name: "response",
109
+ description: "Agent response",
110
+ parts: [{ kind: "text", text: textBuffer }]
111
+ })
112
+ );
113
+ artifactIndex++;
114
+ }
115
+ const hasOutput = artifactIndex > 0;
116
+ const completionMessage = hasOutput ? void 0 : "Task completed with no output";
117
+ eventBus.publish(statusUpdate(taskId, contextId, "completed", completionMessage, true));
118
+ eventBus.finished();
119
+ } catch (error) {
120
+ eventBus.publish(statusUpdate(taskId, contextId, "failed", String(error), true));
121
+ eventBus.finished();
122
+ }
123
+ }
124
+ function createAIStreamExecutor(execute) {
125
+ return {
126
+ async execute(requestContext, eventBus) {
127
+ const { userMessage, taskId, contextId, task } = requestContext;
128
+ if (!task) {
129
+ const initialTask = {
130
+ kind: "task",
131
+ id: taskId,
132
+ contextId,
133
+ status: {
134
+ state: "submitted",
135
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
136
+ },
137
+ history: [userMessage],
138
+ artifacts: []
139
+ };
140
+ eventBus.publish(initialTask);
141
+ }
142
+ eventBus.publish(statusUpdate(taskId, contextId, "working"));
143
+ const prompt = userMessage.parts.filter((p) => p.kind === "text").map((p) => p.text).join("\n");
144
+ try {
145
+ const stream = await execute(prompt);
146
+ await translateAIStreamToA2A(stream, taskId, contextId, eventBus);
147
+ } catch (error) {
148
+ eventBus.publish(statusUpdate(taskId, contextId, "failed", String(error), true));
149
+ eventBus.finished();
150
+ }
151
+ },
152
+ async cancelTask(taskId, eventBus) {
153
+ eventBus.publish({
154
+ kind: "status-update",
155
+ taskId,
156
+ contextId: "",
157
+ status: {
158
+ state: "canceled",
159
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
160
+ },
161
+ final: true
162
+ });
163
+ eventBus.finished();
164
+ }
165
+ };
166
+ }
167
+ function isAsyncGenerator(obj) {
168
+ return obj !== null && typeof obj === "object" && Symbol.asyncIterator in obj;
169
+ }
170
+ async function* streamToSSE(generator) {
171
+ const encoder = new TextEncoder();
172
+ for await (const event of generator) {
173
+ yield encoder.encode(`data: ${JSON.stringify(event)}
174
+
175
+ `);
176
+ }
177
+ }
178
+ function createAgent(config) {
179
+ const { card, taskStore, execute } = config;
180
+ function buildCard({ url }) {
181
+ return {
182
+ name: card.name,
183
+ description: card.description,
184
+ url,
185
+ version: card.version ?? "1.0.0",
186
+ protocolVersion: card.protocolVersion ?? "0.2.0",
187
+ capabilities: card.capabilities ?? {},
188
+ defaultInputModes: card.defaultInputModes ?? ["text"],
189
+ defaultOutputModes: card.defaultOutputModes ?? ["text"],
190
+ skills: (card.skills ?? []).map((s) => ({
191
+ id: s.id,
192
+ name: s.name,
193
+ description: s.description ?? "",
194
+ tags: s.tags ?? []
195
+ }))
196
+ };
197
+ }
198
+ const executor = createAIStreamExecutor(execute);
199
+ const handler = async function handler2(request) {
200
+ const url = new URL(request.url);
201
+ const agentCard = buildCard({ url: url.pathname });
202
+ const requestHandler = new DefaultRequestHandler(agentCard, taskStore, executor);
203
+ const transport = new JsonRpcTransportHandler(requestHandler);
204
+ const body = await request.json();
205
+ const result = await transport.handle(body);
206
+ if (isAsyncGenerator(result)) {
207
+ const stream = new ReadableStream({
208
+ async pull(controller) {
209
+ for await (const chunk of streamToSSE(result)) {
210
+ controller.enqueue(chunk);
211
+ }
212
+ controller.close();
213
+ }
214
+ });
215
+ return new Response(stream, {
216
+ headers: {
217
+ "Content-Type": "text/event-stream",
218
+ "Cache-Control": "no-cache",
219
+ Connection: "keep-alive"
220
+ }
221
+ });
222
+ }
223
+ return Response.json(result);
224
+ };
225
+ handler.buildCard = buildCard;
226
+ return handler;
227
+ }
228
+ export {
229
+ InMemoryTaskStore,
230
+ createAgent
231
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "a2a-nextjs",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "peerDependencies": {
24
+ "@a2a-js/sdk": "^0.3.9"
25
+ },
26
+ "devDependencies": {
27
+ "@a2a-js/sdk": "^0.3.9",
28
+ "@types/node": "^22.0.0",
29
+ "tsup": "^8.4.0",
30
+ "typescript": "^5.0.0",
31
+ "vitest": "^4.0.18"
32
+ }
33
+ }