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 +135 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +231 -0
- package/package.json +33 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|