@aiwg/cli 2026.8.17 → 2026.8.19
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/bin/aiwg.mjs +24 -2
- package/dist/src/a2a/agent-card.js +4 -1
- package/dist/src/a2a/client.js +148 -68
- package/dist/src/a2a/codecs.js +480 -0
- package/dist/src/a2a/events.js +226 -0
- package/dist/src/a2a/hitl-driver.js +8 -6
- package/dist/src/a2a/hitl.js +2 -1
- package/dist/src/a2a/http.js +85 -5
- package/dist/src/a2a/protocol.js +136 -0
- package/dist/src/a2a/types.js +4 -14
- package/dist/src/a2a/webhook.js +101 -4
- package/dist/src/artifacts/index-builder.js +63 -12
- package/dist/src/artifacts/index-files.js +26 -5
- package/dist/src/artifacts/query-engine.js +67 -67
- package/dist/src/artifacts/stats.js +6 -2
- package/dist/src/artifacts/types.js +1 -1
- package/dist/src/audit/operator-decision.js +15 -1
- package/dist/src/channel/manager.mjs +89 -17
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/installation.js +79 -0
- package/dist/src/cli/handlers/refresh.js +6 -2
- package/dist/src/cli/handlers/runtime-info.js +9 -1
- package/dist/src/cli/handlers/serve.js +107 -4
- package/dist/src/cli/handlers/session.js +12 -26
- package/dist/src/cli/handlers/use.js +19 -4
- package/dist/src/cli/handlers/utilities.js +4 -0
- package/dist/src/cli/handlers/version.js +4 -0
- package/dist/src/config/user-config-dir.mjs +29 -0
- package/dist/src/config/user-config.js +4 -22
- package/dist/src/extensions/commands/definitions.js +20 -1
- package/dist/src/features/catalog.js +2 -1
- package/dist/src/flow/graph-metadata.js +56 -0
- package/dist/src/installation/manager-command.mjs +31 -0
- package/dist/src/installation/manager.mjs +264 -0
- package/dist/src/serve/a2a-terminal-observer.js +28 -5
- package/dist/src/serve/dispatch-router.js +32 -4
- package/dist/src/serve/executor-registry.js +29 -0
- package/dist/src/serve/mission-conductor.js +15 -1
- package/dist/src/serve/stack-adapters.js +2 -1
- package/dist/src/serve/telemetry.js +5 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/line-endings.js +12 -0
- package/dist/src/smiths/context-pipeline/managed-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +3 -1
- package/dist/src/update/checker.mjs +16 -15
- package/dist/src/update/notifier.mjs +8 -3
- package/dist/src/update/service.mjs +51 -5
- package/package.json +1 -1
- package/tools/agents/deploy-agents.mjs +28 -8
- package/tools/agents/providers/base.mjs +5 -3
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { decodeArtifact, decodeMessage, decodeStatus, decodeTask, A2AWireValidationError, } from './codecs.js';
|
|
2
|
+
import { isTerminalTaskState, } from './types.js';
|
|
3
|
+
/** Strict 1.0 StreamResponse decoder and separate 0.3 compatibility decoder. */
|
|
4
|
+
export function decodeStreamResponse(version, input, opts = {}) {
|
|
5
|
+
return version === '1.0'
|
|
6
|
+
? decodeV1StreamResponse(input, opts)
|
|
7
|
+
: decodeLegacyStreamEvent(input, opts);
|
|
8
|
+
}
|
|
9
|
+
export function decodeV1StreamResponse(input, opts = {}) {
|
|
10
|
+
const obj = objectAt('1.0', '$', input);
|
|
11
|
+
if (typeof obj.kind === 'string') {
|
|
12
|
+
fail('1.0', '$.kind', 'legacy event discriminator is not valid in StreamResponse');
|
|
13
|
+
}
|
|
14
|
+
const members = ['task', 'message', 'statusUpdate', 'artifactUpdate']
|
|
15
|
+
.filter(key => Object.prototype.hasOwnProperty.call(obj, key));
|
|
16
|
+
if (members.length !== 1) {
|
|
17
|
+
fail('1.0', '$', 'StreamResponse must contain exactly one of task, message, statusUpdate, artifactUpdate');
|
|
18
|
+
}
|
|
19
|
+
const base = eventBase('1.0', opts);
|
|
20
|
+
switch (members[0]) {
|
|
21
|
+
case 'task':
|
|
22
|
+
return { ...base, type: 'task', task: decodeTask('1.0', obj.task, '$.task') };
|
|
23
|
+
case 'message':
|
|
24
|
+
return { ...base, type: 'message', message: decodeMessage('1.0', obj.message, '$.message') };
|
|
25
|
+
case 'statusUpdate': {
|
|
26
|
+
const update = objectAt('1.0', '$.statusUpdate', obj.statusUpdate);
|
|
27
|
+
return {
|
|
28
|
+
...base,
|
|
29
|
+
type: 'status',
|
|
30
|
+
taskId: stringAt('1.0', '$.statusUpdate.taskId', update.taskId),
|
|
31
|
+
contextId: stringAt('1.0', '$.statusUpdate.contextId', update.contextId),
|
|
32
|
+
status: decodeStatus('1.0', update.status, '$.statusUpdate.status'),
|
|
33
|
+
...(update.metadata !== undefined
|
|
34
|
+
? { metadata: jsonObjectAt('1.0', '$.statusUpdate.metadata', update.metadata) }
|
|
35
|
+
: {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
case 'artifactUpdate': {
|
|
39
|
+
const update = objectAt('1.0', '$.artifactUpdate', obj.artifactUpdate);
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
type: 'artifact',
|
|
43
|
+
taskId: stringAt('1.0', '$.artifactUpdate.taskId', update.taskId),
|
|
44
|
+
contextId: stringAt('1.0', '$.artifactUpdate.contextId', update.contextId),
|
|
45
|
+
artifact: decodeArtifact('1.0', update.artifact, '$.artifactUpdate.artifact'),
|
|
46
|
+
...(update.append !== undefined ? { append: booleanAt('1.0', '$.artifactUpdate.append', update.append) } : {}),
|
|
47
|
+
...(update.lastChunk !== undefined ? { lastChunk: booleanAt('1.0', '$.artifactUpdate.lastChunk', update.lastChunk) } : {}),
|
|
48
|
+
...(update.metadata !== undefined
|
|
49
|
+
? { metadata: jsonObjectAt('1.0', '$.artifactUpdate.metadata', update.metadata) }
|
|
50
|
+
: {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
fail('1.0', '$', 'unknown StreamResponse member');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function decodeLegacyStreamEvent(input, opts = {}) {
|
|
58
|
+
const obj = objectAt('0.3', '$', input);
|
|
59
|
+
const base = eventBase('0.3', opts);
|
|
60
|
+
// Some 0.3 subscriptions use the SSE event name for a full initial Task.
|
|
61
|
+
if (typeof obj.id === 'string' && obj.status !== undefined) {
|
|
62
|
+
return { ...base, type: 'task', task: decodeTask('0.3', obj) };
|
|
63
|
+
}
|
|
64
|
+
const kind = typeof obj.kind === 'string' ? obj.kind : opts.eventName;
|
|
65
|
+
if (!kind)
|
|
66
|
+
fail('0.3', '$', 'legacy stream event requires kind or SSE event name');
|
|
67
|
+
switch (kind) {
|
|
68
|
+
case 'task-state':
|
|
69
|
+
return { ...base, type: 'task', task: decodeTask('0.3', obj.task, '$.task') };
|
|
70
|
+
case 'status-update':
|
|
71
|
+
return {
|
|
72
|
+
...base,
|
|
73
|
+
type: 'status',
|
|
74
|
+
taskId: stringAt('0.3', '$.taskId', obj.taskId),
|
|
75
|
+
status: decodeStatus('0.3', obj.status, '$.status'),
|
|
76
|
+
};
|
|
77
|
+
case 'artifact-update':
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
type: 'artifact',
|
|
81
|
+
taskId: stringAt('0.3', '$.taskId', obj.taskId),
|
|
82
|
+
artifact: decodeArtifact('0.3', obj.artifact, '$.artifact'),
|
|
83
|
+
...(obj.append !== undefined ? { append: booleanAt('0.3', '$.append', obj.append) } : {}),
|
|
84
|
+
};
|
|
85
|
+
default:
|
|
86
|
+
fail('0.3', '$.kind', `unsupported legacy event '${kind}'`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Shared ordering/ownership/state-transition gate for SSE and push. It returns
|
|
91
|
+
* null for an exact duplicate and throws before state mutation for invalid
|
|
92
|
+
* ownership, out-of-order delivery, missing initial snapshots, or terminal
|
|
93
|
+
* regression.
|
|
94
|
+
*/
|
|
95
|
+
export class A2AEventReconciler {
|
|
96
|
+
taskId;
|
|
97
|
+
contextId;
|
|
98
|
+
initialized;
|
|
99
|
+
terminal;
|
|
100
|
+
lastSequence;
|
|
101
|
+
eventIds = new Set();
|
|
102
|
+
constructor(opts) {
|
|
103
|
+
this.taskId = opts.taskId;
|
|
104
|
+
this.contextId = opts.contextId ?? opts.initialTask?.contextId;
|
|
105
|
+
this.initialized = opts.initialTask !== undefined || opts.requireInitialSnapshot !== true;
|
|
106
|
+
this.terminal = opts.initialTask ? isTerminalTaskState(opts.initialTask.status.state) : false;
|
|
107
|
+
}
|
|
108
|
+
accept(event) {
|
|
109
|
+
if (event.eventId && this.eventIds.has(event.eventId))
|
|
110
|
+
return null;
|
|
111
|
+
if (event.sequence !== undefined) {
|
|
112
|
+
if (this.lastSequence !== undefined && event.sequence <= this.lastSequence) {
|
|
113
|
+
if (event.sequence === this.lastSequence)
|
|
114
|
+
return null;
|
|
115
|
+
throw new Error(`A2A event sequence regressed from ${this.lastSequence} to ${event.sequence}`);
|
|
116
|
+
}
|
|
117
|
+
if (this.lastSequence !== undefined && event.sequence !== this.lastSequence + 1) {
|
|
118
|
+
throw new Error(`A2A event sequence gap after ${this.lastSequence}: received ${event.sequence}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const eventTaskId = taskIdOf(event);
|
|
122
|
+
if (eventTaskId && eventTaskId !== this.taskId) {
|
|
123
|
+
throw new Error(`A2A event belongs to task ${eventTaskId}, expected ${this.taskId}`);
|
|
124
|
+
}
|
|
125
|
+
const eventContextId = contextIdOf(event);
|
|
126
|
+
if (this.contextId && eventContextId && this.contextId !== eventContextId) {
|
|
127
|
+
throw new Error(`A2A event belongs to context ${eventContextId}, expected ${this.contextId}`);
|
|
128
|
+
}
|
|
129
|
+
if (!this.initialized) {
|
|
130
|
+
if (event.type !== 'task') {
|
|
131
|
+
throw new Error('A2A subscription must begin with a Task snapshot before deltas');
|
|
132
|
+
}
|
|
133
|
+
this.initialized = true;
|
|
134
|
+
}
|
|
135
|
+
if (this.terminal && event.type !== 'task') {
|
|
136
|
+
throw new Error(`A2A event '${event.type}' arrived after task ${this.taskId} became terminal`);
|
|
137
|
+
}
|
|
138
|
+
if (event.type === 'task') {
|
|
139
|
+
if (this.terminal && !isTerminalTaskState(event.task.status.state)) {
|
|
140
|
+
throw new Error(`A2A task ${this.taskId} cannot regress from terminal state`);
|
|
141
|
+
}
|
|
142
|
+
this.contextId ??= event.task.contextId;
|
|
143
|
+
this.terminal = isTerminalTaskState(event.task.status.state);
|
|
144
|
+
}
|
|
145
|
+
else if (event.type === 'status') {
|
|
146
|
+
this.contextId ??= event.contextId;
|
|
147
|
+
this.terminal = isTerminalTaskState(event.status.state);
|
|
148
|
+
}
|
|
149
|
+
else if (event.type === 'message') {
|
|
150
|
+
this.contextId ??= event.message.contextId;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.contextId ??= event.contextId;
|
|
154
|
+
}
|
|
155
|
+
if (event.eventId)
|
|
156
|
+
this.eventIds.add(event.eventId);
|
|
157
|
+
if (event.sequence !== undefined)
|
|
158
|
+
this.lastSequence = event.sequence;
|
|
159
|
+
return event;
|
|
160
|
+
}
|
|
161
|
+
isTerminal() {
|
|
162
|
+
return this.terminal;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function taskIdOf(event) {
|
|
166
|
+
if (event.type === 'task')
|
|
167
|
+
return event.task.id;
|
|
168
|
+
if (event.type === 'message')
|
|
169
|
+
return event.message.taskId;
|
|
170
|
+
return event.taskId;
|
|
171
|
+
}
|
|
172
|
+
function contextIdOf(event) {
|
|
173
|
+
if (event.type === 'task')
|
|
174
|
+
return event.task.contextId;
|
|
175
|
+
if (event.type === 'message')
|
|
176
|
+
return event.message.contextId;
|
|
177
|
+
return event.contextId;
|
|
178
|
+
}
|
|
179
|
+
function eventBase(version, opts) {
|
|
180
|
+
return {
|
|
181
|
+
protocolVersion: version,
|
|
182
|
+
...(opts.sequence !== undefined ? { sequence: opts.sequence } : {}),
|
|
183
|
+
...(opts.eventId !== undefined ? { eventId: opts.eventId } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function objectAt(version, path, value) {
|
|
187
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
188
|
+
fail(version, path, 'must be an object');
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
function stringAt(version, path, value) {
|
|
192
|
+
if (typeof value !== 'string' || !value)
|
|
193
|
+
fail(version, path, 'must be a non-empty string');
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function booleanAt(version, path, value) {
|
|
197
|
+
if (typeof value !== 'boolean')
|
|
198
|
+
fail(version, path, 'must be a boolean');
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
function jsonObjectAt(version, path, value) {
|
|
202
|
+
const object = objectAt(version, path, value);
|
|
203
|
+
assertJsonValue(version, path, object);
|
|
204
|
+
return object;
|
|
205
|
+
}
|
|
206
|
+
function assertJsonValue(version, path, value) {
|
|
207
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
208
|
+
return;
|
|
209
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
210
|
+
return;
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
value.forEach((entry, index) => assertJsonValue(version, `${path}[${index}]`, entry));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (value && typeof value === 'object') {
|
|
216
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
217
|
+
assertJsonValue(version, `${path}.${key}`, entry);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
fail(version, path, 'must contain only JSON values');
|
|
222
|
+
}
|
|
223
|
+
function fail(version, path, detail) {
|
|
224
|
+
throw new A2AWireValidationError(version, path, detail);
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -337,13 +337,13 @@ export async function driveOnePrompt(opts) {
|
|
|
337
337
|
}
|
|
338
338
|
// ── helpers ────────────────────────────────────────────────────────────
|
|
339
339
|
function extractEnvelopeFromEvent(event) {
|
|
340
|
-
if (event.
|
|
340
|
+
if (event.type === 'task') {
|
|
341
341
|
const result = extractHitlEnvelope(event.task);
|
|
342
342
|
if (result?.ok)
|
|
343
343
|
return result.envelope;
|
|
344
344
|
return null;
|
|
345
345
|
}
|
|
346
|
-
if (event.
|
|
346
|
+
if (event.type === 'status') {
|
|
347
347
|
const result = extractHitlEnvelope(event.status);
|
|
348
348
|
if (result?.ok)
|
|
349
349
|
return result.envelope;
|
|
@@ -352,14 +352,16 @@ function extractEnvelopeFromEvent(event) {
|
|
|
352
352
|
return null;
|
|
353
353
|
}
|
|
354
354
|
function inferEventContext(event) {
|
|
355
|
-
if (event.
|
|
355
|
+
if (event.type === 'task') {
|
|
356
356
|
return event.task.contextId !== undefined
|
|
357
357
|
? { contextId: event.task.contextId }
|
|
358
358
|
: {};
|
|
359
359
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
return event.type === 'status' || event.type === 'artifact'
|
|
361
|
+
? event.contextId !== undefined ? { contextId: event.contextId } : {}
|
|
362
|
+
: event.type === 'message' && event.message.contextId !== undefined
|
|
363
|
+
? { contextId: event.message.contextId }
|
|
364
|
+
: {};
|
|
363
365
|
}
|
|
364
366
|
function parseDeadline(deadline) {
|
|
365
367
|
if (!deadline)
|
package/dist/src/a2a/hitl.js
CHANGED
|
@@ -110,11 +110,12 @@ export function buildHitlResponseMessage(opts) {
|
|
|
110
110
|
role: 'user',
|
|
111
111
|
parts: [
|
|
112
112
|
{
|
|
113
|
-
|
|
113
|
+
type: 'data',
|
|
114
114
|
data: opts.response,
|
|
115
115
|
},
|
|
116
116
|
],
|
|
117
117
|
metadata: {
|
|
118
|
+
...(opts.metadata ?? {}),
|
|
118
119
|
hitl_response_for: {
|
|
119
120
|
prompt_id: opts.promptId,
|
|
120
121
|
payload: opts.response,
|
package/dist/src/a2a/http.js
CHANGED
|
@@ -15,12 +15,24 @@ export class A2AError extends Error {
|
|
|
15
15
|
status;
|
|
16
16
|
problem;
|
|
17
17
|
path;
|
|
18
|
-
|
|
18
|
+
category;
|
|
19
|
+
versionNotSupported;
|
|
20
|
+
constructor(status, path, problem, category) {
|
|
19
21
|
super(`${status} ${problem.code ?? problem.title}: ${problem.detail ?? problem.title}`);
|
|
20
22
|
this.name = 'A2AError';
|
|
21
23
|
this.status = status;
|
|
22
24
|
this.problem = problem;
|
|
23
25
|
this.path = path;
|
|
26
|
+
this.versionNotSupported = isVersionNotSupportedProblem(problem);
|
|
27
|
+
this.category = category ?? (this.versionNotSupported
|
|
28
|
+
? 'negotiation'
|
|
29
|
+
: status === 401 || status === 403
|
|
30
|
+
? 'authorization'
|
|
31
|
+
: status === 0
|
|
32
|
+
? 'transport'
|
|
33
|
+
: status >= 400
|
|
34
|
+
? 'application'
|
|
35
|
+
: 'transport');
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
38
|
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
@@ -34,6 +46,7 @@ export class A2AHttpClient {
|
|
|
34
46
|
onExtensionEchoMissing;
|
|
35
47
|
/** Per-process dedupe set: one log per (path, sunset_date). */
|
|
36
48
|
seenDeprecations = new Set();
|
|
49
|
+
protocolVersion;
|
|
37
50
|
constructor(opts) {
|
|
38
51
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
39
52
|
this.bearer = opts.bearer;
|
|
@@ -42,16 +55,21 @@ export class A2AHttpClient {
|
|
|
42
55
|
this.fetchImpl = opts.fetch ?? fetch;
|
|
43
56
|
this.onDeprecation = opts.onDeprecation;
|
|
44
57
|
this.onExtensionEchoMissing = opts.onExtensionEchoMissing;
|
|
58
|
+
this.protocolVersion = opts.protocolVersion ?? '0.3';
|
|
45
59
|
}
|
|
46
60
|
async request(path, options = {}) {
|
|
47
61
|
const method = (options.method ?? 'GET').toUpperCase();
|
|
48
62
|
const url = path.startsWith('http') ? path : this.baseUrl + path;
|
|
63
|
+
const protocolVersion = options.protocolVersion ?? this.protocolVersion;
|
|
64
|
+
const mediaType = protocolVersion === '1.0' ? 'application/a2a+json' : 'application/json';
|
|
49
65
|
const headers = {
|
|
50
66
|
authorization: `Bearer ${options.bearer ?? this.bearer}`,
|
|
51
|
-
accept:
|
|
67
|
+
accept: mediaType,
|
|
52
68
|
};
|
|
69
|
+
if (protocolVersion === '1.0')
|
|
70
|
+
headers['a2a-version'] = '1.0';
|
|
53
71
|
if (options.body !== undefined && options.bodyRaw === undefined) {
|
|
54
|
-
headers['content-type'] =
|
|
72
|
+
headers['content-type'] = mediaType;
|
|
55
73
|
}
|
|
56
74
|
// Inject A2A-Extensions on mutating calls. Caller can override with
|
|
57
75
|
// options.extensions (empty array clears injection).
|
|
@@ -74,7 +92,18 @@ export class A2AHttpClient {
|
|
|
74
92
|
else if (options.body !== undefined) {
|
|
75
93
|
init.body = JSON.stringify(options.body);
|
|
76
94
|
}
|
|
77
|
-
|
|
95
|
+
let resp;
|
|
96
|
+
try {
|
|
97
|
+
resp = await this.fetchImpl(url, init);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new A2AError(0, path, {
|
|
101
|
+
type: 'about:blank',
|
|
102
|
+
title: 'A2A transport failure',
|
|
103
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
104
|
+
code: 'aiwg.transport_failure',
|
|
105
|
+
}, 'transport');
|
|
106
|
+
}
|
|
78
107
|
// Capture deprecation headers regardless of status.
|
|
79
108
|
const deprecation = captureDeprecation(path, resp.headers);
|
|
80
109
|
if (deprecation) {
|
|
@@ -121,6 +150,18 @@ export class A2AHttpClient {
|
|
|
121
150
|
// Parse body (JSON, problem+json, or empty).
|
|
122
151
|
let body;
|
|
123
152
|
const ct = resp.headers.get('content-type') ?? '';
|
|
153
|
+
if (protocolVersion === '1.0'
|
|
154
|
+
&& resp.status < 400
|
|
155
|
+
&& resp.status !== 204
|
|
156
|
+
&& resp.status !== 205
|
|
157
|
+
&& !ct.toLowerCase().includes('application/a2a+json')) {
|
|
158
|
+
throw new A2AError(502, path, {
|
|
159
|
+
type: 'about:blank',
|
|
160
|
+
title: 'Invalid A2A 1.0 content type',
|
|
161
|
+
detail: `Expected application/a2a+json, received ${ct || '(missing)'}`,
|
|
162
|
+
code: 'aiwg.invalid_content_type',
|
|
163
|
+
}, 'transport');
|
|
164
|
+
}
|
|
124
165
|
if (resp.status !== 204 && resp.status !== 205) {
|
|
125
166
|
const text = await resp.text();
|
|
126
167
|
if (text.length > 0) {
|
|
@@ -143,7 +184,7 @@ export class A2AHttpClient {
|
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
if (resp.status >= 400) {
|
|
146
|
-
const problem = body ?? {
|
|
187
|
+
const problem = normalizeProblemDetails(resp.status, body) ?? {
|
|
147
188
|
type: 'about:blank',
|
|
148
189
|
title: `HTTP ${resp.status}`,
|
|
149
190
|
};
|
|
@@ -159,6 +200,45 @@ export class A2AHttpClient {
|
|
|
159
200
|
};
|
|
160
201
|
}
|
|
161
202
|
}
|
|
203
|
+
export function isVersionNotSupportedProblem(problem) {
|
|
204
|
+
const type = problem.type?.toLowerCase() ?? '';
|
|
205
|
+
const code = problem.code?.toLowerCase() ?? '';
|
|
206
|
+
return type.includes('version-not-supported')
|
|
207
|
+
|| code === 'versionnotsupportederror'
|
|
208
|
+
|| code === 'version_not_supported'
|
|
209
|
+
|| code === 'a2a.version_not_supported'
|
|
210
|
+
|| code === '-32009';
|
|
211
|
+
}
|
|
212
|
+
function normalizeProblemDetails(status, body) {
|
|
213
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
214
|
+
return undefined;
|
|
215
|
+
const obj = body;
|
|
216
|
+
// HTTP+JSON uses RFC 7807. JSON-RPC bindings may nest the standard code.
|
|
217
|
+
const nested = obj.error && typeof obj.error === 'object' && !Array.isArray(obj.error)
|
|
218
|
+
? obj.error
|
|
219
|
+
: undefined;
|
|
220
|
+
const source = nested ?? obj;
|
|
221
|
+
const codeValue = source.code;
|
|
222
|
+
const code = typeof codeValue === 'string' || typeof codeValue === 'number'
|
|
223
|
+
? String(codeValue)
|
|
224
|
+
: undefined;
|
|
225
|
+
const title = typeof obj.title === 'string'
|
|
226
|
+
? obj.title
|
|
227
|
+
: typeof source.message === 'string'
|
|
228
|
+
? source.message
|
|
229
|
+
: `HTTP ${status}`;
|
|
230
|
+
const problem = {
|
|
231
|
+
type: typeof obj.type === 'string' ? obj.type : 'about:blank',
|
|
232
|
+
title,
|
|
233
|
+
status,
|
|
234
|
+
...(typeof obj.detail === 'string' ? { detail: obj.detail } : {}),
|
|
235
|
+
...(code ? { code } : {}),
|
|
236
|
+
...(Array.isArray(obj.supportedVersions) && obj.supportedVersions.every(v => typeof v === 'string')
|
|
237
|
+
? { supportedVersions: obj.supportedVersions }
|
|
238
|
+
: {}),
|
|
239
|
+
};
|
|
240
|
+
return problem;
|
|
241
|
+
}
|
|
162
242
|
// ---------- header helpers ----------
|
|
163
243
|
function parseExtensionList(header) {
|
|
164
244
|
if (!header)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const A2A_HTTP_JSON_BINDING = 'HTTP+JSON';
|
|
2
|
+
export const A2A_LEGACY_REST_BINDING = 'REST';
|
|
3
|
+
export class A2ANegotiationError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'A2ANegotiationError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function normalizeProtocolVersion(value) {
|
|
12
|
+
if (typeof value !== 'string')
|
|
13
|
+
return null;
|
|
14
|
+
const match = /^(0\.3|1\.0)(?:\.\d+)?$/.exec(value.trim());
|
|
15
|
+
return match?.[1] ?? null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse 0.3 top-level cards and 1.0 per-interface cards into one discovery
|
|
19
|
+
* model. A card that claims top-level 1.0 while retaining only 0.3 interface
|
|
20
|
+
* fields is rejected instead of being treated as proof of 1.0 support.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeAgentCard(card) {
|
|
23
|
+
if (!card || typeof card !== 'object') {
|
|
24
|
+
throw new A2ANegotiationError('agent_card.invalid', 'AgentCard must be an object');
|
|
25
|
+
}
|
|
26
|
+
if (typeof card.name !== 'string' || !card.name.trim()) {
|
|
27
|
+
throw new A2ANegotiationError('agent_card.name_missing', 'AgentCard.name is required');
|
|
28
|
+
}
|
|
29
|
+
if (typeof card.version !== 'string' || !card.version.trim()) {
|
|
30
|
+
throw new A2ANegotiationError('agent_card.version_missing', 'AgentCard.version is required');
|
|
31
|
+
}
|
|
32
|
+
const topVersion = normalizeProtocolVersion(card.protocolVersion);
|
|
33
|
+
if (card.protocolVersion !== undefined && !topVersion) {
|
|
34
|
+
throw new A2ANegotiationError('agent_card.protocol_version_invalid', `Unsupported AgentCard.protocolVersion '${String(card.protocolVersion)}'`);
|
|
35
|
+
}
|
|
36
|
+
const interfaces = [];
|
|
37
|
+
for (const [preference, entry] of (card.supportedInterfaces ?? []).entries()) {
|
|
38
|
+
if (!entry || typeof entry !== 'object' || typeof entry.url !== 'string') {
|
|
39
|
+
throw new A2ANegotiationError('agent_card.interface_invalid', `supportedInterfaces[${preference}] must contain an absolute URL`);
|
|
40
|
+
}
|
|
41
|
+
assertAbsoluteUrl(entry.url, `supportedInterfaces[${preference}].url`);
|
|
42
|
+
const interfaceVersion = normalizeProtocolVersion(entry.protocolVersion);
|
|
43
|
+
if (entry.protocolVersion !== undefined && !interfaceVersion) {
|
|
44
|
+
throw new A2ANegotiationError('agent_card.interface_version_invalid', `supportedInterfaces[${preference}].protocolVersion is unsupported`);
|
|
45
|
+
}
|
|
46
|
+
if (interfaceVersion) {
|
|
47
|
+
if (typeof entry.protocolBinding !== 'string' || !entry.protocolBinding.trim()) {
|
|
48
|
+
throw new A2ANegotiationError('agent_card.interface_binding_missing', `supportedInterfaces[${preference}] declares ${interfaceVersion} without protocolBinding`);
|
|
49
|
+
}
|
|
50
|
+
interfaces.push({
|
|
51
|
+
url: trimUrl(entry.url),
|
|
52
|
+
protocolBinding: entry.protocolBinding,
|
|
53
|
+
protocolVersion: interfaceVersion,
|
|
54
|
+
...(entry.tenant ? { tenant: entry.tenant } : {}),
|
|
55
|
+
preference,
|
|
56
|
+
legacy: false,
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// 0.3 cards put the version at top level and binding under `transport`.
|
|
61
|
+
if (topVersion !== '0.3') {
|
|
62
|
+
throw new A2ANegotiationError('agent_card.mixed_interface_shape', `supportedInterfaces[${preference}] uses legacy transport fields without a 0.3 top-level declaration`);
|
|
63
|
+
}
|
|
64
|
+
if (typeof entry.transport !== 'string' || !entry.transport.trim()) {
|
|
65
|
+
throw new A2ANegotiationError('agent_card.interface_transport_missing', `supportedInterfaces[${preference}] requires transport for the 0.3 card shape`);
|
|
66
|
+
}
|
|
67
|
+
interfaces.push({
|
|
68
|
+
url: trimUrl(entry.url),
|
|
69
|
+
protocolBinding: entry.transport,
|
|
70
|
+
protocolVersion: '0.3',
|
|
71
|
+
...(entry.tenant ? { tenant: entry.tenant } : {}),
|
|
72
|
+
preference,
|
|
73
|
+
legacy: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Legacy cards commonly omit supportedInterfaces entirely.
|
|
77
|
+
if (topVersion === '0.3' && typeof card.url === 'string') {
|
|
78
|
+
assertAbsoluteUrl(card.url, 'AgentCard.url');
|
|
79
|
+
if (!interfaces.some(entry => entry.protocolVersion === '0.3' && entry.url === trimUrl(card.url))) {
|
|
80
|
+
interfaces.push({
|
|
81
|
+
url: trimUrl(card.url),
|
|
82
|
+
protocolBinding: card.preferredTransport ?? A2A_LEGACY_REST_BINDING,
|
|
83
|
+
protocolVersion: '0.3',
|
|
84
|
+
preference: interfaces.length,
|
|
85
|
+
legacy: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (topVersion === '1.0' && interfaces.length === 0) {
|
|
90
|
+
throw new A2ANegotiationError('agent_card.v1_interfaces_missing', 'A2A 1.0 cards must declare versioned supportedInterfaces; top-level protocolVersion/url are 0.3 fields');
|
|
91
|
+
}
|
|
92
|
+
if (interfaces.length === 0) {
|
|
93
|
+
throw new A2ANegotiationError('agent_card.interfaces_missing', 'AgentCard exposes no usable protocol interface');
|
|
94
|
+
}
|
|
95
|
+
return { card, interfaces };
|
|
96
|
+
}
|
|
97
|
+
export function selectAgentInterface(input, opts) {
|
|
98
|
+
const normalized = 'interfaces' in input ? input : normalizeAgentCard(input);
|
|
99
|
+
const bindings = opts.bindings ?? [A2A_HTTP_JSON_BINDING, A2A_LEGACY_REST_BINDING];
|
|
100
|
+
const versions = opts.policy === 'auto'
|
|
101
|
+
? opts.versionPreference ?? ['1.0', '0.3']
|
|
102
|
+
: [opts.policy];
|
|
103
|
+
for (const version of versions) {
|
|
104
|
+
for (const binding of bindings) {
|
|
105
|
+
const selected = normalized.interfaces
|
|
106
|
+
.filter(entry => entry.protocolVersion === version && sameBinding(entry.protocolBinding, binding))
|
|
107
|
+
.sort((a, b) => a.preference - b.preference)[0];
|
|
108
|
+
if (selected)
|
|
109
|
+
return selected;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const available = normalized.interfaces
|
|
113
|
+
.map(entry => `${entry.protocolVersion}/${entry.protocolBinding}@${entry.url}`)
|
|
114
|
+
.join(', ');
|
|
115
|
+
throw new A2ANegotiationError('agent_card.no_compatible_interface', `No compatible A2A interface for policy=${opts.policy}; available: ${available || 'none'}`);
|
|
116
|
+
}
|
|
117
|
+
export function agentInterfaceCacheKey(host, instanceId, selected) {
|
|
118
|
+
return [host, instanceId, selected.protocolVersion, selected.protocolBinding, selected.url].join('|');
|
|
119
|
+
}
|
|
120
|
+
function sameBinding(actual, supported) {
|
|
121
|
+
return actual.trim().toUpperCase() === supported.trim().toUpperCase();
|
|
122
|
+
}
|
|
123
|
+
function trimUrl(value) {
|
|
124
|
+
return value.replace(/\/+$/, '');
|
|
125
|
+
}
|
|
126
|
+
function assertAbsoluteUrl(value, field) {
|
|
127
|
+
try {
|
|
128
|
+
const url = new URL(value);
|
|
129
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
130
|
+
throw new Error('not HTTP(S)');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new A2ANegotiationError('agent_card.url_invalid', `${field} must be an absolute HTTP(S) URL`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=protocol.js.map
|
package/dist/src/a2a/types.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
// A2A
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// - send_message.rs — POST /agents/{id}/v1/messages:send
|
|
6
|
-
// - get_task.rs — GET /agents/{id}/v1/tasks/{tid}
|
|
7
|
-
// - cancel_task.rs — POST /agents/{id}/v1/tasks/{tid}/cancel
|
|
8
|
-
// - subscribe_task — GET /agents/{id}/v1/tasks/{tid}/subscribe (SSE)
|
|
9
|
-
// - push_delivery.rs — * /agents/{id}/v1/tasks/{tid}/pushNotificationConfigs/*
|
|
10
|
-
// - agent_card.rs — GET /agents/{id}/.well-known/agent-card.json
|
|
11
|
-
// GET /agents/{id}/v1/extendedAgentCard
|
|
12
|
-
//
|
|
13
|
-
// Types are intentionally permissive (extra fields allowed). We track only
|
|
14
|
-
// what the AIWG orchestrator inspects; the rest is forwarded opaquely.
|
|
1
|
+
// Normalized A2A domain types. Protocol 0.3 and 1.0 wire values are decoded
|
|
2
|
+
// into these types at the boundary in codecs.ts. Application code must not
|
|
3
|
+
// depend on either version's enum spellings, kind fields, or oneof layout.
|
|
4
|
+
export { AIWG_GRAPH_METADATA_KEY } from '../flow/graph-metadata.js';
|
|
15
5
|
export const TERMINAL_TASK_STATES = [
|
|
16
6
|
'completed',
|
|
17
7
|
'failed',
|