@supaku/agentfactory-server 0.7.52 → 0.7.55
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/dist/src/a2a-server.d.ts +88 -0
- package/dist/src/a2a-server.d.ts.map +1 -0
- package/dist/src/a2a-server.integration.test.d.ts +9 -0
- package/dist/src/a2a-server.integration.test.d.ts.map +1 -0
- package/dist/src/a2a-server.integration.test.js +397 -0
- package/dist/src/a2a-server.js +235 -0
- package/dist/src/a2a-server.test.d.ts +2 -0
- package/dist/src/a2a-server.test.d.ts.map +1 -0
- package/dist/src/a2a-server.test.js +311 -0
- package/dist/src/a2a-types.d.ts +125 -0
- package/dist/src/a2a-types.d.ts.map +1 -0
- package/dist/src/a2a-types.js +8 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/types.d.ts +1 -1
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Server Handlers
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic utilities for exposing AgentFactory fleet capabilities
|
|
5
|
+
* via the Agent-to-Agent (A2A) protocol. Provides AgentCard generation,
|
|
6
|
+
* JSON-RPC request routing, and SSE event formatting.
|
|
7
|
+
*
|
|
8
|
+
* Consuming applications wire these handlers into their own HTTP server
|
|
9
|
+
* (Express, Hono, Next.js, etc.) — this module has no framework dependency.
|
|
10
|
+
*/
|
|
11
|
+
import type { A2aAgentCard, A2aAuthScheme, A2aMessage, A2aSkill, A2aTask, A2aTaskEvent, JsonRpcRequest, JsonRpcResponse } from './a2a-types.js';
|
|
12
|
+
/** Configuration for building an AgentCard */
|
|
13
|
+
export interface A2aServerConfig {
|
|
14
|
+
/** Human-readable agent name */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Short description of the agent's purpose */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Base URL where A2A endpoints are exposed */
|
|
19
|
+
url: string;
|
|
20
|
+
/** Semantic version of the agent (defaults to '1.0.0') */
|
|
21
|
+
version?: string;
|
|
22
|
+
/** Explicit skill list; when omitted skills are derived from AgentWorkType */
|
|
23
|
+
skills?: A2aSkill[];
|
|
24
|
+
/** Whether the agent supports SSE streaming (defaults to false) */
|
|
25
|
+
streaming?: boolean;
|
|
26
|
+
/** Authentication schemes the endpoint accepts */
|
|
27
|
+
authSchemes?: A2aAuthScheme[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build an A2A AgentCard from the supplied configuration.
|
|
31
|
+
*
|
|
32
|
+
* If no explicit skills are provided the card is populated with skills
|
|
33
|
+
* derived from every known {@link AgentWorkType}.
|
|
34
|
+
*
|
|
35
|
+
* @param config - Server configuration
|
|
36
|
+
* @returns A fully-formed AgentCard
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildAgentCard(config: A2aServerConfig): A2aAgentCard;
|
|
39
|
+
/** Callbacks that the consuming application must supply */
|
|
40
|
+
export interface A2aHandlerOptions {
|
|
41
|
+
/** Handle an incoming message, optionally targeting an existing task */
|
|
42
|
+
onSendMessage: (message: A2aMessage, taskId?: string) => Promise<A2aTask>;
|
|
43
|
+
/** Retrieve an existing task by ID */
|
|
44
|
+
onGetTask: (taskId: string) => Promise<A2aTask | null>;
|
|
45
|
+
/** Cancel an existing task by ID */
|
|
46
|
+
onCancelTask: (taskId: string) => Promise<A2aTask | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Verify the Authorization header.
|
|
49
|
+
* Defaults to Bearer-token verification via {@link verifyApiKey}.
|
|
50
|
+
*/
|
|
51
|
+
verifyAuth?: (authHeader: string | undefined) => boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A framework-agnostic function that accepts a parsed JSON-RPC request
|
|
55
|
+
* (plus an optional Authorization header) and returns a JSON-RPC response.
|
|
56
|
+
*/
|
|
57
|
+
export type A2aRequestHandler = (request: JsonRpcRequest, authHeader?: string) => Promise<JsonRpcResponse>;
|
|
58
|
+
/**
|
|
59
|
+
* Create a framework-agnostic A2A request handler.
|
|
60
|
+
*
|
|
61
|
+
* The returned function processes a single JSON-RPC request and returns
|
|
62
|
+
* the corresponding response. The consuming application is responsible
|
|
63
|
+
* for HTTP parsing, serialisation, and transport.
|
|
64
|
+
*
|
|
65
|
+
* Supported methods:
|
|
66
|
+
* - `message/send` — send a message (optionally to an existing task)
|
|
67
|
+
* - `tasks/get` — retrieve a task by ID
|
|
68
|
+
* - `tasks/cancel` — cancel a task by ID
|
|
69
|
+
*
|
|
70
|
+
* @param options - Callbacks for task lifecycle and (optional) auth
|
|
71
|
+
* @returns An async handler function
|
|
72
|
+
*/
|
|
73
|
+
export declare function createA2aRequestHandler(options: A2aHandlerOptions): A2aRequestHandler;
|
|
74
|
+
/**
|
|
75
|
+
* Format an A2A task event as a Server-Sent Events (SSE) message.
|
|
76
|
+
*
|
|
77
|
+
* The output follows the SSE text/event-stream format:
|
|
78
|
+
* ```
|
|
79
|
+
* event: <type>
|
|
80
|
+
* data: <JSON payload>
|
|
81
|
+
*
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @param event - The task event to format
|
|
85
|
+
* @returns A string ready to write to an SSE response stream
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatSseEvent(event: A2aTaskEvent): string;
|
|
88
|
+
//# sourceMappingURL=a2a-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a-server.d.ts","sourceRoot":"","sources":["../../src/a2a-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,cAAc,EACd,eAAe,EAChB,MAAM,gBAAgB,CAAA;AAMvB,8CAA8C;AAC9C,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAA;IACnB,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAA;IACX,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAA;IACnB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,kDAAkD;IAClD,WAAW,CAAC,EAAE,aAAa,EAAE,CAAA;CAC9B;AA2ED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,YAAY,CAmBpE;AAMD,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACzE,sCAAsC;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IACtD,oCAAoC;IACpC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IACzD;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAA;CACzD;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAC9B,OAAO,EAAE,cAAc,EACvB,UAAU,CAAC,EAAE,MAAM,KAChB,OAAO,CAAC,eAAe,CAAC,CAAA;AAwC7B;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAoErF;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAG1D"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the A2A Server
|
|
3
|
+
*
|
|
4
|
+
* Wires the A2A request handler to mock task callbacks and verifies
|
|
5
|
+
* end-to-end JSON-RPC handling, AgentCard generation, SSE formatting,
|
|
6
|
+
* and auth integration.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=a2a-server.integration.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a-server.integration.test.d.ts","sourceRoot":"","sources":["../../src/a2a-server.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the A2A Server
|
|
3
|
+
*
|
|
4
|
+
* Wires the A2A request handler to mock task callbacks and verifies
|
|
5
|
+
* end-to-end JSON-RPC handling, AgentCard generation, SSE formatting,
|
|
6
|
+
* and auth integration.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import { buildAgentCard, createA2aRequestHandler, formatSseEvent, } from './a2a-server.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function makeRequest(method, params, id = 'req-1') {
|
|
14
|
+
return { jsonrpc: '2.0', id, method, params };
|
|
15
|
+
}
|
|
16
|
+
function makeMessage(text, role = 'user') {
|
|
17
|
+
return { role, parts: [{ type: 'text', text }] };
|
|
18
|
+
}
|
|
19
|
+
function makeTask(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
id: 'task-1',
|
|
22
|
+
status: 'submitted',
|
|
23
|
+
messages: [],
|
|
24
|
+
artifacts: [],
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// 1. Full message/send -> task lifecycle
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
describe('A2A Server — full task lifecycle', () => {
|
|
32
|
+
it('creates a task via message/send, retrieves it, and cancels it', async () => {
|
|
33
|
+
// In-memory task store
|
|
34
|
+
const tasks = new Map();
|
|
35
|
+
const onSendMessage = vi.fn(async (message, _taskId) => {
|
|
36
|
+
const task = {
|
|
37
|
+
id: 'task-lifecycle-1',
|
|
38
|
+
status: 'working',
|
|
39
|
+
messages: [message],
|
|
40
|
+
artifacts: [],
|
|
41
|
+
};
|
|
42
|
+
tasks.set(task.id, task);
|
|
43
|
+
return task;
|
|
44
|
+
});
|
|
45
|
+
const onGetTask = vi.fn(async (taskId) => {
|
|
46
|
+
return tasks.get(taskId) ?? null;
|
|
47
|
+
});
|
|
48
|
+
const onCancelTask = vi.fn(async (taskId) => {
|
|
49
|
+
const task = tasks.get(taskId);
|
|
50
|
+
if (!task)
|
|
51
|
+
return null;
|
|
52
|
+
task.status = 'canceled';
|
|
53
|
+
return task;
|
|
54
|
+
});
|
|
55
|
+
const handler = createA2aRequestHandler({
|
|
56
|
+
onSendMessage,
|
|
57
|
+
onGetTask,
|
|
58
|
+
onCancelTask,
|
|
59
|
+
verifyAuth: () => true,
|
|
60
|
+
});
|
|
61
|
+
// Step 1: Send message
|
|
62
|
+
const sendRes = await handler(makeRequest('message/send', { message: makeMessage('Build the feature') }));
|
|
63
|
+
expect(sendRes.error).toBeUndefined();
|
|
64
|
+
const createdTask = sendRes.result;
|
|
65
|
+
expect(createdTask.id).toBe('task-lifecycle-1');
|
|
66
|
+
expect(createdTask.status).toBe('working');
|
|
67
|
+
expect(createdTask.messages).toHaveLength(1);
|
|
68
|
+
expect(createdTask.messages[0].parts[0]).toMatchObject({ type: 'text', text: 'Build the feature' });
|
|
69
|
+
expect(onSendMessage).toHaveBeenCalledWith(makeMessage('Build the feature'), undefined);
|
|
70
|
+
// Step 2: Get task
|
|
71
|
+
const getRes = await handler(makeRequest('tasks/get', { taskId: 'task-lifecycle-1' }));
|
|
72
|
+
expect(getRes.error).toBeUndefined();
|
|
73
|
+
const retrievedTask = getRes.result;
|
|
74
|
+
expect(retrievedTask.id).toBe('task-lifecycle-1');
|
|
75
|
+
expect(retrievedTask.status).toBe('working');
|
|
76
|
+
expect(onGetTask).toHaveBeenCalledWith('task-lifecycle-1');
|
|
77
|
+
// Step 3: Cancel task
|
|
78
|
+
const cancelRes = await handler(makeRequest('tasks/cancel', { taskId: 'task-lifecycle-1' }));
|
|
79
|
+
expect(cancelRes.error).toBeUndefined();
|
|
80
|
+
const canceledTask = cancelRes.result;
|
|
81
|
+
expect(canceledTask.id).toBe('task-lifecycle-1');
|
|
82
|
+
expect(canceledTask.status).toBe('canceled');
|
|
83
|
+
expect(onCancelTask).toHaveBeenCalledWith('task-lifecycle-1');
|
|
84
|
+
});
|
|
85
|
+
it('returns -32001 when getting a non-existent task', async () => {
|
|
86
|
+
const handler = createA2aRequestHandler({
|
|
87
|
+
onSendMessage: vi.fn(async () => makeTask()),
|
|
88
|
+
onGetTask: vi.fn(async () => null),
|
|
89
|
+
onCancelTask: vi.fn(async () => null),
|
|
90
|
+
verifyAuth: () => true,
|
|
91
|
+
});
|
|
92
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'nonexistent' }));
|
|
93
|
+
expect(res.error?.code).toBe(-32001);
|
|
94
|
+
expect(res.error?.message).toBe('Task not found');
|
|
95
|
+
});
|
|
96
|
+
it('returns -32001 when canceling a non-existent task', async () => {
|
|
97
|
+
const handler = createA2aRequestHandler({
|
|
98
|
+
onSendMessage: vi.fn(async () => makeTask()),
|
|
99
|
+
onGetTask: vi.fn(async () => null),
|
|
100
|
+
onCancelTask: vi.fn(async () => null),
|
|
101
|
+
verifyAuth: () => true,
|
|
102
|
+
});
|
|
103
|
+
const res = await handler(makeRequest('tasks/cancel', { taskId: 'nonexistent' }));
|
|
104
|
+
expect(res.error?.code).toBe(-32001);
|
|
105
|
+
expect(res.error?.message).toBe('Task not found');
|
|
106
|
+
});
|
|
107
|
+
it('passes taskId to onSendMessage for follow-up messages', async () => {
|
|
108
|
+
const onSendMessage = vi.fn(async () => makeTask({ id: 'task-followup', status: 'working' }));
|
|
109
|
+
const handler = createA2aRequestHandler({
|
|
110
|
+
onSendMessage,
|
|
111
|
+
onGetTask: vi.fn(async () => null),
|
|
112
|
+
onCancelTask: vi.fn(async () => null),
|
|
113
|
+
verifyAuth: () => true,
|
|
114
|
+
});
|
|
115
|
+
await handler(makeRequest('message/send', {
|
|
116
|
+
message: makeMessage('Continue working'),
|
|
117
|
+
taskId: 'task-followup',
|
|
118
|
+
}));
|
|
119
|
+
expect(onSendMessage).toHaveBeenCalledWith(makeMessage('Continue working'), 'task-followup');
|
|
120
|
+
});
|
|
121
|
+
it('returns -32603 when onSendMessage throws an error', async () => {
|
|
122
|
+
const handler = createA2aRequestHandler({
|
|
123
|
+
onSendMessage: vi.fn(async () => { throw new Error('Queue overflow'); }),
|
|
124
|
+
onGetTask: vi.fn(async () => null),
|
|
125
|
+
onCancelTask: vi.fn(async () => null),
|
|
126
|
+
verifyAuth: () => true,
|
|
127
|
+
});
|
|
128
|
+
const res = await handler(makeRequest('message/send', { message: makeMessage('hello') }));
|
|
129
|
+
expect(res.error?.code).toBe(-32603);
|
|
130
|
+
expect(res.error?.message).toBe('Queue overflow');
|
|
131
|
+
});
|
|
132
|
+
it('returns -32601 for unknown methods', async () => {
|
|
133
|
+
const handler = createA2aRequestHandler({
|
|
134
|
+
onSendMessage: vi.fn(async () => makeTask()),
|
|
135
|
+
onGetTask: vi.fn(async () => null),
|
|
136
|
+
onCancelTask: vi.fn(async () => null),
|
|
137
|
+
verifyAuth: () => true,
|
|
138
|
+
});
|
|
139
|
+
const res = await handler(makeRequest('tasks/unknown'));
|
|
140
|
+
expect(res.error?.code).toBe(-32601);
|
|
141
|
+
expect(res.error?.message).toContain('Method not found');
|
|
142
|
+
expect(res.error?.message).toContain('tasks/unknown');
|
|
143
|
+
});
|
|
144
|
+
it('always returns JSON-RPC 2.0 version and echoes request id', async () => {
|
|
145
|
+
const handler = createA2aRequestHandler({
|
|
146
|
+
onSendMessage: vi.fn(async () => makeTask()),
|
|
147
|
+
onGetTask: vi.fn(async () => makeTask()),
|
|
148
|
+
onCancelTask: vi.fn(async () => null),
|
|
149
|
+
verifyAuth: () => true,
|
|
150
|
+
});
|
|
151
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'task-1' }, 'my-req-42'));
|
|
152
|
+
expect(res.jsonrpc).toBe('2.0');
|
|
153
|
+
expect(res.id).toBe('my-req-42');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// 2. AgentCard generation end-to-end
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
describe('A2A Server — AgentCard generation', () => {
|
|
160
|
+
it('builds a complete agent card with all fields', () => {
|
|
161
|
+
const config = {
|
|
162
|
+
name: 'FleetCoordinator',
|
|
163
|
+
description: 'Coordinates work across multiple agents',
|
|
164
|
+
url: 'https://fleet.example.com/a2a',
|
|
165
|
+
version: '2.1.0',
|
|
166
|
+
streaming: true,
|
|
167
|
+
authSchemes: [
|
|
168
|
+
{ type: 'http', scheme: 'bearer' },
|
|
169
|
+
{ type: 'apiKey', in: 'header', name: 'x-api-key' },
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
const card = buildAgentCard(config);
|
|
173
|
+
expect(card.name).toBe('FleetCoordinator');
|
|
174
|
+
expect(card.description).toBe('Coordinates work across multiple agents');
|
|
175
|
+
expect(card.url).toBe('https://fleet.example.com/a2a');
|
|
176
|
+
expect(card.version).toBe('2.1.0');
|
|
177
|
+
expect(card.capabilities.streaming).toBe(true);
|
|
178
|
+
expect(card.capabilities.pushNotifications).toBe(false);
|
|
179
|
+
expect(card.capabilities.stateTransitionHistory).toBe(false);
|
|
180
|
+
expect(card.authentication).toEqual([
|
|
181
|
+
{ type: 'http', scheme: 'bearer' },
|
|
182
|
+
{ type: 'apiKey', in: 'header', name: 'x-api-key' },
|
|
183
|
+
]);
|
|
184
|
+
expect(card.defaultInputContentTypes).toEqual(['text/plain']);
|
|
185
|
+
expect(card.defaultOutputContentTypes).toEqual(['text/plain']);
|
|
186
|
+
});
|
|
187
|
+
it('auto-generates skills from work types when none provided', () => {
|
|
188
|
+
const card = buildAgentCard({
|
|
189
|
+
name: 'Worker',
|
|
190
|
+
description: 'Generic worker',
|
|
191
|
+
url: 'https://worker.example.com/a2a',
|
|
192
|
+
});
|
|
193
|
+
expect(card.skills.length).toBeGreaterThan(0);
|
|
194
|
+
const skillIds = card.skills.map(s => s.id);
|
|
195
|
+
expect(skillIds).toContain('code-development');
|
|
196
|
+
expect(skillIds).toContain('quality-assurance');
|
|
197
|
+
expect(skillIds).toContain('research-analysis');
|
|
198
|
+
expect(skillIds).toContain('backlog-creation');
|
|
199
|
+
expect(skillIds).toContain('inflight-work');
|
|
200
|
+
expect(skillIds).toContain('acceptance-review');
|
|
201
|
+
expect(skillIds).toContain('refinement');
|
|
202
|
+
expect(skillIds).toContain('coordination');
|
|
203
|
+
expect(skillIds).toContain('qa-coordination');
|
|
204
|
+
expect(skillIds).toContain('acceptance-coordination');
|
|
205
|
+
// Each skill should have an id, name, and description
|
|
206
|
+
for (const skill of card.skills) {
|
|
207
|
+
expect(skill.id).toBeTruthy();
|
|
208
|
+
expect(skill.name).toBeTruthy();
|
|
209
|
+
expect(skill.description).toBeTruthy();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
it('uses explicit skills when provided', () => {
|
|
213
|
+
const card = buildAgentCard({
|
|
214
|
+
name: 'Specialist',
|
|
215
|
+
description: 'A specialist agent',
|
|
216
|
+
url: 'https://specialist.example.com/a2a',
|
|
217
|
+
skills: [
|
|
218
|
+
{
|
|
219
|
+
id: 'data-analysis',
|
|
220
|
+
name: 'Data Analysis',
|
|
221
|
+
description: 'Analyze data sets and produce reports',
|
|
222
|
+
tags: ['analysis'],
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
expect(card.skills).toHaveLength(1);
|
|
227
|
+
expect(card.skills[0].id).toBe('data-analysis');
|
|
228
|
+
expect(card.skills[0].name).toBe('Data Analysis');
|
|
229
|
+
expect(card.skills[0].tags).toEqual(['analysis']);
|
|
230
|
+
});
|
|
231
|
+
it('defaults version to 1.0.0 and streaming to false', () => {
|
|
232
|
+
const card = buildAgentCard({
|
|
233
|
+
name: 'Default',
|
|
234
|
+
description: 'Defaults test',
|
|
235
|
+
url: 'https://default.example.com/a2a',
|
|
236
|
+
});
|
|
237
|
+
expect(card.version).toBe('1.0.0');
|
|
238
|
+
expect(card.capabilities.streaming).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
it('omits authentication when no authSchemes provided', () => {
|
|
241
|
+
const card = buildAgentCard({
|
|
242
|
+
name: 'NoAuth',
|
|
243
|
+
description: 'No auth agent',
|
|
244
|
+
url: 'https://noauth.example.com/a2a',
|
|
245
|
+
});
|
|
246
|
+
expect(card.authentication).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// 3. SSE event formatting round-trip
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
describe('A2A Server — SSE formatting round-trip', () => {
|
|
253
|
+
it('formats and parses TaskStatusUpdate correctly', () => {
|
|
254
|
+
const event = {
|
|
255
|
+
type: 'TaskStatusUpdate',
|
|
256
|
+
taskId: 'task-rt-1',
|
|
257
|
+
status: 'working',
|
|
258
|
+
message: makeMessage('Processing your request...', 'agent'),
|
|
259
|
+
final: false,
|
|
260
|
+
};
|
|
261
|
+
const formatted = formatSseEvent(event);
|
|
262
|
+
// Parse the SSE output
|
|
263
|
+
const lines = formatted.split('\n');
|
|
264
|
+
expect(lines[0]).toBe('event: TaskStatusUpdate');
|
|
265
|
+
const dataLine = lines[1];
|
|
266
|
+
expect(dataLine.startsWith('data: ')).toBe(true);
|
|
267
|
+
const parsed = JSON.parse(dataLine.slice(6));
|
|
268
|
+
expect(parsed.type).toBe('TaskStatusUpdate');
|
|
269
|
+
expect(parsed.taskId).toBe('task-rt-1');
|
|
270
|
+
expect(parsed.status).toBe('working');
|
|
271
|
+
expect(parsed.message.role).toBe('agent');
|
|
272
|
+
expect(parsed.message.parts[0].text).toBe('Processing your request...');
|
|
273
|
+
expect(parsed.final).toBe(false);
|
|
274
|
+
// Ends with double newline
|
|
275
|
+
expect(formatted.endsWith('\n\n')).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
it('formats and parses TaskArtifactUpdate correctly', () => {
|
|
278
|
+
const event = {
|
|
279
|
+
type: 'TaskArtifactUpdate',
|
|
280
|
+
taskId: 'task-rt-2',
|
|
281
|
+
artifact: {
|
|
282
|
+
name: 'output.json',
|
|
283
|
+
parts: [{ type: 'data', data: { result: 42, status: 'ok' } }],
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const formatted = formatSseEvent(event);
|
|
287
|
+
const lines = formatted.split('\n');
|
|
288
|
+
expect(lines[0]).toBe('event: TaskArtifactUpdate');
|
|
289
|
+
const parsed = JSON.parse(lines[1].slice(6));
|
|
290
|
+
expect(parsed.type).toBe('TaskArtifactUpdate');
|
|
291
|
+
expect(parsed.taskId).toBe('task-rt-2');
|
|
292
|
+
expect(parsed.artifact.name).toBe('output.json');
|
|
293
|
+
expect(parsed.artifact.parts[0].data).toEqual({ result: 42, status: 'ok' });
|
|
294
|
+
});
|
|
295
|
+
it('produces valid SSE format with event and data lines separated by double newline', () => {
|
|
296
|
+
const event = {
|
|
297
|
+
type: 'TaskStatusUpdate',
|
|
298
|
+
taskId: 'task-fmt',
|
|
299
|
+
status: 'completed',
|
|
300
|
+
final: true,
|
|
301
|
+
};
|
|
302
|
+
const formatted = formatSseEvent(event);
|
|
303
|
+
// Should match the exact SSE spec: event line, data line, empty line
|
|
304
|
+
const pattern = /^event: \w+\ndata: .+\n\n$/;
|
|
305
|
+
expect(formatted).toMatch(pattern);
|
|
306
|
+
});
|
|
307
|
+
it('round-trips a completed status event with final: true', () => {
|
|
308
|
+
const original = {
|
|
309
|
+
type: 'TaskStatusUpdate',
|
|
310
|
+
taskId: 'task-complete',
|
|
311
|
+
status: 'completed',
|
|
312
|
+
message: makeMessage('All done!', 'agent'),
|
|
313
|
+
final: true,
|
|
314
|
+
};
|
|
315
|
+
const formatted = formatSseEvent(original);
|
|
316
|
+
const dataLine = formatted.split('\n')[1];
|
|
317
|
+
const roundTripped = JSON.parse(dataLine.slice(6));
|
|
318
|
+
expect(roundTripped.type).toBe(original.type);
|
|
319
|
+
expect(roundTripped.taskId).toBe(original.taskId);
|
|
320
|
+
expect(roundTripped.status).toBe(original.status);
|
|
321
|
+
expect(roundTripped.final).toBe(original.final);
|
|
322
|
+
expect(roundTripped.message).toEqual(original.message);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// 4. Auth integration
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
describe('A2A Server — auth integration', () => {
|
|
329
|
+
function createAuthHandler(validTokens) {
|
|
330
|
+
const verifyAuth = (authHeader) => {
|
|
331
|
+
if (!authHeader)
|
|
332
|
+
return false;
|
|
333
|
+
const token = authHeader.startsWith('Bearer ')
|
|
334
|
+
? authHeader.slice(7)
|
|
335
|
+
: authHeader;
|
|
336
|
+
return validTokens.includes(token);
|
|
337
|
+
};
|
|
338
|
+
return createA2aRequestHandler({
|
|
339
|
+
onSendMessage: vi.fn(async () => makeTask()),
|
|
340
|
+
onGetTask: vi.fn(async () => makeTask()),
|
|
341
|
+
onCancelTask: vi.fn(async () => makeTask({ status: 'canceled' })),
|
|
342
|
+
verifyAuth,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
it('allows requests with a valid Bearer token', async () => {
|
|
346
|
+
const handler = createAuthHandler(['valid-token-123']);
|
|
347
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'task-1' }), 'Bearer valid-token-123');
|
|
348
|
+
expect(res.error).toBeUndefined();
|
|
349
|
+
expect(res.result).toBeDefined();
|
|
350
|
+
});
|
|
351
|
+
it('allows requests with a valid raw API key', async () => {
|
|
352
|
+
const handler = createAuthHandler(['raw-api-key-456']);
|
|
353
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'task-1' }), 'raw-api-key-456');
|
|
354
|
+
expect(res.error).toBeUndefined();
|
|
355
|
+
expect(res.result).toBeDefined();
|
|
356
|
+
});
|
|
357
|
+
it('rejects requests with an invalid token', async () => {
|
|
358
|
+
const handler = createAuthHandler(['valid-token-123']);
|
|
359
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'task-1' }), 'Bearer wrong-token');
|
|
360
|
+
expect(res.error?.code).toBe(-32000);
|
|
361
|
+
expect(res.error?.message).toBe('Unauthorized');
|
|
362
|
+
expect(res.result).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
it('rejects requests with no auth header', async () => {
|
|
365
|
+
const handler = createAuthHandler(['valid-token-123']);
|
|
366
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'task-1' }));
|
|
367
|
+
expect(res.error?.code).toBe(-32000);
|
|
368
|
+
expect(res.error?.message).toBe('Unauthorized');
|
|
369
|
+
});
|
|
370
|
+
it('rejects requests with empty auth header', async () => {
|
|
371
|
+
const handler = createAuthHandler(['valid-token-123']);
|
|
372
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'task-1' }), '');
|
|
373
|
+
expect(res.error?.code).toBe(-32000);
|
|
374
|
+
expect(res.error?.message).toBe('Unauthorized');
|
|
375
|
+
});
|
|
376
|
+
it('allows different methods when auth passes', async () => {
|
|
377
|
+
const handler = createAuthHandler(['super-secret']);
|
|
378
|
+
// message/send
|
|
379
|
+
const sendRes = await handler(makeRequest('message/send', { message: makeMessage('hi') }), 'Bearer super-secret');
|
|
380
|
+
expect(sendRes.error).toBeUndefined();
|
|
381
|
+
// tasks/get
|
|
382
|
+
const getRes = await handler(makeRequest('tasks/get', { taskId: 'task-1' }), 'Bearer super-secret');
|
|
383
|
+
expect(getRes.error).toBeUndefined();
|
|
384
|
+
// tasks/cancel
|
|
385
|
+
const cancelRes = await handler(makeRequest('tasks/cancel', { taskId: 'task-1' }), 'Bearer super-secret');
|
|
386
|
+
expect(cancelRes.error).toBeUndefined();
|
|
387
|
+
});
|
|
388
|
+
it('blocks all methods when auth fails', async () => {
|
|
389
|
+
const handler = createAuthHandler(['good-key']);
|
|
390
|
+
const sendRes = await handler(makeRequest('message/send', { message: makeMessage('hi') }), 'Bearer bad-key');
|
|
391
|
+
expect(sendRes.error?.code).toBe(-32000);
|
|
392
|
+
const getRes = await handler(makeRequest('tasks/get', { taskId: 'task-1' }), 'Bearer bad-key');
|
|
393
|
+
expect(getRes.error?.code).toBe(-32000);
|
|
394
|
+
const cancelRes = await handler(makeRequest('tasks/cancel', { taskId: 'task-1' }), 'Bearer bad-key');
|
|
395
|
+
expect(cancelRes.error?.code).toBe(-32000);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Server Handlers
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic utilities for exposing AgentFactory fleet capabilities
|
|
5
|
+
* via the Agent-to-Agent (A2A) protocol. Provides AgentCard generation,
|
|
6
|
+
* JSON-RPC request routing, and SSE event formatting.
|
|
7
|
+
*
|
|
8
|
+
* Consuming applications wire these handlers into their own HTTP server
|
|
9
|
+
* (Express, Hono, Next.js, etc.) — this module has no framework dependency.
|
|
10
|
+
*/
|
|
11
|
+
import { extractBearerToken, verifyApiKey } from './worker-auth.js';
|
|
12
|
+
/**
|
|
13
|
+
* Mapping from AgentWorkType values to auto-generated skill descriptors.
|
|
14
|
+
* Used when the caller does not supply an explicit skill list.
|
|
15
|
+
*/
|
|
16
|
+
const WORK_TYPE_SKILLS = {
|
|
17
|
+
development: {
|
|
18
|
+
id: 'code-development',
|
|
19
|
+
name: 'Code Development',
|
|
20
|
+
description: 'Implement features, fix bugs, and write code changes',
|
|
21
|
+
tags: ['coding', 'development'],
|
|
22
|
+
},
|
|
23
|
+
qa: {
|
|
24
|
+
id: 'quality-assurance',
|
|
25
|
+
name: 'Quality Assurance',
|
|
26
|
+
description: 'Run tests, review code quality, and verify changes',
|
|
27
|
+
tags: ['testing', 'qa'],
|
|
28
|
+
},
|
|
29
|
+
research: {
|
|
30
|
+
id: 'research-analysis',
|
|
31
|
+
name: 'Research & Analysis',
|
|
32
|
+
description: 'Investigate topics, gather information, and produce research reports',
|
|
33
|
+
tags: ['research', 'analysis'],
|
|
34
|
+
},
|
|
35
|
+
'backlog-creation': {
|
|
36
|
+
id: 'backlog-creation',
|
|
37
|
+
name: 'Backlog Creation',
|
|
38
|
+
description: 'Create and prioritize backlog items from requirements',
|
|
39
|
+
tags: ['planning', 'backlog'],
|
|
40
|
+
},
|
|
41
|
+
inflight: {
|
|
42
|
+
id: 'inflight-work',
|
|
43
|
+
name: 'In-Flight Work',
|
|
44
|
+
description: 'Continue work on in-progress tasks',
|
|
45
|
+
tags: ['workflow'],
|
|
46
|
+
},
|
|
47
|
+
acceptance: {
|
|
48
|
+
id: 'acceptance-review',
|
|
49
|
+
name: 'Acceptance Review',
|
|
50
|
+
description: 'Review completed work against acceptance criteria',
|
|
51
|
+
tags: ['review', 'acceptance'],
|
|
52
|
+
},
|
|
53
|
+
refinement: {
|
|
54
|
+
id: 'refinement',
|
|
55
|
+
name: 'Refinement',
|
|
56
|
+
description: 'Refine and improve existing work based on feedback',
|
|
57
|
+
tags: ['refinement', 'iteration'],
|
|
58
|
+
},
|
|
59
|
+
coordination: {
|
|
60
|
+
id: 'coordination',
|
|
61
|
+
name: 'Coordination',
|
|
62
|
+
description: 'Coordinate work across multiple agents and tasks',
|
|
63
|
+
tags: ['coordination', 'orchestration'],
|
|
64
|
+
},
|
|
65
|
+
'qa-coordination': {
|
|
66
|
+
id: 'qa-coordination',
|
|
67
|
+
name: 'QA Coordination',
|
|
68
|
+
description: 'Coordinate quality assurance across multiple agents',
|
|
69
|
+
tags: ['qa', 'coordination'],
|
|
70
|
+
},
|
|
71
|
+
'acceptance-coordination': {
|
|
72
|
+
id: 'acceptance-coordination',
|
|
73
|
+
name: 'Acceptance Coordination',
|
|
74
|
+
description: 'Coordinate acceptance reviews across multiple agents',
|
|
75
|
+
tags: ['acceptance', 'coordination'],
|
|
76
|
+
},
|
|
77
|
+
'refinement-coordination': {
|
|
78
|
+
id: 'refinement-coordination',
|
|
79
|
+
name: 'Refinement Coordination',
|
|
80
|
+
description: 'Coordinate refinement across sub-issues after QA/acceptance failure',
|
|
81
|
+
tags: ['refinement', 'coordination'],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Build an A2A AgentCard from the supplied configuration.
|
|
86
|
+
*
|
|
87
|
+
* If no explicit skills are provided the card is populated with skills
|
|
88
|
+
* derived from every known {@link AgentWorkType}.
|
|
89
|
+
*
|
|
90
|
+
* @param config - Server configuration
|
|
91
|
+
* @returns A fully-formed AgentCard
|
|
92
|
+
*/
|
|
93
|
+
export function buildAgentCard(config) {
|
|
94
|
+
const skills = config.skills ?? Object.values(WORK_TYPE_SKILLS);
|
|
95
|
+
return {
|
|
96
|
+
name: config.name,
|
|
97
|
+
description: config.description,
|
|
98
|
+
url: config.url,
|
|
99
|
+
version: config.version ?? '1.0.0',
|
|
100
|
+
capabilities: {
|
|
101
|
+
streaming: config.streaming ?? false,
|
|
102
|
+
pushNotifications: false,
|
|
103
|
+
stateTransitionHistory: false,
|
|
104
|
+
},
|
|
105
|
+
skills,
|
|
106
|
+
...(config.authSchemes && { authentication: config.authSchemes }),
|
|
107
|
+
defaultInputContentTypes: ['text/plain'],
|
|
108
|
+
defaultOutputContentTypes: ['text/plain'],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// ---- JSON-RPC error helpers ------------------------------------------------
|
|
112
|
+
const JSON_RPC_VERSION = '2.0';
|
|
113
|
+
function rpcError(id, code, message, data) {
|
|
114
|
+
return {
|
|
115
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
116
|
+
id,
|
|
117
|
+
error: { code, message, ...(data !== undefined && { data }) },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function rpcResult(id, result) {
|
|
121
|
+
return {
|
|
122
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
123
|
+
id,
|
|
124
|
+
result,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// ---- Default auth verifier --------------------------------------------------
|
|
128
|
+
function defaultVerifyAuth(authHeader) {
|
|
129
|
+
const token = extractBearerToken(authHeader ?? null);
|
|
130
|
+
if (!token)
|
|
131
|
+
return false;
|
|
132
|
+
return verifyApiKey(token);
|
|
133
|
+
}
|
|
134
|
+
// ---- Factory ----------------------------------------------------------------
|
|
135
|
+
/**
|
|
136
|
+
* Create a framework-agnostic A2A request handler.
|
|
137
|
+
*
|
|
138
|
+
* The returned function processes a single JSON-RPC request and returns
|
|
139
|
+
* the corresponding response. The consuming application is responsible
|
|
140
|
+
* for HTTP parsing, serialisation, and transport.
|
|
141
|
+
*
|
|
142
|
+
* Supported methods:
|
|
143
|
+
* - `message/send` — send a message (optionally to an existing task)
|
|
144
|
+
* - `tasks/get` — retrieve a task by ID
|
|
145
|
+
* - `tasks/cancel` — cancel a task by ID
|
|
146
|
+
*
|
|
147
|
+
* @param options - Callbacks for task lifecycle and (optional) auth
|
|
148
|
+
* @returns An async handler function
|
|
149
|
+
*/
|
|
150
|
+
export function createA2aRequestHandler(options) {
|
|
151
|
+
const { onSendMessage, onGetTask, onCancelTask } = options;
|
|
152
|
+
const verifyAuth = options.verifyAuth ?? defaultVerifyAuth;
|
|
153
|
+
return async (request, authHeader) => {
|
|
154
|
+
const id = request.id ?? null;
|
|
155
|
+
// --- Auth check ---
|
|
156
|
+
if (!verifyAuth(authHeader)) {
|
|
157
|
+
return rpcError(id, -32000, 'Unauthorized');
|
|
158
|
+
}
|
|
159
|
+
// --- Method routing ---
|
|
160
|
+
switch (request.method) {
|
|
161
|
+
case 'message/send': {
|
|
162
|
+
const params = request.params;
|
|
163
|
+
if (!params || !params.message) {
|
|
164
|
+
return rpcError(id, -32602, 'Invalid params: message is required');
|
|
165
|
+
}
|
|
166
|
+
const message = params.message;
|
|
167
|
+
const taskId = params.taskId;
|
|
168
|
+
try {
|
|
169
|
+
const task = await onSendMessage(message, taskId);
|
|
170
|
+
return rpcResult(id, task);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
const msg = err instanceof Error ? err.message : 'Internal error';
|
|
174
|
+
return rpcError(id, -32603, msg);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
case 'tasks/get': {
|
|
178
|
+
const params = request.params;
|
|
179
|
+
if (!params || typeof params.taskId !== 'string') {
|
|
180
|
+
return rpcError(id, -32602, 'Invalid params: taskId is required');
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const task = await onGetTask(params.taskId);
|
|
184
|
+
if (!task) {
|
|
185
|
+
return rpcError(id, -32001, 'Task not found');
|
|
186
|
+
}
|
|
187
|
+
return rpcResult(id, task);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const msg = err instanceof Error ? err.message : 'Internal error';
|
|
191
|
+
return rpcError(id, -32603, msg);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
case 'tasks/cancel': {
|
|
195
|
+
const params = request.params;
|
|
196
|
+
if (!params || typeof params.taskId !== 'string') {
|
|
197
|
+
return rpcError(id, -32602, 'Invalid params: taskId is required');
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const task = await onCancelTask(params.taskId);
|
|
201
|
+
if (!task) {
|
|
202
|
+
return rpcError(id, -32001, 'Task not found');
|
|
203
|
+
}
|
|
204
|
+
return rpcResult(id, task);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : 'Internal error';
|
|
208
|
+
return rpcError(id, -32603, msg);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
default:
|
|
212
|
+
return rpcError(id, -32601, `Method not found: ${request.method}`);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// SSE event formatter
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
/**
|
|
220
|
+
* Format an A2A task event as a Server-Sent Events (SSE) message.
|
|
221
|
+
*
|
|
222
|
+
* The output follows the SSE text/event-stream format:
|
|
223
|
+
* ```
|
|
224
|
+
* event: <type>
|
|
225
|
+
* data: <JSON payload>
|
|
226
|
+
*
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @param event - The task event to format
|
|
230
|
+
* @returns A string ready to write to an SSE response stream
|
|
231
|
+
*/
|
|
232
|
+
export function formatSseEvent(event) {
|
|
233
|
+
const data = JSON.stringify(event);
|
|
234
|
+
return `event: ${event.type}\ndata: ${data}\n\n`;
|
|
235
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a-server.test.d.ts","sourceRoot":"","sources":["../../src/a2a-server.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { buildAgentCard, createA2aRequestHandler, formatSseEvent } from './a2a-server.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function makeRequest(method, params, id = 1) {
|
|
7
|
+
return { jsonrpc: '2.0', id, method, params };
|
|
8
|
+
}
|
|
9
|
+
function makeMessage(text, role = 'user') {
|
|
10
|
+
return { role, parts: [{ type: 'text', text }] };
|
|
11
|
+
}
|
|
12
|
+
function makeTask(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
id: 'task-1',
|
|
15
|
+
status: 'submitted',
|
|
16
|
+
messages: [],
|
|
17
|
+
artifacts: [],
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Build a handler with permissive auth and controllable callbacks */
|
|
22
|
+
function createTestHandler(opts = {}) {
|
|
23
|
+
const onSendMessage = opts.onSendMessage ?? vi.fn(async () => makeTask());
|
|
24
|
+
const onGetTask = opts.onGetTask ?? vi.fn(async () => makeTask());
|
|
25
|
+
const onCancelTask = opts.onCancelTask ?? vi.fn(async () => makeTask());
|
|
26
|
+
const verifyAuth = opts.verifyAuth ?? (() => true);
|
|
27
|
+
const handler = createA2aRequestHandler({
|
|
28
|
+
onSendMessage,
|
|
29
|
+
onGetTask,
|
|
30
|
+
onCancelTask,
|
|
31
|
+
verifyAuth,
|
|
32
|
+
});
|
|
33
|
+
return { handler, onSendMessage, onGetTask, onCancelTask };
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// buildAgentCard
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
describe('buildAgentCard', () => {
|
|
39
|
+
const baseConfig = {
|
|
40
|
+
name: 'TestAgent',
|
|
41
|
+
description: 'A test agent',
|
|
42
|
+
url: 'https://example.com/a2a',
|
|
43
|
+
};
|
|
44
|
+
it('generates a valid card from minimal config', () => {
|
|
45
|
+
const card = buildAgentCard(baseConfig);
|
|
46
|
+
expect(card.name).toBe('TestAgent');
|
|
47
|
+
expect(card.description).toBe('A test agent');
|
|
48
|
+
expect(card.url).toBe('https://example.com/a2a');
|
|
49
|
+
expect(card.version).toBe('1.0.0');
|
|
50
|
+
expect(card.capabilities.streaming).toBe(false);
|
|
51
|
+
expect(card.capabilities.pushNotifications).toBe(false);
|
|
52
|
+
expect(card.capabilities.stateTransitionHistory).toBe(false);
|
|
53
|
+
expect(card.defaultInputContentTypes).toEqual(['text/plain']);
|
|
54
|
+
expect(card.defaultOutputContentTypes).toEqual(['text/plain']);
|
|
55
|
+
});
|
|
56
|
+
it('uses the provided version', () => {
|
|
57
|
+
const card = buildAgentCard({ ...baseConfig, version: '2.3.1' });
|
|
58
|
+
expect(card.version).toBe('2.3.1');
|
|
59
|
+
});
|
|
60
|
+
it('sets streaming capability when configured', () => {
|
|
61
|
+
const card = buildAgentCard({ ...baseConfig, streaming: true });
|
|
62
|
+
expect(card.capabilities.streaming).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('includes authentication schemes when provided', () => {
|
|
65
|
+
const card = buildAgentCard({
|
|
66
|
+
...baseConfig,
|
|
67
|
+
authSchemes: [{ type: 'http', scheme: 'bearer' }],
|
|
68
|
+
});
|
|
69
|
+
expect(card.authentication).toEqual([{ type: 'http', scheme: 'bearer' }]);
|
|
70
|
+
});
|
|
71
|
+
it('does not include authentication when not provided', () => {
|
|
72
|
+
const card = buildAgentCard(baseConfig);
|
|
73
|
+
expect(card.authentication).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
it('auto-generates skills from AgentWorkType values when none provided', () => {
|
|
76
|
+
const card = buildAgentCard(baseConfig);
|
|
77
|
+
expect(card.skills.length).toBeGreaterThan(0);
|
|
78
|
+
const ids = card.skills.map((s) => s.id);
|
|
79
|
+
expect(ids).toContain('code-development');
|
|
80
|
+
expect(ids).toContain('quality-assurance');
|
|
81
|
+
expect(ids).toContain('research-analysis');
|
|
82
|
+
expect(ids).toContain('backlog-creation');
|
|
83
|
+
expect(ids).toContain('coordination');
|
|
84
|
+
});
|
|
85
|
+
it('uses explicit skills when provided, ignoring auto-generation', () => {
|
|
86
|
+
const customSkills = [
|
|
87
|
+
{ id: 'custom-skill', name: 'Custom', description: 'A custom skill' },
|
|
88
|
+
];
|
|
89
|
+
const card = buildAgentCard({ ...baseConfig, skills: customSkills });
|
|
90
|
+
expect(card.skills).toEqual(customSkills);
|
|
91
|
+
expect(card.skills).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// createA2aRequestHandler — method routing
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
describe('createA2aRequestHandler', () => {
|
|
98
|
+
describe('message/send', () => {
|
|
99
|
+
it('routes message/send and returns the task', async () => {
|
|
100
|
+
const task = makeTask({ id: 'task-42', status: 'working' });
|
|
101
|
+
const onSendMessage = vi.fn(async () => task);
|
|
102
|
+
const { handler } = createTestHandler({ onSendMessage });
|
|
103
|
+
const message = makeMessage('Hello agent');
|
|
104
|
+
const res = await handler(makeRequest('message/send', { message }));
|
|
105
|
+
expect(res.error).toBeUndefined();
|
|
106
|
+
expect(res.result).toEqual(task);
|
|
107
|
+
expect(onSendMessage).toHaveBeenCalledWith(message, undefined);
|
|
108
|
+
});
|
|
109
|
+
it('passes taskId to onSendMessage when provided', async () => {
|
|
110
|
+
const onSendMessage = vi.fn(async () => makeTask());
|
|
111
|
+
const { handler } = createTestHandler({ onSendMessage });
|
|
112
|
+
const message = makeMessage('Continue working');
|
|
113
|
+
await handler(makeRequest('message/send', { message, taskId: 'existing-task' }));
|
|
114
|
+
expect(onSendMessage).toHaveBeenCalledWith(message, 'existing-task');
|
|
115
|
+
});
|
|
116
|
+
it('returns -32602 when message param is missing', async () => {
|
|
117
|
+
const { handler } = createTestHandler();
|
|
118
|
+
const res = await handler(makeRequest('message/send', {}));
|
|
119
|
+
expect(res.error?.code).toBe(-32602);
|
|
120
|
+
expect(res.error?.message).toContain('message is required');
|
|
121
|
+
});
|
|
122
|
+
it('returns -32602 when params are missing entirely', async () => {
|
|
123
|
+
const { handler } = createTestHandler();
|
|
124
|
+
const res = await handler(makeRequest('message/send'));
|
|
125
|
+
expect(res.error?.code).toBe(-32602);
|
|
126
|
+
});
|
|
127
|
+
it('returns -32603 when onSendMessage throws', async () => {
|
|
128
|
+
const onSendMessage = vi.fn(async () => {
|
|
129
|
+
throw new Error('Queue full');
|
|
130
|
+
});
|
|
131
|
+
const { handler } = createTestHandler({ onSendMessage });
|
|
132
|
+
const res = await handler(makeRequest('message/send', { message: makeMessage('hi') }));
|
|
133
|
+
expect(res.error?.code).toBe(-32603);
|
|
134
|
+
expect(res.error?.message).toBe('Queue full');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('tasks/get', () => {
|
|
138
|
+
it('returns the task when found', async () => {
|
|
139
|
+
const task = makeTask({ id: 'task-99', status: 'completed' });
|
|
140
|
+
const onGetTask = vi.fn(async () => task);
|
|
141
|
+
const { handler } = createTestHandler({ onGetTask });
|
|
142
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'task-99' }));
|
|
143
|
+
expect(res.error).toBeUndefined();
|
|
144
|
+
expect(res.result).toEqual(task);
|
|
145
|
+
expect(onGetTask).toHaveBeenCalledWith('task-99');
|
|
146
|
+
});
|
|
147
|
+
it('returns -32001 when task is not found', async () => {
|
|
148
|
+
const onGetTask = vi.fn(async () => null);
|
|
149
|
+
const { handler } = createTestHandler({ onGetTask });
|
|
150
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'missing' }));
|
|
151
|
+
expect(res.error?.code).toBe(-32001);
|
|
152
|
+
expect(res.error?.message).toBe('Task not found');
|
|
153
|
+
});
|
|
154
|
+
it('returns -32602 when taskId is missing', async () => {
|
|
155
|
+
const { handler } = createTestHandler();
|
|
156
|
+
const res = await handler(makeRequest('tasks/get', {}));
|
|
157
|
+
expect(res.error?.code).toBe(-32602);
|
|
158
|
+
expect(res.error?.message).toContain('taskId is required');
|
|
159
|
+
});
|
|
160
|
+
it('returns -32602 when taskId is not a string', async () => {
|
|
161
|
+
const { handler } = createTestHandler();
|
|
162
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 123 }));
|
|
163
|
+
expect(res.error?.code).toBe(-32602);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('tasks/cancel', () => {
|
|
167
|
+
it('returns the canceled task', async () => {
|
|
168
|
+
const task = makeTask({ id: 'task-5', status: 'canceled' });
|
|
169
|
+
const onCancelTask = vi.fn(async () => task);
|
|
170
|
+
const { handler } = createTestHandler({ onCancelTask });
|
|
171
|
+
const res = await handler(makeRequest('tasks/cancel', { taskId: 'task-5' }));
|
|
172
|
+
expect(res.error).toBeUndefined();
|
|
173
|
+
expect(res.result).toEqual(task);
|
|
174
|
+
expect(onCancelTask).toHaveBeenCalledWith('task-5');
|
|
175
|
+
});
|
|
176
|
+
it('returns -32001 when task is not found', async () => {
|
|
177
|
+
const onCancelTask = vi.fn(async () => null);
|
|
178
|
+
const { handler } = createTestHandler({ onCancelTask });
|
|
179
|
+
const res = await handler(makeRequest('tasks/cancel', { taskId: 'no-such' }));
|
|
180
|
+
expect(res.error?.code).toBe(-32001);
|
|
181
|
+
expect(res.error?.message).toBe('Task not found');
|
|
182
|
+
});
|
|
183
|
+
it('returns -32602 when taskId is missing', async () => {
|
|
184
|
+
const { handler } = createTestHandler();
|
|
185
|
+
const res = await handler(makeRequest('tasks/cancel'));
|
|
186
|
+
expect(res.error?.code).toBe(-32602);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('unknown method', () => {
|
|
190
|
+
it('returns -32601 for an unrecognised method', async () => {
|
|
191
|
+
const { handler } = createTestHandler();
|
|
192
|
+
const res = await handler(makeRequest('foo/bar'));
|
|
193
|
+
expect(res.error?.code).toBe(-32601);
|
|
194
|
+
expect(res.error?.message).toContain('Method not found');
|
|
195
|
+
expect(res.error?.message).toContain('foo/bar');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe('authentication', () => {
|
|
199
|
+
it('returns -32000 when verifyAuth rejects', async () => {
|
|
200
|
+
const verifyAuth = () => false;
|
|
201
|
+
const { handler } = createTestHandler({ verifyAuth });
|
|
202
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'x' }));
|
|
203
|
+
expect(res.error?.code).toBe(-32000);
|
|
204
|
+
expect(res.error?.message).toBe('Unauthorized');
|
|
205
|
+
});
|
|
206
|
+
it('passes the auth header to verifyAuth', async () => {
|
|
207
|
+
const verifyAuth = vi.fn(() => true);
|
|
208
|
+
const { handler } = createTestHandler({ verifyAuth });
|
|
209
|
+
await handler(makeRequest('tasks/get', { taskId: 'x' }), 'Bearer tok-123');
|
|
210
|
+
expect(verifyAuth).toHaveBeenCalledWith('Bearer tok-123');
|
|
211
|
+
});
|
|
212
|
+
it('passes undefined when no auth header is provided', async () => {
|
|
213
|
+
const verifyAuth = vi.fn(() => true);
|
|
214
|
+
const { handler } = createTestHandler({ verifyAuth });
|
|
215
|
+
await handler(makeRequest('tasks/get', { taskId: 'x' }));
|
|
216
|
+
expect(verifyAuth).toHaveBeenCalledWith(undefined);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe('response format', () => {
|
|
220
|
+
it('always includes jsonrpc 2.0 version', async () => {
|
|
221
|
+
const { handler } = createTestHandler();
|
|
222
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'x' }));
|
|
223
|
+
expect(res.jsonrpc).toBe('2.0');
|
|
224
|
+
});
|
|
225
|
+
it('echoes back the request id', async () => {
|
|
226
|
+
const { handler } = createTestHandler();
|
|
227
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'x' }, 42));
|
|
228
|
+
expect(res.id).toBe(42);
|
|
229
|
+
});
|
|
230
|
+
it('echoes string ids correctly', async () => {
|
|
231
|
+
const { handler } = createTestHandler();
|
|
232
|
+
const res = await handler(makeRequest('tasks/get', { taskId: 'x' }, 'req-abc'));
|
|
233
|
+
expect(res.id).toBe('req-abc');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// formatSseEvent
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
describe('formatSseEvent', () => {
|
|
241
|
+
it('formats a TaskStatusUpdate event', () => {
|
|
242
|
+
const event = {
|
|
243
|
+
type: 'TaskStatusUpdate',
|
|
244
|
+
taskId: 'task-1',
|
|
245
|
+
status: 'working',
|
|
246
|
+
final: false,
|
|
247
|
+
};
|
|
248
|
+
const result = formatSseEvent(event);
|
|
249
|
+
expect(result).toBe('event: TaskStatusUpdate\n' +
|
|
250
|
+
`data: ${JSON.stringify(event)}\n\n`);
|
|
251
|
+
});
|
|
252
|
+
it('formats a TaskStatusUpdate with message', () => {
|
|
253
|
+
const event = {
|
|
254
|
+
type: 'TaskStatusUpdate',
|
|
255
|
+
taskId: 'task-2',
|
|
256
|
+
status: 'completed',
|
|
257
|
+
message: makeMessage('Done!', 'agent'),
|
|
258
|
+
final: true,
|
|
259
|
+
};
|
|
260
|
+
const result = formatSseEvent(event);
|
|
261
|
+
expect(result).toContain('event: TaskStatusUpdate\n');
|
|
262
|
+
expect(result).toContain('"final":true');
|
|
263
|
+
expect(result).toContain('"text":"Done!"');
|
|
264
|
+
expect(result.endsWith('\n\n')).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
it('formats a TaskArtifactUpdate event', () => {
|
|
267
|
+
const event = {
|
|
268
|
+
type: 'TaskArtifactUpdate',
|
|
269
|
+
taskId: 'task-3',
|
|
270
|
+
artifact: {
|
|
271
|
+
name: 'report.md',
|
|
272
|
+
parts: [{ type: 'text', text: '# Report' }],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const result = formatSseEvent(event);
|
|
276
|
+
expect(result).toBe('event: TaskArtifactUpdate\n' +
|
|
277
|
+
`data: ${JSON.stringify(event)}\n\n`);
|
|
278
|
+
});
|
|
279
|
+
it('uses the event type as the SSE event name', () => {
|
|
280
|
+
const event = {
|
|
281
|
+
type: 'TaskStatusUpdate',
|
|
282
|
+
taskId: 'task-1',
|
|
283
|
+
status: 'failed',
|
|
284
|
+
final: true,
|
|
285
|
+
};
|
|
286
|
+
const lines = formatSseEvent(event).split('\n');
|
|
287
|
+
expect(lines[0]).toBe('event: TaskStatusUpdate');
|
|
288
|
+
});
|
|
289
|
+
it('puts the full JSON payload on the data line', () => {
|
|
290
|
+
const event = {
|
|
291
|
+
type: 'TaskStatusUpdate',
|
|
292
|
+
taskId: 'task-1',
|
|
293
|
+
status: 'submitted',
|
|
294
|
+
final: false,
|
|
295
|
+
};
|
|
296
|
+
const lines = formatSseEvent(event).split('\n');
|
|
297
|
+
const dataLine = lines[1];
|
|
298
|
+
const parsed = JSON.parse(dataLine.replace('data: ', ''));
|
|
299
|
+
expect(parsed.taskId).toBe('task-1');
|
|
300
|
+
expect(parsed.status).toBe('submitted');
|
|
301
|
+
});
|
|
302
|
+
it('ends with double newline', () => {
|
|
303
|
+
const event = {
|
|
304
|
+
type: 'TaskStatusUpdate',
|
|
305
|
+
taskId: 't',
|
|
306
|
+
status: 'working',
|
|
307
|
+
final: false,
|
|
308
|
+
};
|
|
309
|
+
expect(formatSseEvent(event)).toMatch(/\n\n$/);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Protocol Types
|
|
3
|
+
*
|
|
4
|
+
* TypeScript type definitions for the Agent-to-Agent (A2A) protocol.
|
|
5
|
+
* Covers AgentCard discovery, JSON-RPC messaging, task lifecycle,
|
|
6
|
+
* and Server-Sent Events (SSE) streaming.
|
|
7
|
+
*/
|
|
8
|
+
/** Describes an agent's identity, capabilities, and discoverable skills */
|
|
9
|
+
export interface A2aAgentCard {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
url: string;
|
|
13
|
+
version: string;
|
|
14
|
+
capabilities: A2aCapabilities;
|
|
15
|
+
skills: A2aSkill[];
|
|
16
|
+
authentication?: A2aAuthScheme[];
|
|
17
|
+
defaultInputContentTypes?: string[];
|
|
18
|
+
defaultOutputContentTypes?: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Feature flags advertised by the agent */
|
|
21
|
+
export interface A2aCapabilities {
|
|
22
|
+
streaming: boolean;
|
|
23
|
+
pushNotifications: boolean;
|
|
24
|
+
stateTransitionHistory: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** A discrete capability the agent can perform */
|
|
27
|
+
export interface A2aSkill {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
examples?: string[];
|
|
33
|
+
}
|
|
34
|
+
/** Authentication scheme supported by the agent endpoint */
|
|
35
|
+
export interface A2aAuthScheme {
|
|
36
|
+
type: 'apiKey' | 'http' | 'oauth2';
|
|
37
|
+
/** For http type, e.g. 'bearer' */
|
|
38
|
+
scheme?: string;
|
|
39
|
+
/** For apiKey type, e.g. 'header' */
|
|
40
|
+
in?: string;
|
|
41
|
+
/** For apiKey type, the header name */
|
|
42
|
+
name?: string;
|
|
43
|
+
}
|
|
44
|
+
/** A JSON-RPC 2.0 request object */
|
|
45
|
+
export interface JsonRpcRequest {
|
|
46
|
+
jsonrpc: '2.0';
|
|
47
|
+
id: string | number;
|
|
48
|
+
method: string;
|
|
49
|
+
params?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/** A JSON-RPC 2.0 response object */
|
|
52
|
+
export interface JsonRpcResponse {
|
|
53
|
+
jsonrpc: '2.0';
|
|
54
|
+
id: string | number | null;
|
|
55
|
+
result?: unknown;
|
|
56
|
+
error?: JsonRpcError;
|
|
57
|
+
}
|
|
58
|
+
/** A JSON-RPC 2.0 error object */
|
|
59
|
+
export interface JsonRpcError {
|
|
60
|
+
code: number;
|
|
61
|
+
message: string;
|
|
62
|
+
data?: unknown;
|
|
63
|
+
}
|
|
64
|
+
/** Represents a unit of work managed by the agent */
|
|
65
|
+
export interface A2aTask {
|
|
66
|
+
id: string;
|
|
67
|
+
status: A2aTaskStatus;
|
|
68
|
+
messages: A2aMessage[];
|
|
69
|
+
artifacts: A2aArtifact[];
|
|
70
|
+
metadata?: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
/** Possible states of an A2A task */
|
|
73
|
+
export type A2aTaskStatus = 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
|
|
74
|
+
/** A message exchanged between user and agent */
|
|
75
|
+
export interface A2aMessage {
|
|
76
|
+
role: 'user' | 'agent';
|
|
77
|
+
parts: A2aPart[];
|
|
78
|
+
metadata?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
/** A content part within a message or artifact */
|
|
81
|
+
export type A2aPart = A2aTextPart | A2aFilePart | A2aDataPart;
|
|
82
|
+
/** Plain text content part */
|
|
83
|
+
export interface A2aTextPart {
|
|
84
|
+
type: 'text';
|
|
85
|
+
text: string;
|
|
86
|
+
}
|
|
87
|
+
/** File content part (URI or inline bytes) */
|
|
88
|
+
export interface A2aFilePart {
|
|
89
|
+
type: 'file';
|
|
90
|
+
file: {
|
|
91
|
+
name: string;
|
|
92
|
+
mimeType: string;
|
|
93
|
+
uri?: string;
|
|
94
|
+
bytes?: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Structured data content part */
|
|
98
|
+
export interface A2aDataPart {
|
|
99
|
+
type: 'data';
|
|
100
|
+
data: Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
/** An output artifact produced by the agent */
|
|
103
|
+
export interface A2aArtifact {
|
|
104
|
+
name: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
parts: A2aPart[];
|
|
107
|
+
metadata?: Record<string, unknown>;
|
|
108
|
+
}
|
|
109
|
+
/** Notifies the client of a task status change */
|
|
110
|
+
export interface A2aTaskStatusUpdateEvent {
|
|
111
|
+
type: 'TaskStatusUpdate';
|
|
112
|
+
taskId: string;
|
|
113
|
+
status: A2aTaskStatus;
|
|
114
|
+
message?: A2aMessage;
|
|
115
|
+
final: boolean;
|
|
116
|
+
}
|
|
117
|
+
/** Notifies the client of a new or updated artifact */
|
|
118
|
+
export interface A2aTaskArtifactUpdateEvent {
|
|
119
|
+
type: 'TaskArtifactUpdate';
|
|
120
|
+
taskId: string;
|
|
121
|
+
artifact: A2aArtifact;
|
|
122
|
+
}
|
|
123
|
+
/** Union of all SSE event types emitted during task streaming */
|
|
124
|
+
export type A2aTaskEvent = A2aTaskStatusUpdateEvent | A2aTaskArtifactUpdateEvent;
|
|
125
|
+
//# sourceMappingURL=a2a-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a-types.d.ts","sourceRoot":"","sources":["../../src/a2a-types.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -21,4 +21,6 @@ export * from './governor-dedup.js';
|
|
|
21
21
|
export * from './redis-rate-limiter.js';
|
|
22
22
|
export * from './redis-circuit-breaker.js';
|
|
23
23
|
export * from './quota-tracker.js';
|
|
24
|
+
export * from './a2a-types.js';
|
|
25
|
+
export * from './a2a-server.js';
|
|
24
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAG3B,cAAc,YAAY,CAAA;AAG1B,cAAc,YAAY,CAAA;AAG1B,cAAc,sBAAsB,CAAA;AAGpC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,0BAA0B,CAAA;AAGxC,cAAc,sBAAsB,CAAA;AAGpC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,kBAAkB,CAAA;AAGhC,cAAc,mBAAmB,CAAA;AAGjC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,oBAAoB,CAAA;AAGlC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,uBAAuB,CAAA;AAGrC,cAAc,+BAA+B,CAAA;AAG7C,cAAc,yBAAyB,CAAA;AAGvC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,yBAAyB,CAAA;AAGvC,cAAc,4BAA4B,CAAA;AAG1C,cAAc,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAG3B,cAAc,YAAY,CAAA;AAG1B,cAAc,YAAY,CAAA;AAG1B,cAAc,sBAAsB,CAAA;AAGpC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,0BAA0B,CAAA;AAGxC,cAAc,sBAAsB,CAAA;AAGpC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,kBAAkB,CAAA;AAGhC,cAAc,mBAAmB,CAAA;AAGjC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,oBAAoB,CAAA;AAGlC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,uBAAuB,CAAA;AAGrC,cAAc,+BAA+B,CAAA;AAG7C,cAAc,yBAAyB,CAAA;AAGvC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,yBAAyB,CAAA;AAGvC,cAAc,4BAA4B,CAAA;AAG1C,cAAc,oBAAoB,CAAA;AAGlC,cAAc,gBAAgB,CAAA;AAG9B,cAAc,iBAAiB,CAAA"}
|
package/dist/src/index.js
CHANGED
|
@@ -44,3 +44,7 @@ export * from './redis-rate-limiter.js';
|
|
|
44
44
|
export * from './redis-circuit-breaker.js';
|
|
45
45
|
// Linear API quota tracker (reads rate limit headers)
|
|
46
46
|
export * from './quota-tracker.js';
|
|
47
|
+
// A2A Protocol types
|
|
48
|
+
export * from './a2a-types.js';
|
|
49
|
+
// A2A Server (AgentCard + JSON-RPC handlers)
|
|
50
|
+
export * from './a2a-server.js';
|
package/dist/src/types.d.ts
CHANGED
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* Type of agent work being performed based on issue status
|
|
9
9
|
*/
|
|
10
|
-
export type AgentWorkType = 'research' | 'backlog-creation' | 'development' | 'inflight' | 'qa' | 'acceptance' | 'refinement' | 'coordination' | 'qa-coordination' | 'acceptance-coordination';
|
|
10
|
+
export type AgentWorkType = 'research' | 'backlog-creation' | 'development' | 'inflight' | 'qa' | 'acceptance' | 'refinement' | 'refinement-coordination' | 'coordination' | 'qa-coordination' | 'acceptance-coordination';
|
|
11
11
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/src/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB,UAAU,GACV,kBAAkB,GAClB,aAAa,GACb,UAAU,GACV,IAAI,GACJ,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,iBAAiB,GACjB,yBAAyB,CAAA"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB,UAAU,GACV,kBAAkB,GAClB,aAAa,GACb,UAAU,GACV,IAAI,GACJ,YAAY,GACZ,YAAY,GACZ,yBAAyB,GACzB,cAAc,GACd,iBAAiB,GACjB,yBAAyB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supaku/agentfactory-server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Webhook server and distributed worker pool for AgentFactory — Redis queues, issue locks, session management",
|
|
6
6
|
"author": "Supaku (https://supaku.com)",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"ioredis": "^5.4.2",
|
|
47
|
-
"@supaku/agentfactory": "0.7.
|
|
48
|
-
"@supaku/agentfactory-linear": "0.7.
|
|
47
|
+
"@supaku/agentfactory": "0.7.55",
|
|
48
|
+
"@supaku/agentfactory-linear": "0.7.55"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.5.4",
|