@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.
Files changed (40) hide show
  1. package/README.md +445 -41
  2. package/dist/capability-diagnostics.d.ts +13 -0
  3. package/dist/capability-diagnostics.d.ts.map +1 -0
  4. package/dist/capability-diagnostics.js +131 -0
  5. package/dist/capability-diagnostics.js.map +1 -0
  6. package/dist/errors.d.ts +1 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +1 -0
  9. package/dist/errors.js.map +1 -1
  10. package/dist/request-normalization.d.ts +4 -10
  11. package/dist/request-normalization.d.ts.map +1 -1
  12. package/dist/request-normalization.js +83 -47
  13. package/dist/request-normalization.js.map +1 -1
  14. package/dist/result-shape.d.ts +66 -15
  15. package/dist/result-shape.d.ts.map +1 -1
  16. package/dist/result-shape.js +333 -85
  17. package/dist/result-shape.js.map +1 -1
  18. package/dist/schemas.d.ts +54 -5
  19. package/dist/schemas.d.ts.map +1 -1
  20. package/dist/schemas.js +275 -23
  21. package/dist/schemas.js.map +1 -1
  22. package/dist/sdk-client-pool.d.ts +0 -2
  23. package/dist/sdk-client-pool.d.ts.map +1 -1
  24. package/dist/sdk-client-pool.js +2 -22
  25. package/dist/sdk-client-pool.js.map +1 -1
  26. package/dist/service.d.ts +5 -2
  27. package/dist/service.d.ts.map +1 -1
  28. package/dist/service.js +411 -79
  29. package/dist/service.js.map +1 -1
  30. package/dist/target-catalog.d.ts +27 -5
  31. package/dist/target-catalog.d.ts.map +1 -1
  32. package/dist/target-catalog.js +164 -58
  33. package/dist/target-catalog.js.map +1 -1
  34. package/dist/task-handle-registry.d.ts +3 -2
  35. package/dist/task-handle-registry.d.ts.map +1 -1
  36. package/dist/task-handle-registry.js +37 -5
  37. package/dist/task-handle-registry.js.map +1 -1
  38. package/openclaw.plugin.json +42 -12
  39. package/package.json +10 -11
  40. 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, summarizeStreamEvent, watchSuccess, } from "./result-shape.js";
6
- import { buildRequestOptions, normalizePlainIntentRequest, } from "./request-normalization.js";
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 firstStreamTaskId(events) {
36
- for (const event of events) {
37
- switch (event.kind) {
38
- case "message":
39
- if (event.taskId !== undefined) {
40
- return event.taskId;
41
- }
42
- break;
43
- case "task":
44
- return event.id;
45
- case "status-update":
46
- case "artifact-update":
47
- return event.taskId;
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 undefined;
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
- async function consumeStream(stream, action, target, state, onUpdate) {
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
- state.events.push(event);
113
- state.latestSummary = summarizeStreamEvent(target, event);
114
- onUpdate?.(streamUpdate(action, target, event));
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
- async resolveSendTarget(input) {
223
- const explicitTarget = this.resolveExplicitTarget(input);
224
- const target = explicitTarget ?? this.targetCatalog.resolveDefaultTarget();
225
- if (!target) {
226
- throw missingTargetContextError("send");
227
- }
228
- return this.resolveClient(target);
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
- const clientEntry = await this.clientPool.get(handleRecord.target);
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: handleRecord.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 clientEntry = await this.clientPool.get(target);
257
- return {
258
- target,
259
- clientEntry,
260
- taskId: input.task_id,
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, { target, taskId })
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({ target, taskId }).taskHandle;
476
+ return this.taskHandleRegistry.create({
477
+ target,
478
+ taskId,
479
+ ...(contextId !== undefined ? { contextId } : {}),
480
+ }).taskHandle;
269
481
  }
270
- bindStreamTaskHandle(target, events, taskHandle) {
271
- const taskId = firstStreamTaskId(events);
272
- return taskId !== undefined
273
- ? this.bindTaskHandle(target, taskId, taskHandle)
274
- : undefined;
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 resolved = await this.resolveSendTarget(input);
298
- target = resolved.target;
299
- const normalized = normalizePlainIntentRequest(input, {
300
- defaultTimeoutMs: this.config.defaults.timeoutMs,
301
- defaultServiceParameters: this.config.defaults.serviceParameters,
302
- signal: options.signal,
303
- });
304
- const raw = await resolved.clientEntry.client.sendMessage(normalized.sendParams, normalized.requestOptions);
305
- const taskHandle = raw.kind === "task" ? this.bindTaskHandle(target, raw.id) : undefined;
306
- return sendSuccess(target, raw, taskHandle);
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
- const toolError = toToolError(error, fallbackErrorCode(error));
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
- const resolved = await this.resolveSendTarget(input);
328
- target = resolved.target;
329
- const normalized = normalizePlainIntentRequest(input, {
330
- defaultTimeoutMs: this.config.defaults.timeoutMs,
331
- defaultServiceParameters: this.config.defaults.serviceParameters,
332
- signal: options.signal,
333
- });
334
- await consumeStream(resolved.clientEntry.client.sendMessageStream(normalized.sendParams, normalized.requestOptions), "send", target, state, options.onUpdate);
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.bindStreamTaskHandle(target, state.events));
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.bindTaskHandle(target, raw.id, resolved.taskHandle));
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.target.streamingSupported === false) {
400
- throw streamingNotSupportedError(resolved.target, resolved.taskId);
730
+ if (resolved.taskId === undefined) {
731
+ throw missingTargetContextError("watch");
401
732
  }
402
- const params = {
403
- id: resolved.taskId,
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.bindStreamTaskHandle(target, state.events, resolved.taskHandle));
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.bindTaskHandle(target, raw.id, resolved.taskHandle));
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));