@zhin.js/a2a 1.0.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/lib/a2a-parts.d.ts +10 -0
- package/lib/a2a-parts.d.ts.map +1 -0
- package/lib/a2a-parts.js +57 -0
- package/lib/a2a-parts.js.map +1 -0
- package/lib/agent-executor.d.ts +16 -0
- package/lib/agent-executor.d.ts.map +1 -0
- package/lib/agent-executor.js +125 -0
- package/lib/agent-executor.js.map +1 -0
- package/lib/auth.d.ts +8 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +20 -0
- package/lib/auth.js.map +1 -0
- package/lib/card-builder.d.ts +8 -0
- package/lib/card-builder.d.ts.map +1 -0
- package/lib/card-builder.js +87 -0
- package/lib/card-builder.js.map +1 -0
- package/lib/config.d.ts +15 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +26 -0
- package/lib/config.js.map +1 -0
- package/lib/http-handlers.d.ts +10 -0
- package/lib/http-handlers.d.ts.map +1 -0
- package/lib/http-handlers.js +183 -0
- package/lib/http-handlers.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +130 -0
- package/lib/index.js.map +1 -0
- package/lib/message-parts.d.ts +7 -0
- package/lib/message-parts.d.ts.map +1 -0
- package/lib/message-parts.js +7 -0
- package/lib/message-parts.js.map +1 -0
- package/lib/rest-transport-handler.d.ts +20 -0
- package/lib/rest-transport-handler.d.ts.map +1 -0
- package/lib/rest-transport-handler.js +65 -0
- package/lib/rest-transport-handler.js.map +1 -0
- package/package.json +67 -0
- package/src/a2a-parts.ts +73 -0
- package/src/agent-executor.ts +160 -0
- package/src/auth.ts +23 -0
- package/src/card-builder.ts +101 -0
- package/src/config.ts +31 -0
- package/src/http-handlers.ts +237 -0
- package/src/index.ts +167 -0
- package/src/message-parts.ts +12 -0
- package/src/rest-transport-handler.ts +81 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZhinA2AExecutor — runs inbound A2A tasks via bound ZhinAgent.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import type { Task } from '@a2a-js/sdk';
|
|
6
|
+
import { TaskState } from '@a2a-js/sdk';
|
|
7
|
+
import {
|
|
8
|
+
AgentEvent,
|
|
9
|
+
type AgentExecutor,
|
|
10
|
+
type ExecutionEventBus,
|
|
11
|
+
type RequestContext,
|
|
12
|
+
} from '@a2a-js/sdk/server';
|
|
13
|
+
import { createSyntheticMessage } from '@zhin.js/core';
|
|
14
|
+
import type { ZhinAgent } from '@zhin.js/agent';
|
|
15
|
+
import type { ResolvedAgentBinding } from '@zhin.js/agent/config';
|
|
16
|
+
import { agentTextMessage, partsToPromptText, textPart } from './a2a-parts.js';
|
|
17
|
+
|
|
18
|
+
export interface ZhinA2AExecutorOptions {
|
|
19
|
+
agentName: string;
|
|
20
|
+
getAgent: () => ZhinAgent | null;
|
|
21
|
+
resolveBinding: () => ResolvedAgentBinding | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function outputElementsToText(elements: Array<{ type: string; content?: string }>): string {
|
|
25
|
+
return elements
|
|
26
|
+
.map((el) => (el.type === 'text' ? el.content || '' : ''))
|
|
27
|
+
.join('\n')
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function initialTask(requestContext: RequestContext, agentName: string): Task {
|
|
32
|
+
return {
|
|
33
|
+
id: requestContext.taskId,
|
|
34
|
+
contextId: requestContext.contextId,
|
|
35
|
+
status: {
|
|
36
|
+
state: TaskState.TASK_STATE_WORKING,
|
|
37
|
+
message: undefined,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
},
|
|
40
|
+
artifacts: [],
|
|
41
|
+
history: [requestContext.userMessage],
|
|
42
|
+
metadata: { zhinAgent: agentName },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class ZhinA2AExecutor implements AgentExecutor {
|
|
47
|
+
private readonly running = new Set<string>();
|
|
48
|
+
|
|
49
|
+
constructor(private readonly options: ZhinA2AExecutorOptions) {}
|
|
50
|
+
|
|
51
|
+
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
|
|
52
|
+
const agent = this.options.getAgent();
|
|
53
|
+
const binding = this.options.resolveBinding();
|
|
54
|
+
if (!agent || !binding) {
|
|
55
|
+
const failed: Task = {
|
|
56
|
+
...initialTask(requestContext, this.options.agentName),
|
|
57
|
+
status: {
|
|
58
|
+
state: TaskState.TASK_STATE_FAILED,
|
|
59
|
+
message: agentTextMessage(
|
|
60
|
+
randomUUID(),
|
|
61
|
+
requestContext.contextId,
|
|
62
|
+
`Agent "${this.options.agentName}" not ready`,
|
|
63
|
+
requestContext.taskId,
|
|
64
|
+
),
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
eventBus.publish(AgentEvent.task(failed));
|
|
69
|
+
eventBus.finished();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const task = initialTask(requestContext, this.options.agentName);
|
|
74
|
+
eventBus.publish(AgentEvent.task(task));
|
|
75
|
+
this.running.add(requestContext.taskId);
|
|
76
|
+
|
|
77
|
+
const prompt = partsToPromptText(requestContext.userMessage.parts);
|
|
78
|
+
agent.configure({ activeBinding: binding });
|
|
79
|
+
|
|
80
|
+
const commMessage = createSyntheticMessage({
|
|
81
|
+
adapter: 'a2a',
|
|
82
|
+
endpoint: this.options.agentName,
|
|
83
|
+
sender: { id: 'a2a-client', isMaster: true },
|
|
84
|
+
channel: { type: 'private', id: requestContext.contextId },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const output = await agent.prompt(prompt, commMessage);
|
|
89
|
+
const resultText = outputElementsToText(output as Array<{ type: string; content?: string }>)
|
|
90
|
+
|| '(A2A 任务已完成,Agent 未返回文本)';
|
|
91
|
+
|
|
92
|
+
eventBus.publish(AgentEvent.artifactUpdate({
|
|
93
|
+
taskId: task.id,
|
|
94
|
+
contextId: task.contextId,
|
|
95
|
+
artifact: {
|
|
96
|
+
artifactId: randomUUID(),
|
|
97
|
+
name: 'result',
|
|
98
|
+
description: '',
|
|
99
|
+
parts: [textPart(resultText)],
|
|
100
|
+
metadata: undefined,
|
|
101
|
+
extensions: [],
|
|
102
|
+
},
|
|
103
|
+
append: false,
|
|
104
|
+
lastChunk: true,
|
|
105
|
+
metadata: undefined,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
eventBus.publish(AgentEvent.statusUpdate({
|
|
109
|
+
taskId: task.id,
|
|
110
|
+
contextId: task.contextId,
|
|
111
|
+
status: {
|
|
112
|
+
state: TaskState.TASK_STATE_COMPLETED,
|
|
113
|
+
message: agentTextMessage(
|
|
114
|
+
randomUUID(),
|
|
115
|
+
requestContext.contextId,
|
|
116
|
+
resultText,
|
|
117
|
+
task.id,
|
|
118
|
+
),
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
},
|
|
121
|
+
metadata: undefined,
|
|
122
|
+
}));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
125
|
+
eventBus.publish(AgentEvent.statusUpdate({
|
|
126
|
+
taskId: task.id,
|
|
127
|
+
contextId: task.contextId,
|
|
128
|
+
status: {
|
|
129
|
+
state: TaskState.TASK_STATE_FAILED,
|
|
130
|
+
message: agentTextMessage(
|
|
131
|
+
randomUUID(),
|
|
132
|
+
requestContext.contextId,
|
|
133
|
+
errorText,
|
|
134
|
+
task.id,
|
|
135
|
+
),
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
},
|
|
138
|
+
metadata: undefined,
|
|
139
|
+
}));
|
|
140
|
+
} finally {
|
|
141
|
+
this.running.delete(requestContext.taskId);
|
|
142
|
+
eventBus.finished();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {
|
|
147
|
+
this.running.delete(taskId);
|
|
148
|
+
eventBus.publish(AgentEvent.statusUpdate({
|
|
149
|
+
taskId,
|
|
150
|
+
contextId: '',
|
|
151
|
+
status: {
|
|
152
|
+
state: TaskState.TASK_STATE_CANCELED,
|
|
153
|
+
message: undefined,
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
},
|
|
156
|
+
metadata: undefined,
|
|
157
|
+
}));
|
|
158
|
+
eventBus.finished();
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer auth for A2A HTTP endpoints (reuses MCP mesh-auth timing-safe compare).
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage } from 'node:http';
|
|
5
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export function timingSafeEqualString(a: string, b: string): boolean {
|
|
8
|
+
if (!a || !b) return false;
|
|
9
|
+
const bufA = Buffer.from(a);
|
|
10
|
+
const bufB = Buffer.from(b);
|
|
11
|
+
if (bufA.length !== bufB.length) return false;
|
|
12
|
+
return timingSafeEqual(bufA, bufB);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function extractBearerToken(req: IncomingMessage): string {
|
|
16
|
+
const auth = req.headers.authorization ?? '';
|
|
17
|
+
return auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function verifyA2aBearer(req: IncomingMessage, expectedToken: string): boolean {
|
|
21
|
+
if (!expectedToken) return false;
|
|
22
|
+
return timingSafeEqualString(expectedToken, extractBearerToken(req));
|
|
23
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build A2A AgentCard per ai.agents binding.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentCard, AgentSkill } from '@a2a-js/sdk';
|
|
5
|
+
import { A2A_PROTOCOL_VERSION } from '@a2a-js/sdk';
|
|
6
|
+
import type { AgentBindingRegistry } from '@zhin.js/agent/config';
|
|
7
|
+
import { a2aAgentCardUrl, a2aJsonRpcUrl, a2aRestUrl } from './config.js';
|
|
8
|
+
|
|
9
|
+
function makeSkill(partial: Pick<AgentSkill, 'id' | 'name' | 'description'> & { tags?: string[] }): AgentSkill {
|
|
10
|
+
return {
|
|
11
|
+
...partial,
|
|
12
|
+
tags: partial.tags ?? [],
|
|
13
|
+
examples: [],
|
|
14
|
+
inputModes: ['text/plain'],
|
|
15
|
+
outputModes: ['text/plain'],
|
|
16
|
+
securityRequirements: [{ schemes: { bearer: { list: [] } } }],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SKILLS: AgentSkill[] = [
|
|
21
|
+
makeSkill({
|
|
22
|
+
id: 'delegate',
|
|
23
|
+
name: 'Delegate Task',
|
|
24
|
+
description: 'Accept structured task delegation via A2A Send Message',
|
|
25
|
+
tags: ['orchestration', 'delegate'],
|
|
26
|
+
}),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function buildAgentCardForBinding(
|
|
30
|
+
agentName: string,
|
|
31
|
+
registry: AgentBindingRegistry,
|
|
32
|
+
publicBaseUrl: string,
|
|
33
|
+
): AgentCard | null {
|
|
34
|
+
const binding = registry.getBinding(agentName);
|
|
35
|
+
if (!binding) return null;
|
|
36
|
+
|
|
37
|
+
const nickname = binding.nickname ?? agentName;
|
|
38
|
+
const jsonRpcUrl = a2aJsonRpcUrl(publicBaseUrl, agentName);
|
|
39
|
+
const restUrl = a2aRestUrl(publicBaseUrl, agentName);
|
|
40
|
+
|
|
41
|
+
const skills: AgentSkill[] = [
|
|
42
|
+
makeSkill({
|
|
43
|
+
id: agentName,
|
|
44
|
+
name: nickname,
|
|
45
|
+
description: `Zhin agent "${agentName}" (${binding.providerAlias}/${binding.model})`,
|
|
46
|
+
tags: ['zhin', agentName],
|
|
47
|
+
}),
|
|
48
|
+
...DEFAULT_SKILLS,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: nickname,
|
|
53
|
+
description: `Zhin.js A2A agent "${agentName}" — provider ${binding.providerAlias}, model ${binding.model}`,
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
supportedInterfaces: [
|
|
56
|
+
{
|
|
57
|
+
url: jsonRpcUrl,
|
|
58
|
+
protocolBinding: 'JSONRPC',
|
|
59
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
60
|
+
tenant: '',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
url: restUrl,
|
|
64
|
+
protocolBinding: 'HTTP+JSON',
|
|
65
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
66
|
+
tenant: '',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
provider: {
|
|
70
|
+
organization: 'Zhin.js',
|
|
71
|
+
url: 'https://github.com/zhinjs/zhin',
|
|
72
|
+
},
|
|
73
|
+
capabilities: {
|
|
74
|
+
streaming: true,
|
|
75
|
+
pushNotifications: false,
|
|
76
|
+
extensions: [],
|
|
77
|
+
},
|
|
78
|
+
securitySchemes: {
|
|
79
|
+
bearer: {
|
|
80
|
+
scheme: {
|
|
81
|
+
$case: 'httpAuthSecurityScheme',
|
|
82
|
+
value: {
|
|
83
|
+
description: 'Bearer token (http.token)',
|
|
84
|
+
scheme: 'Bearer',
|
|
85
|
+
bearerFormat: '',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
securityRequirements: [{ schemes: { bearer: { list: [] } } }],
|
|
91
|
+
defaultInputModes: ['text/plain', 'application/json'],
|
|
92
|
+
defaultOutputModes: ['text/plain'],
|
|
93
|
+
skills,
|
|
94
|
+
signatures: [],
|
|
95
|
+
documentationUrl: a2aAgentCardUrl(publicBaseUrl, agentName),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function listExposableAgentNames(registry: AgentBindingRegistry): string[] {
|
|
100
|
+
return registry.listAgentNames();
|
|
101
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build public base URL for Agent Card interfaces.
|
|
3
|
+
*/
|
|
4
|
+
export function resolvePublicBaseUrl(config: {
|
|
5
|
+
http?: { host?: string; port?: number; publicUrl?: string };
|
|
6
|
+
}): string {
|
|
7
|
+
const http = config.http ?? {};
|
|
8
|
+
if (http.publicUrl?.trim()) {
|
|
9
|
+
return http.publicUrl.trim().replace(/\/$/, '');
|
|
10
|
+
}
|
|
11
|
+
const host = http.host?.trim() || '127.0.0.1';
|
|
12
|
+
const port = http.port ?? 8086;
|
|
13
|
+
const hostname = host === '0.0.0.0' ? '127.0.0.1' : host;
|
|
14
|
+
return `http://${hostname}:${port}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function a2aAgentBasePath(agentName: string): string {
|
|
18
|
+
return `/a2a/${encodeURIComponent(agentName)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function a2aJsonRpcUrl(publicBase: string, agentName: string): string {
|
|
22
|
+
return `${publicBase}${a2aAgentBasePath(agentName)}/jsonrpc`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function a2aRestUrl(publicBase: string, agentName: string): string {
|
|
26
|
+
return `${publicBase}${a2aAgentBasePath(agentName)}/rest`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function a2aAgentCardUrl(publicBase: string, agentName: string): string {
|
|
30
|
+
return `${publicBase}${a2aAgentBasePath(agentName)}/.well-known/agent-card.json`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP handlers for A2A JSON-RPC, REST, and Agent Card (Node http — no Express).
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import {
|
|
7
|
+
A2A_CONTENT_TYPE,
|
|
8
|
+
A2A_VERSION_HEADER,
|
|
9
|
+
HTTP_EXTENSION_HEADER,
|
|
10
|
+
} from '@a2a-js/sdk';
|
|
11
|
+
import {
|
|
12
|
+
JsonRpcTransportHandler,
|
|
13
|
+
ServerCallContext,
|
|
14
|
+
UnauthenticatedUser,
|
|
15
|
+
validateVersion,
|
|
16
|
+
} from '@a2a-js/sdk/server';
|
|
17
|
+
import { Extensions } from '@a2a-js/sdk';
|
|
18
|
+
import type { A2ARequestHandler } from '@a2a-js/sdk/server';
|
|
19
|
+
import { RestTransportHandler } from './rest-transport-handler.js';
|
|
20
|
+
|
|
21
|
+
const SSE_HEADERS: Record<string, string> = {
|
|
22
|
+
'Content-Type': 'text/event-stream',
|
|
23
|
+
'Cache-Control': 'no-cache',
|
|
24
|
+
Connection: 'keep-alive',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function formatSSEEvent(data: unknown): string {
|
|
28
|
+
return `data: ${JSON.stringify(data)}\n\n`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatSSEErrorEvent(data: unknown): string {
|
|
32
|
+
return `event: error\ndata: ${JSON.stringify(data)}\n\n`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
36
|
+
const chunks: Buffer[] = [];
|
|
37
|
+
for await (const chunk of req) {
|
|
38
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
39
|
+
}
|
|
40
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
41
|
+
if (!raw.trim()) return {};
|
|
42
|
+
return JSON.parse(raw) as unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildServerContext(req: IncomingMessage): ServerCallContext {
|
|
46
|
+
return new ServerCallContext({
|
|
47
|
+
requestedExtensions: Extensions.parseServiceParameter(
|
|
48
|
+
req.headers[HTTP_EXTENSION_HEADER.toLowerCase()] as string | undefined,
|
|
49
|
+
),
|
|
50
|
+
user: new UnauthenticatedUser(),
|
|
51
|
+
requestedVersion: req.headers[A2A_VERSION_HEADER.toLowerCase()] as string | undefined,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sendJson(res: ServerResponse, status: number, body: unknown, extraHeaders?: Record<string, string>): void {
|
|
56
|
+
res.writeHead(status, { 'Content-Type': 'application/json', ...extraHeaders });
|
|
57
|
+
res.end(JSON.stringify(body));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function computeETag(json: string): string {
|
|
61
|
+
const hash = createHash('sha256').update(json).digest('hex').slice(0, 16);
|
|
62
|
+
return `W/"${hash}"`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function handleAgentCard(
|
|
66
|
+
req: IncomingMessage,
|
|
67
|
+
res: ServerResponse,
|
|
68
|
+
requestHandler: A2ARequestHandler,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
if (req.method !== 'GET') {
|
|
71
|
+
sendJson(res, 405, { error: 'Method not allowed' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const agentCard = await requestHandler.getAgentCard();
|
|
76
|
+
const body = JSON.stringify(agentCard);
|
|
77
|
+
const etag = computeETag(body);
|
|
78
|
+
res.setHeader('ETag', etag);
|
|
79
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
80
|
+
if (req.headers['if-none-match'] === etag) {
|
|
81
|
+
res.writeHead(304);
|
|
82
|
+
res.end();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
sendJson(res, 200, agentCard);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function handleJsonRpc(
|
|
92
|
+
req: IncomingMessage,
|
|
93
|
+
res: ServerResponse,
|
|
94
|
+
requestHandler: A2ARequestHandler,
|
|
95
|
+
preParsedBody?: unknown,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
if (req.method !== 'POST') {
|
|
98
|
+
sendJson(res, 405, { error: 'Method not allowed', Allow: 'POST' });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const transport = new JsonRpcTransportHandler(requestHandler);
|
|
103
|
+
try {
|
|
104
|
+
const body = preParsedBody !== undefined ? preParsedBody : await readJsonBody(req);
|
|
105
|
+
const context = buildServerContext(req);
|
|
106
|
+
const agentCard = await requestHandler.getAgentCard();
|
|
107
|
+
validateVersion(context.requestedVersion, agentCard, 'JSONRPC');
|
|
108
|
+
|
|
109
|
+
const rpcResponseOrStream = await transport.handle(
|
|
110
|
+
body as string | Record<string, unknown>,
|
|
111
|
+
context,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (context.activatedExtensions) {
|
|
115
|
+
res.setHeader(HTTP_EXTENSION_HEADER, Array.from(context.activatedExtensions).join(', '));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (rpcResponseOrStream && typeof (rpcResponseOrStream as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function') {
|
|
119
|
+
const stream = rpcResponseOrStream as AsyncGenerator<unknown, void, undefined>;
|
|
120
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
121
|
+
let firstResult = await iterator.next();
|
|
122
|
+
|
|
123
|
+
for (const [key, value] of Object.entries(SSE_HEADERS)) {
|
|
124
|
+
res.setHeader(key, value);
|
|
125
|
+
}
|
|
126
|
+
if (!firstResult.done) {
|
|
127
|
+
res.write(formatSSEEvent(firstResult.value));
|
|
128
|
+
}
|
|
129
|
+
for (;;) {
|
|
130
|
+
const next = await iterator.next();
|
|
131
|
+
if (next.done) break;
|
|
132
|
+
res.write(formatSSEEvent(next.value));
|
|
133
|
+
}
|
|
134
|
+
if (!res.writableEnded) res.end();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sendJson(res, 200, rpcResponseOrStream);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const errorResponse = {
|
|
141
|
+
jsonrpc: '2.0',
|
|
142
|
+
id: null,
|
|
143
|
+
error: JsonRpcTransportHandler.mapToJSONRPCError(err),
|
|
144
|
+
};
|
|
145
|
+
sendJson(res, 200, errorResponse);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function handleRest(
|
|
150
|
+
req: IncomingMessage,
|
|
151
|
+
res: ServerResponse,
|
|
152
|
+
requestHandler: A2ARequestHandler,
|
|
153
|
+
subPath: string,
|
|
154
|
+
preParsedBody?: unknown,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
const transport = new RestTransportHandler(requestHandler);
|
|
157
|
+
const context = buildServerContext(req);
|
|
158
|
+
const method = req.method ?? 'GET';
|
|
159
|
+
const path = subPath.replace(/^\//, '');
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const agentCard = await requestHandler.getAgentCard();
|
|
163
|
+
validateVersion(context.requestedVersion, agentCard, 'HTTP+JSON');
|
|
164
|
+
|
|
165
|
+
if (method === 'GET' && (path === '' || path === 'v1/card')) {
|
|
166
|
+
sendJson(res, 200, await transport.getAgentCard(), { 'Content-Type': A2A_CONTENT_TYPE });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (method === 'POST' && path === 'v1/message:send') {
|
|
171
|
+
const params = preParsedBody !== undefined ? preParsedBody : await readJsonBody(req);
|
|
172
|
+
const result = await transport.sendMessage(params as never, context);
|
|
173
|
+
sendJson(res, 200, result, { 'Content-Type': A2A_CONTENT_TYPE });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (method === 'POST' && path === 'v1/message:stream') {
|
|
178
|
+
const params = preParsedBody !== undefined ? preParsedBody : await readJsonBody(req);
|
|
179
|
+
const stream = await transport.sendMessageStream(params as never, context);
|
|
180
|
+
for (const [key, value] of Object.entries(SSE_HEADERS)) {
|
|
181
|
+
res.setHeader(key, value);
|
|
182
|
+
}
|
|
183
|
+
for await (const event of stream) {
|
|
184
|
+
res.write(formatSSEEvent(event));
|
|
185
|
+
}
|
|
186
|
+
if (!res.writableEnded) res.end();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const taskGet = path.match(/^v1\/tasks\/([^/]+)$/);
|
|
191
|
+
if (method === 'GET' && taskGet) {
|
|
192
|
+
const historyLength = req.url?.includes('historyLength=')
|
|
193
|
+
? new URL(req.url, 'http://localhost').searchParams.get('historyLength') ?? undefined
|
|
194
|
+
: undefined;
|
|
195
|
+
const result = await transport.getTask(taskGet[1]!, context, historyLength ?? undefined);
|
|
196
|
+
sendJson(res, 200, result, { 'Content-Type': A2A_CONTENT_TYPE });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const taskCancel = path.match(/^v1\/tasks\/([^/]+):cancel$/);
|
|
201
|
+
if (method === 'POST' && taskCancel) {
|
|
202
|
+
const result = await transport.cancelTask(taskCancel[1]!, context);
|
|
203
|
+
sendJson(res, 200, result, { 'Content-Type': A2A_CONTENT_TYPE });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (method === 'GET' && path === 'v1/tasks') {
|
|
208
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
209
|
+
const query: Record<string, string | undefined> = {};
|
|
210
|
+
url.searchParams.forEach((v, k) => { query[k] = v; });
|
|
211
|
+
const result = await transport.listTasks(query, context);
|
|
212
|
+
sendJson(res, 200, result, { 'Content-Type': A2A_CONTENT_TYPE });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
sendJson(res, 404, { error: 'Not found' });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
sendJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function handleRestStreamError(
|
|
223
|
+
res: ServerResponse,
|
|
224
|
+
err: unknown,
|
|
225
|
+
requestId: unknown,
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
if (!res.headersSent) {
|
|
228
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
res.write(formatSSEErrorEvent({
|
|
232
|
+
jsonrpc: '2.0',
|
|
233
|
+
id: requestId ?? null,
|
|
234
|
+
error: JsonRpcTransportHandler.mapToJSONRPCError(err),
|
|
235
|
+
}));
|
|
236
|
+
if (!res.writableEnded) res.end();
|
|
237
|
+
}
|