@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -0
  4. package/lib/a2a-parts.d.ts +10 -0
  5. package/lib/a2a-parts.d.ts.map +1 -0
  6. package/lib/a2a-parts.js +57 -0
  7. package/lib/a2a-parts.js.map +1 -0
  8. package/lib/agent-executor.d.ts +16 -0
  9. package/lib/agent-executor.d.ts.map +1 -0
  10. package/lib/agent-executor.js +125 -0
  11. package/lib/agent-executor.js.map +1 -0
  12. package/lib/auth.d.ts +8 -0
  13. package/lib/auth.d.ts.map +1 -0
  14. package/lib/auth.js +20 -0
  15. package/lib/auth.js.map +1 -0
  16. package/lib/card-builder.d.ts +8 -0
  17. package/lib/card-builder.d.ts.map +1 -0
  18. package/lib/card-builder.js +87 -0
  19. package/lib/card-builder.js.map +1 -0
  20. package/lib/config.d.ts +15 -0
  21. package/lib/config.d.ts.map +1 -0
  22. package/lib/config.js +26 -0
  23. package/lib/config.js.map +1 -0
  24. package/lib/http-handlers.d.ts +10 -0
  25. package/lib/http-handlers.d.ts.map +1 -0
  26. package/lib/http-handlers.js +183 -0
  27. package/lib/http-handlers.js.map +1 -0
  28. package/lib/index.d.ts +2 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +130 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/message-parts.d.ts +7 -0
  33. package/lib/message-parts.d.ts.map +1 -0
  34. package/lib/message-parts.js +7 -0
  35. package/lib/message-parts.js.map +1 -0
  36. package/lib/rest-transport-handler.d.ts +20 -0
  37. package/lib/rest-transport-handler.d.ts.map +1 -0
  38. package/lib/rest-transport-handler.js +65 -0
  39. package/lib/rest-transport-handler.js.map +1 -0
  40. package/package.json +67 -0
  41. package/src/a2a-parts.ts +73 -0
  42. package/src/agent-executor.ts +160 -0
  43. package/src/auth.ts +23 -0
  44. package/src/card-builder.ts +101 -0
  45. package/src/config.ts +31 -0
  46. package/src/http-handlers.ts +237 -0
  47. package/src/index.ts +167 -0
  48. package/src/message-parts.ts +12 -0
  49. package/src/rest-transport-handler.ts +81 -0
package/src/index.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @zhin.js/a2a — A2A v1.0 server plugin for Zhin.js Host.
3
+ *
4
+ * Auto-exposes one Agent Card per `ai.agents[]` entry at:
5
+ * GET /a2a/{agentName}/.well-known/agent-card.json
6
+ * POST /a2a/{agentName}/jsonrpc
7
+ * * /a2a/{agentName}/rest/*
8
+ */
9
+ import { formatCompact, usePlugin } from '@zhin.js/core';
10
+ import type { Router, RouterContext } from '@zhin.js/host-router';
11
+ import { paramPath } from '@zhin.js/host-router';
12
+ import {
13
+ DefaultRequestHandler,
14
+ InMemoryTaskStore,
15
+ type A2ARequestHandler,
16
+ } from '@a2a-js/sdk/server';
17
+ import type { AgentCard } from '@a2a-js/sdk';
18
+ import { AGENT_CARD_PATH } from '@a2a-js/sdk';
19
+ import { getAgentRuntimeRegistry } from '@zhin.js/agent';
20
+ import type { AIService } from '@zhin.js/agent';
21
+ import { resolvePublicBaseUrl, a2aAgentBasePath } from './config.js';
22
+ import { verifyA2aBearer } from './auth.js';
23
+ import { buildAgentCardForBinding, listExposableAgentNames } from './card-builder.js';
24
+ import { ZhinA2AExecutor } from './agent-executor.js';
25
+ import { handleAgentCard, handleJsonRpc, handleRest } from './http-handlers.js';
26
+
27
+ interface AgentA2AStack {
28
+ agentName: string;
29
+ requestHandler: A2ARequestHandler;
30
+ agentCard: AgentCard;
31
+ }
32
+
33
+ const A2A_ROUTE = '/a2a/:agentName/:tail+';
34
+
35
+ const plugin = usePlugin();
36
+ const { root, useContext, logger, onDispose } = plugin;
37
+
38
+ let agentStacks = new Map<string, AgentA2AStack>();
39
+ let httpToken = '';
40
+ let publicBaseUrl = 'http://127.0.0.1:8086';
41
+
42
+ function rebuildAgentStacks(ai: AIService): void {
43
+ const registry = ai.getBindingRegistry();
44
+ publicBaseUrl = resolvePublicBaseUrl(
45
+ root.inject('config')?.get<{ http?: { host?: string; port?: number; publicUrl?: string } }>('zhin.config.yml') ?? {},
46
+ );
47
+ const runtime = getAgentRuntimeRegistry();
48
+ const primaryAgent = runtime.getDefault();
49
+ const next = new Map<string, AgentA2AStack>();
50
+
51
+ for (const agentName of listExposableAgentNames(registry)) {
52
+ const card = buildAgentCardForBinding(agentName, registry, publicBaseUrl);
53
+ if (!card || !primaryAgent) continue;
54
+
55
+ const executor = new ZhinA2AExecutor({
56
+ agentName,
57
+ getAgent: () => runtime.getDefault(),
58
+ resolveBinding: () => registry.getBinding(agentName),
59
+ });
60
+
61
+ const requestHandler = new DefaultRequestHandler(
62
+ card,
63
+ new InMemoryTaskStore(),
64
+ executor,
65
+ );
66
+
67
+ next.set(agentName, { agentName, requestHandler, agentCard: card });
68
+ }
69
+
70
+ agentStacks = next;
71
+ logger.info(formatCompact({ A2A: 'ready', agents: [...next.keys()].join(',') }));
72
+ }
73
+
74
+ function parseA2aTail(tail: string): { kind: 'card' | 'jsonrpc' | 'rest'; restSubPath?: string } | null {
75
+ const normalized = tail.replace(/^\/+/, '');
76
+ if (normalized === `.well-known/${AGENT_CARD_PATH}` || normalized === '.well-known/agent-card.json') {
77
+ return { kind: 'card' };
78
+ }
79
+ if (normalized === 'jsonrpc' || normalized.startsWith('jsonrpc/')) {
80
+ return { kind: 'jsonrpc' };
81
+ }
82
+ if (normalized === 'rest' || normalized.startsWith('rest/')) {
83
+ const restSubPath = normalized === 'rest' ? '' : normalized.slice('rest/'.length);
84
+ return { kind: 'rest', restSubPath };
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function ensureStacks(ai: AIService | undefined): void {
90
+ if (agentStacks.size > 0 || !ai?.isReady()) return;
91
+ rebuildAgentStacks(ai);
92
+ }
93
+
94
+ function sendJsonViaRawRes(ctx: RouterContext, status: number, body: unknown): void {
95
+ ctx.respond = false;
96
+ ctx.res.writeHead(status, { 'Content-Type': 'application/json' });
97
+ ctx.res.end(JSON.stringify(body));
98
+ }
99
+
100
+ useContext('router', (router: Router) => {
101
+ const configService = root.inject('config');
102
+ const appConfig = configService?.get<{ http?: { token?: string; host?: string; port?: number; publicUrl?: string } }>('zhin.config.yml') ?? {};
103
+ httpToken = appConfig.http?.token ?? '';
104
+ publicBaseUrl = resolvePublicBaseUrl(appConfig);
105
+
106
+ const ai = root.inject('ai') as AIService | undefined;
107
+ if (ai?.isReady()) rebuildAgentStacks(ai);
108
+
109
+ const a2aHandler = async (ctx: RouterContext): Promise<void> => {
110
+ const agentName = decodeURIComponent(ctx.params.agentName ?? '');
111
+ const parsed = parseA2aTail(paramPath(ctx, 'tail'));
112
+ if (!parsed) {
113
+ sendJsonViaRawRes(ctx, 404, { error: 'Not found' });
114
+ return;
115
+ }
116
+
117
+ const aiNow = root.inject('ai') as AIService | undefined;
118
+ ensureStacks(aiNow);
119
+
120
+ if (httpToken && !verifyA2aBearer(ctx.req, httpToken)) {
121
+ sendJsonViaRawRes(ctx, 401, { error: 'Unauthorized — Bearer token required' });
122
+ return;
123
+ }
124
+
125
+ const stack = agentStacks.get(agentName);
126
+ if (!stack) {
127
+ sendJsonViaRawRes(ctx, 404, { error: `A2A agent "${agentName}" not found` });
128
+ return;
129
+ }
130
+
131
+ ctx.respond = false;
132
+ const preParsedBody = ctx.method === 'POST' ? ctx.request.body : undefined;
133
+
134
+ try {
135
+ if (parsed.kind === 'card') {
136
+ await handleAgentCard(ctx.req, ctx.res, stack.requestHandler);
137
+ } else if (parsed.kind === 'jsonrpc') {
138
+ await handleJsonRpc(ctx.req, ctx.res, stack.requestHandler, preParsedBody);
139
+ } else {
140
+ await handleRest(ctx.req, ctx.res, stack.requestHandler, parsed.restSubPath ?? '', preParsedBody);
141
+ }
142
+ } catch (err) {
143
+ logger.error('A2A request error:', err);
144
+ if (!ctx.res.headersSent) {
145
+ sendJsonViaRawRes(ctx, 500, { error: 'Internal server error' });
146
+ }
147
+ }
148
+ };
149
+
150
+ router.all(A2A_ROUTE, a2aHandler);
151
+
152
+ logger.info(formatCompact({ A2A: 'listening', base: `${publicBaseUrl}${a2aAgentBasePath('{agent}')}` }));
153
+
154
+ onDispose(() => {
155
+ agentStacks.clear();
156
+ });
157
+ });
158
+
159
+ // Rebuild when AI service becomes ready (late init)
160
+ useContext('ai', (ai: AIService) => {
161
+ if (ai.isReady()) rebuildAgentStacks(ai);
162
+ return () => {
163
+ agentStacks.clear();
164
+ };
165
+ });
166
+
167
+ import type {} from '@zhin.js/host-router';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Convert A2A Message parts to a delegation prompt for ZhinAgent.
3
+ */
4
+ import type { Message } from '@a2a-js/sdk';
5
+
6
+ export { partsToPromptText } from './a2a-parts.js';
7
+
8
+ export function extractSkillId(message: Message): string | undefined {
9
+ const meta = message.metadata;
10
+ const skill = meta?.skillId ?? meta?.skill_id;
11
+ return typeof skill === 'string' && skill.trim() ? skill.trim() : undefined;
12
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Thin REST transport wrapper over A2ARequestHandler.
3
+ */
4
+ import type {
5
+ CancelTaskRequest,
6
+ GetTaskRequest,
7
+ ListTasksRequest,
8
+ SendMessageRequest,
9
+ } from '@a2a-js/sdk';
10
+ import {
11
+ RequestMalformedError,
12
+ UnsupportedOperationError,
13
+ } from '@a2a-js/sdk/server';
14
+ import type { A2ARequestHandler } from '@a2a-js/sdk/server';
15
+ import type { ServerCallContext } from '@a2a-js/sdk/server';
16
+ import { TaskState } from '@a2a-js/sdk';
17
+
18
+ export class RestTransportHandler {
19
+ constructor(private readonly requestHandler: A2ARequestHandler) {}
20
+
21
+ async getAgentCard() {
22
+ return this.requestHandler.getAgentCard();
23
+ }
24
+
25
+ async sendMessage(params: SendMessageRequest, context: ServerCallContext) {
26
+ this.validateSendMessageRequest(params);
27
+ return this.requestHandler.sendMessage(params, context);
28
+ }
29
+
30
+ async sendMessageStream(params: SendMessageRequest, context: ServerCallContext) {
31
+ await this.requireCapability('streaming');
32
+ this.validateSendMessageRequest(params);
33
+ return this.requestHandler.sendMessageStream(params, context);
34
+ }
35
+
36
+ async getTask(taskId: string, context: ServerCallContext, historyLength?: string) {
37
+ const params: GetTaskRequest = { id: taskId, tenant: '' };
38
+ if (historyLength !== undefined) {
39
+ params.historyLength = this.parseHistoryLength(historyLength);
40
+ }
41
+ return this.requestHandler.getTask(params, context);
42
+ }
43
+
44
+ async cancelTask(taskId: string, context: ServerCallContext) {
45
+ const params: CancelTaskRequest = { id: taskId, tenant: '', metadata: {} };
46
+ return this.requestHandler.cancelTask(params, context);
47
+ }
48
+
49
+ async listTasks(query: Record<string, string | undefined>, context: ServerCallContext) {
50
+ const params: ListTasksRequest = {
51
+ tenant: query.tenant ?? '',
52
+ contextId: query.contextId ?? '',
53
+ pageToken: query.pageToken ?? '',
54
+ status: TaskState.TASK_STATE_UNSPECIFIED,
55
+ statusTimestampAfter: undefined,
56
+ };
57
+ if (query.pageSize) params.pageSize = Number(query.pageSize);
58
+ if (query.historyLength) params.historyLength = Number(query.historyLength);
59
+ return this.requestHandler.listTasks(params, context);
60
+ }
61
+
62
+ private validateSendMessageRequest(params: SendMessageRequest): void {
63
+ if (!params.message) throw new RequestMalformedError('message is required');
64
+ if (!params.message.messageId) throw new RequestMalformedError('message.messageId is required');
65
+ }
66
+
67
+ private async requireCapability(capability: 'streaming' | 'pushNotifications'): Promise<void> {
68
+ const card = await this.getAgentCard();
69
+ if (!card.capabilities?.[capability]) {
70
+ throw new UnsupportedOperationError(`Agent does not support ${capability}`);
71
+ }
72
+ }
73
+
74
+ private parseHistoryLength(value: string): number {
75
+ const parsed = parseInt(value, 10);
76
+ if (Number.isNaN(parsed) || parsed < 0) {
77
+ throw new RequestMalformedError('historyLength must be a non-negative integer');
78
+ }
79
+ return parsed;
80
+ }
81
+ }