@aramisfa/openclaw-a2a-outbound 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +445 -41
- package/dist/capability-diagnostics.d.ts +13 -0
- package/dist/capability-diagnostics.d.ts.map +1 -0
- package/dist/capability-diagnostics.js +131 -0
- package/dist/capability-diagnostics.js.map +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/request-normalization.d.ts +4 -10
- package/dist/request-normalization.d.ts.map +1 -1
- package/dist/request-normalization.js +83 -47
- package/dist/request-normalization.js.map +1 -1
- package/dist/result-shape.d.ts +66 -15
- package/dist/result-shape.d.ts.map +1 -1
- package/dist/result-shape.js +333 -85
- package/dist/result-shape.js.map +1 -1
- package/dist/schemas.d.ts +54 -5
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +275 -23
- package/dist/schemas.js.map +1 -1
- package/dist/sdk-client-pool.d.ts +0 -2
- package/dist/sdk-client-pool.d.ts.map +1 -1
- package/dist/sdk-client-pool.js +2 -22
- package/dist/sdk-client-pool.js.map +1 -1
- package/dist/service.d.ts +5 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +411 -79
- package/dist/service.js.map +1 -1
- package/dist/target-catalog.d.ts +27 -5
- package/dist/target-catalog.d.ts.map +1 -1
- package/dist/target-catalog.js +164 -58
- package/dist/target-catalog.js.map +1 -1
- package/dist/task-handle-registry.d.ts +3 -2
- package/dist/task-handle-registry.d.ts.map +1 -1
- package/dist/task-handle-registry.js +37 -5
- package/dist/task-handle-registry.js.map +1 -1
- package/openclaw.plugin.json +42 -12
- package/package.json +10 -11
- package/skills/remote-agent/SKILL.md +85 -12
package/dist/service.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { UnsupportedOperationError } from "@a2a-js/sdk/client";
|
|
3
|
+
import { evaluateSendCompatibility, } from "./capability-diagnostics.js";
|
|
2
4
|
import { parseA2AOutboundPluginConfig, } from "./config.js";
|
|
3
5
|
import { A2AOutboundError, ERROR_CODES, toToolError, } from "./errors.js";
|
|
4
6
|
import { log, startSpan } from "./logging.js";
|
|
5
|
-
import { cancelSuccess, listTargetsSuccess, remoteAgentFailure, sendStreamSuccess, sendSuccess, statusSuccess, streamUpdate,
|
|
6
|
-
import { buildRequestOptions,
|
|
7
|
+
import { cancelSuccess, listTargetsSuccess, remoteAgentFailure, sendStreamSuccess, sendSuccess, statusSuccess, streamUpdate, summarizeStreamEvents, watchSuccess, } from "./result-shape.js";
|
|
8
|
+
import { buildRequestOptions, normalizeSendRequest, normalizeStrictTaskCreationSendRequest, } from "./request-normalization.js";
|
|
7
9
|
import { createClientPool, } from "./sdk-client-pool.js";
|
|
8
10
|
import { buildRemoteAgentToolDefinition, createRemoteAgentInputValidator, } from "./schemas.js";
|
|
9
11
|
import { createTargetCatalog, } from "./target-catalog.js";
|
|
@@ -32,22 +34,59 @@ function targetSummary(target) {
|
|
|
32
34
|
...(target.alias !== undefined ? { alias: target.alias } : {}),
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
function taskContextFromMessage(message) {
|
|
38
|
+
return {
|
|
39
|
+
...(message.taskId !== undefined ? { taskId: message.taskId } : {}),
|
|
40
|
+
...(message.contextId !== undefined ? { contextId: message.contextId } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function taskContextFromTask(task) {
|
|
44
|
+
return {
|
|
45
|
+
taskId: task.id,
|
|
46
|
+
...(task.contextId !== undefined ? { contextId: task.contextId } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function taskContextFromStatusUpdate(event) {
|
|
50
|
+
return {
|
|
51
|
+
...(event.taskId !== undefined ? { taskId: event.taskId } : {}),
|
|
52
|
+
...(event.contextId !== undefined ? { contextId: event.contextId } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function taskContextFromArtifactUpdate(event) {
|
|
56
|
+
return {
|
|
57
|
+
...(event.taskId !== undefined ? { taskId: event.taskId } : {}),
|
|
58
|
+
...(event.contextId !== undefined ? { contextId: event.contextId } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function taskContextFromEvent(event) {
|
|
62
|
+
switch (event.kind) {
|
|
63
|
+
case "message":
|
|
64
|
+
return taskContextFromMessage(event);
|
|
65
|
+
case "task":
|
|
66
|
+
return taskContextFromTask(event);
|
|
67
|
+
case "status-update":
|
|
68
|
+
return taskContextFromStatusUpdate(event);
|
|
69
|
+
case "artifact-update":
|
|
70
|
+
return taskContextFromArtifactUpdate(event);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function mergeTaskContext(...contexts) {
|
|
74
|
+
const merged = {};
|
|
75
|
+
for (const context of contexts) {
|
|
76
|
+
if (!context) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (context.taskId !== undefined) {
|
|
80
|
+
merged.taskId = context.taskId;
|
|
81
|
+
}
|
|
82
|
+
if (context.contextId !== undefined) {
|
|
83
|
+
merged.contextId = context.contextId;
|
|
84
|
+
}
|
|
85
|
+
if (context.taskHandle !== undefined) {
|
|
86
|
+
merged.taskHandle = context.taskHandle;
|
|
48
87
|
}
|
|
49
88
|
}
|
|
50
|
-
return
|
|
89
|
+
return merged;
|
|
51
90
|
}
|
|
52
91
|
function isRecord(value) {
|
|
53
92
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -90,12 +129,22 @@ function taskHandleTaskIdMismatchError(taskHandle, handleTaskId, explicitTaskId)
|
|
|
90
129
|
explicit_task_id: explicitTaskId,
|
|
91
130
|
});
|
|
92
131
|
}
|
|
132
|
+
function taskHandleContextIdMismatchError(taskHandle, handleContextId, explicitContextId) {
|
|
133
|
+
return new A2AOutboundError(ERROR_CODES.VALIDATION_ERROR, `task_handle "${taskHandle}" does not match context_id "${explicitContextId}"`, {
|
|
134
|
+
task_handle: taskHandle,
|
|
135
|
+
handle_context_id: handleContextId,
|
|
136
|
+
explicit_context_id: explicitContextId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
93
139
|
function missingTargetContextError(action) {
|
|
94
140
|
const message = action === "send"
|
|
95
|
-
? "send requires target_alias, target_url, or a configured default target"
|
|
141
|
+
? "send requires task_handle, target_alias, target_url, or a configured default target"
|
|
96
142
|
: `${action} requires task_handle, or task_id plus target_alias/target_url, or a configured default target`;
|
|
97
143
|
return new A2AOutboundError(ERROR_CODES.VALIDATION_ERROR, message);
|
|
98
144
|
}
|
|
145
|
+
function conversationOnlyLifecycleContinuationError(action) {
|
|
146
|
+
return new A2AOutboundError(ERROR_CODES.VALIDATION_ERROR, `${action} requires task continuity; summary.continuation.conversation.context_id is send-only`);
|
|
147
|
+
}
|
|
99
148
|
function streamingNotSupportedError(target, taskId) {
|
|
100
149
|
return new A2AOutboundError(ERROR_CODES.A2A_SDK_ERROR, `streaming updates are not available for task ${taskId}; retry with action=status`, {
|
|
101
150
|
task_id: taskId,
|
|
@@ -107,11 +156,65 @@ function streamingNotSupportedError(target, taskId) {
|
|
|
107
156
|
suggested_action: "status",
|
|
108
157
|
});
|
|
109
158
|
}
|
|
110
|
-
|
|
159
|
+
function isTerminalTaskStatus(status) {
|
|
160
|
+
return (status === "completed" ||
|
|
161
|
+
status === "failed" ||
|
|
162
|
+
status === "canceled" ||
|
|
163
|
+
status === "rejected");
|
|
164
|
+
}
|
|
165
|
+
function isStrictTaskRequirement(input) {
|
|
166
|
+
return input.task_requirement === "required";
|
|
167
|
+
}
|
|
168
|
+
function taskRequiredButMessageReturnedError(message) {
|
|
169
|
+
return new A2AOutboundError(ERROR_CODES.TASK_REQUIRED_BUT_MESSAGE_RETURNED, 'task_requirement="required" expected a Task response, but the peer returned only a Message', {
|
|
170
|
+
...(message.contextId !== undefined ? { context_id: message.contextId } : {}),
|
|
171
|
+
...(message.messageId !== undefined ? { message_id: message.messageId } : {}),
|
|
172
|
+
response_kind: "message",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function taskSnapshotIdentity(task) {
|
|
176
|
+
return {
|
|
177
|
+
id: task.id,
|
|
178
|
+
...(task.contextId !== undefined ? { contextId: task.contextId } : {}),
|
|
179
|
+
status: structuredClone(task.status),
|
|
180
|
+
...(task.history !== undefined ? { history: structuredClone(task.history) } : {}),
|
|
181
|
+
...(task.artifacts !== undefined
|
|
182
|
+
? { artifacts: structuredClone(task.artifacts) }
|
|
183
|
+
: {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function equivalentTaskSnapshots(initialTask, event) {
|
|
187
|
+
return (event.kind === "task" &&
|
|
188
|
+
isDeepStrictEqual(taskSnapshotIdentity(initialTask), taskSnapshotIdentity(event)));
|
|
189
|
+
}
|
|
190
|
+
function withRecoverableStatusSuggestion(error, taskContext) {
|
|
191
|
+
return withErrorDetails(error, {
|
|
192
|
+
task_id: taskContext.taskId,
|
|
193
|
+
...(taskContext.taskHandle !== undefined
|
|
194
|
+
? { task_handle: taskContext.taskHandle }
|
|
195
|
+
: {}),
|
|
196
|
+
...(taskContext.contextId !== undefined
|
|
197
|
+
? { context_id: taskContext.contextId }
|
|
198
|
+
: {}),
|
|
199
|
+
suggested_action: "status",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function appendStreamEvent(action, target, state, event, onUpdate) {
|
|
203
|
+
state.events.push(event);
|
|
204
|
+
state.taskContext = mergeTaskContext(state.taskContext, taskContextFromEvent(event));
|
|
205
|
+
state.latestSummary = summarizeStreamEvents(target, state.events, state.taskContext);
|
|
206
|
+
onUpdate?.(streamUpdate(action, target, state.events, state.taskContext));
|
|
207
|
+
}
|
|
208
|
+
async function consumeStream(stream, action, target, state, onEvent, onUpdate, shouldSkipEvent) {
|
|
209
|
+
let index = 0;
|
|
111
210
|
for await (const event of stream) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
211
|
+
if (shouldSkipEvent?.(event, index)) {
|
|
212
|
+
index += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
index += 1;
|
|
216
|
+
onEvent?.(event);
|
|
217
|
+
appendStreamEvent(action, target, state, event, onUpdate);
|
|
115
218
|
}
|
|
116
219
|
}
|
|
117
220
|
function requestedAction(input) {
|
|
@@ -142,7 +245,6 @@ export class A2AOutboundService {
|
|
|
142
245
|
createClientPool({
|
|
143
246
|
defaultCardPath: this.config.defaults.cardPath,
|
|
144
247
|
preferredTransports: this.config.defaults.preferredTransports,
|
|
145
|
-
acceptedOutputModes: this.config.policy.acceptedOutputModes,
|
|
146
248
|
normalizeBaseUrl: this.config.policy.normalizeBaseUrl,
|
|
147
249
|
enforceSupportedTransports: this.config.policy.enforceSupportedTransports,
|
|
148
250
|
});
|
|
@@ -203,8 +305,19 @@ export class A2AOutboundService {
|
|
|
203
305
|
}
|
|
204
306
|
async resolveClient(target) {
|
|
205
307
|
const clientEntry = await this.clientPool.get(target);
|
|
308
|
+
let resolvedTarget = target;
|
|
309
|
+
try {
|
|
310
|
+
const card = await clientEntry.client.getAgentCard();
|
|
311
|
+
resolvedTarget = this.targetCatalog.recordAgentCard(target, card);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
log(this.logger, "warn", "a2a.remote_agent.target_card.refresh_error", {
|
|
315
|
+
target,
|
|
316
|
+
error: toToolError(error, fallbackErrorCode(error)),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
206
319
|
return {
|
|
207
|
-
target,
|
|
320
|
+
target: resolvedTarget,
|
|
208
321
|
clientEntry,
|
|
209
322
|
};
|
|
210
323
|
}
|
|
@@ -219,15 +332,83 @@ export class A2AOutboundService {
|
|
|
219
332
|
}
|
|
220
333
|
return undefined;
|
|
221
334
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
335
|
+
resolveTrustedContinuationTarget(target) {
|
|
336
|
+
return this.clientPool.normalizeTarget({
|
|
337
|
+
baseUrl: target.target_url,
|
|
338
|
+
cardPath: target.card_path,
|
|
339
|
+
preferredTransports: target.preferred_transports,
|
|
340
|
+
...(target.target_alias !== undefined
|
|
341
|
+
? { alias: target.target_alias }
|
|
342
|
+
: {}),
|
|
343
|
+
});
|
|
229
344
|
}
|
|
230
345
|
async resolveTaskContext(input) {
|
|
346
|
+
if (input.continuation !== undefined) {
|
|
347
|
+
const continuationTarget = this.resolveTrustedContinuationTarget(input.continuation.target);
|
|
348
|
+
const continuationTask = input.continuation.task;
|
|
349
|
+
const continuationContextId = input.continuation.conversation?.context_id;
|
|
350
|
+
if (input.action !== "send" &&
|
|
351
|
+
continuationTask === undefined &&
|
|
352
|
+
continuationContextId !== undefined) {
|
|
353
|
+
throw conversationOnlyLifecycleContinuationError(input.action);
|
|
354
|
+
}
|
|
355
|
+
if (continuationTask?.task_handle !== undefined) {
|
|
356
|
+
try {
|
|
357
|
+
const handleRecord = this.taskHandleRegistry.resolve(continuationTask.task_handle);
|
|
358
|
+
if (continuationTask.task_id !== handleRecord.taskId) {
|
|
359
|
+
throw taskHandleTaskIdMismatchError(continuationTask.task_handle, handleRecord.taskId, continuationTask.task_id);
|
|
360
|
+
}
|
|
361
|
+
if (targetIdentity(continuationTarget) !==
|
|
362
|
+
targetIdentity(handleRecord.target)) {
|
|
363
|
+
throw taskHandleTargetMismatchError(continuationTask.task_handle, handleRecord.target, continuationTarget);
|
|
364
|
+
}
|
|
365
|
+
if (continuationContextId !== undefined &&
|
|
366
|
+
handleRecord.contextId !== undefined &&
|
|
367
|
+
continuationContextId !== handleRecord.contextId) {
|
|
368
|
+
throw taskHandleContextIdMismatchError(continuationTask.task_handle, handleRecord.contextId, continuationContextId);
|
|
369
|
+
}
|
|
370
|
+
const clientResolution = await this.resolveClient(handleRecord.target);
|
|
371
|
+
return {
|
|
372
|
+
target: clientResolution.target,
|
|
373
|
+
clientEntry: clientResolution.clientEntry,
|
|
374
|
+
taskId: handleRecord.taskId,
|
|
375
|
+
...(continuationContextId !== undefined
|
|
376
|
+
? { contextId: continuationContextId }
|
|
377
|
+
: handleRecord.contextId !== undefined
|
|
378
|
+
? { contextId: handleRecord.contextId }
|
|
379
|
+
: {}),
|
|
380
|
+
taskHandle: continuationTask.task_handle,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (error instanceof A2AOutboundError &&
|
|
385
|
+
(error.code === ERROR_CODES.UNKNOWN_TASK_HANDLE ||
|
|
386
|
+
error.code === ERROR_CODES.EXPIRED_TASK_HANDLE)) {
|
|
387
|
+
const clientResolution = await this.resolveClient(continuationTarget);
|
|
388
|
+
return {
|
|
389
|
+
target: clientResolution.target,
|
|
390
|
+
clientEntry: clientResolution.clientEntry,
|
|
391
|
+
taskId: continuationTask.task_id,
|
|
392
|
+
...(continuationContextId !== undefined
|
|
393
|
+
? { contextId: continuationContextId }
|
|
394
|
+
: {}),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const clientResolution = await this.resolveClient(continuationTarget);
|
|
401
|
+
return {
|
|
402
|
+
target: clientResolution.target,
|
|
403
|
+
clientEntry: clientResolution.clientEntry,
|
|
404
|
+
...(continuationTask !== undefined
|
|
405
|
+
? { taskId: continuationTask.task_id }
|
|
406
|
+
: {}),
|
|
407
|
+
...(continuationContextId !== undefined
|
|
408
|
+
? { contextId: continuationContextId }
|
|
409
|
+
: {}),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
231
412
|
if (input.task_handle !== undefined) {
|
|
232
413
|
const handleRecord = this.taskHandleRegistry.resolve(input.task_handle);
|
|
233
414
|
if (input.task_id !== undefined && input.task_id !== handleRecord.taskId) {
|
|
@@ -238,40 +419,117 @@ export class A2AOutboundService {
|
|
|
238
419
|
targetIdentity(explicitTarget) !== targetIdentity(handleRecord.target)) {
|
|
239
420
|
throw taskHandleTargetMismatchError(input.task_handle, handleRecord.target, explicitTarget);
|
|
240
421
|
}
|
|
241
|
-
|
|
422
|
+
if ("context_id" in input &&
|
|
423
|
+
typeof input.context_id === "string" &&
|
|
424
|
+
handleRecord.contextId !== undefined &&
|
|
425
|
+
input.context_id !== handleRecord.contextId) {
|
|
426
|
+
throw taskHandleContextIdMismatchError(input.task_handle, handleRecord.contextId, input.context_id);
|
|
427
|
+
}
|
|
428
|
+
const clientResolution = await this.resolveClient(handleRecord.target);
|
|
242
429
|
return {
|
|
243
|
-
target:
|
|
244
|
-
clientEntry,
|
|
430
|
+
target: clientResolution.target,
|
|
431
|
+
clientEntry: clientResolution.clientEntry,
|
|
245
432
|
taskId: handleRecord.taskId,
|
|
433
|
+
...("context_id" in input && input.context_id !== undefined
|
|
434
|
+
? { contextId: input.context_id }
|
|
435
|
+
: handleRecord.contextId !== undefined
|
|
436
|
+
? { contextId: handleRecord.contextId }
|
|
437
|
+
: {}),
|
|
246
438
|
taskHandle: input.task_handle,
|
|
247
439
|
};
|
|
248
440
|
}
|
|
249
|
-
if (input.task_id === undefined) {
|
|
250
|
-
throw missingTargetContextError(input.action);
|
|
251
|
-
}
|
|
252
441
|
const target = this.resolveExplicitTarget(input) ?? this.targetCatalog.resolveDefaultTarget();
|
|
253
442
|
if (!target) {
|
|
254
443
|
throw missingTargetContextError(input.action);
|
|
255
444
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
445
|
+
const clientResolution = await this.resolveClient(target);
|
|
446
|
+
if (input.task_id !== undefined) {
|
|
447
|
+
return {
|
|
448
|
+
target: clientResolution.target,
|
|
449
|
+
clientEntry: clientResolution.clientEntry,
|
|
450
|
+
taskId: input.task_id,
|
|
451
|
+
...("context_id" in input && input.context_id !== undefined
|
|
452
|
+
? { contextId: input.context_id }
|
|
453
|
+
: {}),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (input.action === "send") {
|
|
457
|
+
return {
|
|
458
|
+
target: clientResolution.target,
|
|
459
|
+
clientEntry: clientResolution.clientEntry,
|
|
460
|
+
...("context_id" in input && input.context_id !== undefined
|
|
461
|
+
? { contextId: input.context_id }
|
|
462
|
+
: {}),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
throw missingTargetContextError(input.action);
|
|
262
466
|
}
|
|
263
|
-
bindTaskHandle(target, taskId, taskHandle) {
|
|
467
|
+
bindTaskHandle(target, taskId, taskHandle, contextId) {
|
|
264
468
|
if (taskHandle !== undefined) {
|
|
265
|
-
return this.taskHandleRegistry.refresh(taskHandle, {
|
|
469
|
+
return this.taskHandleRegistry.refresh(taskHandle, {
|
|
470
|
+
target,
|
|
471
|
+
taskId,
|
|
472
|
+
...(contextId !== undefined ? { contextId } : {}),
|
|
473
|
+
})
|
|
266
474
|
.taskHandle;
|
|
267
475
|
}
|
|
268
|
-
return this.taskHandleRegistry.create({
|
|
476
|
+
return this.taskHandleRegistry.create({
|
|
477
|
+
target,
|
|
478
|
+
taskId,
|
|
479
|
+
...(contextId !== undefined ? { contextId } : {}),
|
|
480
|
+
}).taskHandle;
|
|
269
481
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
482
|
+
bindTaskContext(target, taskContext) {
|
|
483
|
+
if (taskContext.taskId === undefined) {
|
|
484
|
+
return taskContext;
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
...taskContext,
|
|
488
|
+
taskHandle: this.bindTaskHandle(target, taskContext.taskId, taskContext.taskHandle, taskContext.contextId),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
prepareSend(input, options, requestBuilder = normalizeSendRequest) {
|
|
492
|
+
return this.resolveTaskContext(input).then((resolved) => {
|
|
493
|
+
const normalized = requestBuilder({
|
|
494
|
+
...input,
|
|
495
|
+
...(resolved.taskId !== undefined ? { task_id: resolved.taskId } : {}),
|
|
496
|
+
...(resolved.contextId !== undefined
|
|
497
|
+
? { context_id: resolved.contextId }
|
|
498
|
+
: {}),
|
|
499
|
+
}, {
|
|
500
|
+
defaultTimeoutMs: this.config.defaults.timeoutMs,
|
|
501
|
+
defaultServiceParameters: this.config.defaults.serviceParameters,
|
|
502
|
+
defaultAcceptedOutputModes: this.config.policy.acceptedOutputModes,
|
|
503
|
+
signal: options.signal,
|
|
504
|
+
});
|
|
505
|
+
const capabilityDiagnostics = evaluateSendCompatibility(input, normalized.sendParams.configuration?.acceptedOutputModes ?? [], this.targetCatalog.getCardSnapshot(resolved.target.baseUrl));
|
|
506
|
+
return {
|
|
507
|
+
resolved,
|
|
508
|
+
normalized,
|
|
509
|
+
capabilityDiagnostics,
|
|
510
|
+
};
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
taskQueryOptions(input, signal) {
|
|
514
|
+
return buildRequestOptions(input.timeout_ms, this.config.defaults.timeoutMs, this.config.defaults.serviceParameters, input.service_parameters, signal);
|
|
515
|
+
}
|
|
516
|
+
async consumeResubscribeStream(action, resolved, input, state, options, dedupeInitialTask) {
|
|
517
|
+
if (resolved.taskId === undefined) {
|
|
518
|
+
throw missingTargetContextError("watch");
|
|
519
|
+
}
|
|
520
|
+
const peerCard = this.targetCatalog.getCardSnapshot(resolved.target.baseUrl);
|
|
521
|
+
if (peerCard?.capabilities.streaming === false ||
|
|
522
|
+
(peerCard === undefined && resolved.target.streamingSupported === false)) {
|
|
523
|
+
throw streamingNotSupportedError(resolved.target, resolved.taskId);
|
|
524
|
+
}
|
|
525
|
+
const params = {
|
|
526
|
+
id: resolved.taskId,
|
|
527
|
+
};
|
|
528
|
+
await consumeStream(resolved.clientEntry.client.resubscribeTask(params, this.taskQueryOptions(input, options.signal)), action, resolved.target, state, () => {
|
|
529
|
+
state.taskContext = this.bindTaskContext(resolved.target, state.taskContext);
|
|
530
|
+
}, options.onUpdate, (event, index) => index === 0 &&
|
|
531
|
+
dedupeInitialTask !== undefined &&
|
|
532
|
+
equivalentTaskSnapshots(dedupeInitialTask, event));
|
|
275
533
|
}
|
|
276
534
|
async listTargets() {
|
|
277
535
|
const span = startSpan(this.tracer, "a2a.remote_agent.list_targets");
|
|
@@ -293,20 +551,31 @@ export class A2AOutboundService {
|
|
|
293
551
|
async send(input, options) {
|
|
294
552
|
const span = startSpan(this.tracer, "a2a.remote_agent.send");
|
|
295
553
|
let target;
|
|
554
|
+
let capabilityDiagnostics;
|
|
296
555
|
try {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
556
|
+
const prepared = await this.prepareSend(input, options, isStrictTaskRequirement(input)
|
|
557
|
+
? normalizeStrictTaskCreationSendRequest
|
|
558
|
+
: normalizeSendRequest);
|
|
559
|
+
target = prepared.resolved.target;
|
|
560
|
+
capabilityDiagnostics = prepared.capabilityDiagnostics;
|
|
561
|
+
const raw = await prepared.resolved.clientEntry.client.sendMessage(prepared.normalized.sendParams, prepared.normalized.requestOptions);
|
|
562
|
+
if (isStrictTaskRequirement(input)) {
|
|
563
|
+
if (raw.kind !== "task") {
|
|
564
|
+
throw taskRequiredButMessageReturnedError(raw);
|
|
565
|
+
}
|
|
566
|
+
return sendSuccess(target, raw, this.bindTaskContext(target, mergeTaskContext(prepared.resolved, taskContextFromTask(raw))));
|
|
567
|
+
}
|
|
568
|
+
return sendSuccess(target, raw, this.bindTaskContext(target, mergeTaskContext(prepared.resolved, raw.kind === "task"
|
|
569
|
+
? taskContextFromTask(raw)
|
|
570
|
+
: taskContextFromMessage(raw))));
|
|
307
571
|
}
|
|
308
572
|
catch (error) {
|
|
309
|
-
|
|
573
|
+
let toolError = toToolError(error, fallbackErrorCode(error));
|
|
574
|
+
if (capabilityDiagnostics !== undefined) {
|
|
575
|
+
toolError = withErrorDetails(toolError, {
|
|
576
|
+
capability_diagnostics: capabilityDiagnostics,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
310
579
|
log(this.logger, "error", "a2a.remote_agent.send.error", {
|
|
311
580
|
target,
|
|
312
581
|
error: toolError,
|
|
@@ -320,25 +589,83 @@ export class A2AOutboundService {
|
|
|
320
589
|
async sendStream(input, options) {
|
|
321
590
|
const span = startSpan(this.tracer, "a2a.remote_agent.send_stream");
|
|
322
591
|
let target;
|
|
592
|
+
let capabilityDiagnostics;
|
|
323
593
|
const state = {
|
|
324
594
|
events: [],
|
|
595
|
+
taskContext: {},
|
|
325
596
|
};
|
|
326
597
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
598
|
+
if (isStrictTaskRequirement(input)) {
|
|
599
|
+
const prepared = await this.prepareSend(input, options, normalizeStrictTaskCreationSendRequest);
|
|
600
|
+
target = prepared.resolved.target;
|
|
601
|
+
capabilityDiagnostics = prepared.capabilityDiagnostics;
|
|
602
|
+
const raw = await prepared.resolved.clientEntry.client.sendMessage(prepared.normalized.sendParams, prepared.normalized.requestOptions);
|
|
603
|
+
if (raw.kind !== "task") {
|
|
604
|
+
throw taskRequiredButMessageReturnedError(raw);
|
|
605
|
+
}
|
|
606
|
+
const createdTaskContext = this.bindTaskContext(target, mergeTaskContext(prepared.resolved, taskContextFromTask(raw)));
|
|
607
|
+
state.taskContext = mergeTaskContext(state.taskContext, createdTaskContext);
|
|
608
|
+
appendStreamEvent("send", target, state, raw, options.onUpdate);
|
|
609
|
+
if (isTerminalTaskStatus(raw.status.state)) {
|
|
610
|
+
return sendStreamSuccess(target, state.events, state.taskContext);
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
await this.consumeResubscribeStream("send", {
|
|
614
|
+
...prepared.resolved,
|
|
615
|
+
...createdTaskContext,
|
|
616
|
+
}, input, state, options, raw);
|
|
617
|
+
if (state.events.length === 1) {
|
|
618
|
+
throw new A2AOutboundError(ERROR_CODES.A2A_SDK_ERROR, "stream ended without watch events");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
let effectiveError = error;
|
|
623
|
+
if (effectiveError instanceof UnsupportedOperationError) {
|
|
624
|
+
effectiveError = streamingNotSupportedError(target, raw.id);
|
|
625
|
+
}
|
|
626
|
+
let toolError = toToolError(effectiveError, fallbackErrorCode(effectiveError));
|
|
627
|
+
toolError = withRecoverableStatusSuggestion(toolError, createdTaskContext);
|
|
628
|
+
if (capabilityDiagnostics !== undefined) {
|
|
629
|
+
toolError = withErrorDetails(toolError, {
|
|
630
|
+
capability_diagnostics: capabilityDiagnostics,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
if (state.events.length > 0 && state.latestSummary) {
|
|
634
|
+
toolError = withErrorDetails(toolError, {
|
|
635
|
+
partial_event_count: state.events.length,
|
|
636
|
+
latest_event_summary: state.latestSummary,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
log(this.logger, "error", "a2a.remote_agent.send_stream.error", {
|
|
640
|
+
target,
|
|
641
|
+
error: toolError,
|
|
642
|
+
});
|
|
643
|
+
return remoteAgentFailure("send", toolError);
|
|
644
|
+
}
|
|
645
|
+
if (state.events.length === 0) {
|
|
646
|
+
throw new A2AOutboundError(ERROR_CODES.A2A_SDK_ERROR, "stream ended without events");
|
|
647
|
+
}
|
|
648
|
+
return sendStreamSuccess(target, state.events, state.taskContext);
|
|
649
|
+
}
|
|
650
|
+
const prepared = await this.prepareSend(input, options);
|
|
651
|
+
target = prepared.resolved.target;
|
|
652
|
+
capabilityDiagnostics = prepared.capabilityDiagnostics;
|
|
653
|
+
state.taskContext = mergeTaskContext(state.taskContext, prepared.resolved);
|
|
654
|
+
await consumeStream(prepared.resolved.clientEntry.client.sendMessageStream(prepared.normalized.sendParams, prepared.normalized.requestOptions), "send", target, state, (event) => {
|
|
655
|
+
state.taskContext = this.bindTaskContext(prepared.resolved.target, mergeTaskContext(state.taskContext, taskContextFromEvent(event)));
|
|
656
|
+
}, options.onUpdate);
|
|
335
657
|
if (state.events.length === 0) {
|
|
336
658
|
throw new A2AOutboundError(ERROR_CODES.A2A_SDK_ERROR, "stream ended without events");
|
|
337
659
|
}
|
|
338
|
-
return sendStreamSuccess(target, state.events, this.
|
|
660
|
+
return sendStreamSuccess(target, state.events, this.bindTaskContext(target, state.taskContext));
|
|
339
661
|
}
|
|
340
662
|
catch (error) {
|
|
341
663
|
let toolError = toToolError(error, fallbackErrorCode(error));
|
|
664
|
+
if (capabilityDiagnostics !== undefined) {
|
|
665
|
+
toolError = withErrorDetails(toolError, {
|
|
666
|
+
capability_diagnostics: capabilityDiagnostics,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
342
669
|
if (state.events.length > 0 && state.latestSummary) {
|
|
343
670
|
toolError = withErrorDetails(toolError, {
|
|
344
671
|
partial_event_count: state.events.length,
|
|
@@ -363,6 +690,9 @@ export class A2AOutboundService {
|
|
|
363
690
|
const resolved = await this.resolveTaskContext(input);
|
|
364
691
|
target = resolved.target;
|
|
365
692
|
taskId = resolved.taskId;
|
|
693
|
+
if (resolved.taskId === undefined) {
|
|
694
|
+
throw missingTargetContextError("status");
|
|
695
|
+
}
|
|
366
696
|
const params = {
|
|
367
697
|
id: resolved.taskId,
|
|
368
698
|
...(input.history_length !== undefined
|
|
@@ -370,7 +700,7 @@ export class A2AOutboundService {
|
|
|
370
700
|
: {}),
|
|
371
701
|
};
|
|
372
702
|
const raw = await resolved.clientEntry.client.getTask(params, buildRequestOptions(input.timeout_ms, this.config.defaults.timeoutMs, this.config.defaults.serviceParameters, input.service_parameters, options.signal));
|
|
373
|
-
return statusSuccess(target, raw, this.
|
|
703
|
+
return statusSuccess(target, raw, this.bindTaskContext(target, mergeTaskContext(resolved, taskContextFromTask(raw))));
|
|
374
704
|
}
|
|
375
705
|
catch (error) {
|
|
376
706
|
const toolError = toToolError(error, fallbackErrorCode(error));
|
|
@@ -391,22 +721,21 @@ export class A2AOutboundService {
|
|
|
391
721
|
let taskId;
|
|
392
722
|
const state = {
|
|
393
723
|
events: [],
|
|
724
|
+
taskContext: {},
|
|
394
725
|
};
|
|
395
726
|
try {
|
|
396
727
|
const resolved = await this.resolveTaskContext(input);
|
|
397
728
|
target = resolved.target;
|
|
398
729
|
taskId = resolved.taskId;
|
|
399
|
-
if (resolved.
|
|
400
|
-
throw
|
|
730
|
+
if (resolved.taskId === undefined) {
|
|
731
|
+
throw missingTargetContextError("watch");
|
|
401
732
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
};
|
|
405
|
-
await consumeStream(resolved.clientEntry.client.resubscribeTask(params, buildRequestOptions(input.timeout_ms, this.config.defaults.timeoutMs, this.config.defaults.serviceParameters, input.service_parameters, options.signal)), "watch", target, state, options.onUpdate);
|
|
733
|
+
state.taskContext = mergeTaskContext(state.taskContext, resolved);
|
|
734
|
+
await this.consumeResubscribeStream("watch", resolved, input, state, options);
|
|
406
735
|
if (state.events.length === 0) {
|
|
407
736
|
throw new A2AOutboundError(ERROR_CODES.A2A_SDK_ERROR, "stream ended without events");
|
|
408
737
|
}
|
|
409
|
-
return watchSuccess(target, state.events, this.
|
|
738
|
+
return watchSuccess(target, state.events, this.bindTaskContext(target, state.taskContext));
|
|
410
739
|
}
|
|
411
740
|
catch (error) {
|
|
412
741
|
let effectiveError = error;
|
|
@@ -441,11 +770,14 @@ export class A2AOutboundService {
|
|
|
441
770
|
const resolved = await this.resolveTaskContext(input);
|
|
442
771
|
target = resolved.target;
|
|
443
772
|
taskId = resolved.taskId;
|
|
773
|
+
if (resolved.taskId === undefined) {
|
|
774
|
+
throw missingTargetContextError("cancel");
|
|
775
|
+
}
|
|
444
776
|
const params = {
|
|
445
777
|
id: resolved.taskId,
|
|
446
778
|
};
|
|
447
779
|
const raw = await resolved.clientEntry.client.cancelTask(params, buildRequestOptions(input.timeout_ms, this.config.defaults.timeoutMs, this.config.defaults.serviceParameters, input.service_parameters, options.signal));
|
|
448
|
-
return cancelSuccess(target, raw, this.
|
|
780
|
+
return cancelSuccess(target, raw, this.bindTaskContext(target, mergeTaskContext(resolved, taskContextFromTask(raw))));
|
|
449
781
|
}
|
|
450
782
|
catch (error) {
|
|
451
783
|
const toolError = toToolError(error, fallbackErrorCode(error));
|