@adcp/sdk 14.0.0-beta.5 → 14.0.0-beta.6

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 (80) hide show
  1. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  2. package/dist/lib/core/AgentClient.js +26 -10
  3. package/dist/lib/core/AgentClient.js.map +1 -1
  4. package/dist/lib/core/AgentClient.mjs +26 -10
  5. package/dist/lib/core/AgentClient.mjs.map +1 -1
  6. package/dist/lib/core/AsyncHandler.d.mts +4 -2
  7. package/dist/lib/core/AsyncHandler.d.ts +4 -2
  8. package/dist/lib/core/AsyncHandler.d.ts.map +1 -1
  9. package/dist/lib/core/AsyncHandler.js +15 -1
  10. package/dist/lib/core/AsyncHandler.js.map +1 -1
  11. package/dist/lib/core/AsyncHandler.mjs +15 -1
  12. package/dist/lib/core/AsyncHandler.mjs.map +1 -1
  13. package/dist/lib/core/SingleAgentClient.d.mts +3 -1
  14. package/dist/lib/core/SingleAgentClient.d.ts +3 -1
  15. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  16. package/dist/lib/core/SingleAgentClient.js +158 -39
  17. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  18. package/dist/lib/core/SingleAgentClient.mjs +167 -40
  19. package/dist/lib/core/SingleAgentClient.mjs.map +1 -1
  20. package/dist/lib/core/TaskExecutor.d.mts +57 -2
  21. package/dist/lib/core/TaskExecutor.d.ts +57 -2
  22. package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
  23. package/dist/lib/core/TaskExecutor.js +382 -44
  24. package/dist/lib/core/TaskExecutor.js.map +1 -1
  25. package/dist/lib/core/TaskExecutor.mjs +382 -45
  26. package/dist/lib/core/TaskExecutor.mjs.map +1 -1
  27. package/dist/lib/core/request-proposals-guard.d.mts +11 -0
  28. package/dist/lib/core/request-proposals-guard.d.ts +12 -0
  29. package/dist/lib/core/request-proposals-guard.d.ts.map +1 -0
  30. package/dist/lib/core/request-proposals-guard.js +65 -0
  31. package/dist/lib/core/request-proposals-guard.js.map +1 -0
  32. package/dist/lib/core/request-proposals-guard.mjs +39 -0
  33. package/dist/lib/core/request-proposals-guard.mjs.map +1 -0
  34. package/dist/lib/core/webhook-registration.d.mts +5 -0
  35. package/dist/lib/core/webhook-registration.d.ts +5 -0
  36. package/dist/lib/core/webhook-registration.d.ts.map +1 -1
  37. package/dist/lib/core/webhook-registration.js +13 -0
  38. package/dist/lib/core/webhook-registration.js.map +1 -1
  39. package/dist/lib/core/webhook-registration.mjs +13 -0
  40. package/dist/lib/core/webhook-registration.mjs.map +1 -1
  41. package/dist/lib/media-buy/compatibility.d.mts +45 -10
  42. package/dist/lib/media-buy/compatibility.d.ts +45 -10
  43. package/dist/lib/media-buy/compatibility.d.ts.map +1 -1
  44. package/dist/lib/media-buy/compatibility.js +295 -113
  45. package/dist/lib/media-buy/compatibility.js.map +1 -1
  46. package/dist/lib/media-buy/compatibility.mjs +295 -113
  47. package/dist/lib/media-buy/compatibility.mjs.map +1 -1
  48. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  49. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  50. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  51. package/dist/lib/types/core.generated.d.mts +334 -85
  52. package/dist/lib/types/core.generated.d.ts +334 -85
  53. package/dist/lib/types/core.generated.d.ts.map +1 -1
  54. package/dist/lib/types/core.generated.js.map +1 -1
  55. package/dist/lib/types/decline-proposals.d.ts +0 -5
  56. package/dist/lib/types/index.d.mts +2 -0
  57. package/dist/lib/types/index.d.ts +2 -0
  58. package/dist/lib/types/index.d.ts.map +1 -1
  59. package/dist/lib/types/index.js.map +1 -1
  60. package/dist/lib/types/index.mjs.map +1 -1
  61. package/dist/lib/types/request-proposals.d.ts +254 -36
  62. package/dist/lib/types/schemas.generated.d.ts +3123 -835
  63. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  64. package/dist/lib/types/schemas.generated.js +192 -85
  65. package/dist/lib/types/schemas.generated.js.map +1 -1
  66. package/dist/lib/types/schemas.generated.mjs +192 -85
  67. package/dist/lib/types/schemas.generated.mjs.map +1 -1
  68. package/dist/lib/types/tools.generated.d.mts +1 -121
  69. package/dist/lib/types/tools.generated.d.ts +1 -121
  70. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  71. package/dist/lib/types/tools.generated.js.map +1 -1
  72. package/dist/lib/version.d.mts +3 -3
  73. package/dist/lib/version.d.ts +3 -3
  74. package/dist/lib/version.js +3 -3
  75. package/dist/lib/version.js.map +1 -1
  76. package/dist/lib/version.mjs +3 -3
  77. package/dist/lib/version.mjs.map +1 -1
  78. package/docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md +28 -14
  79. package/docs/llms.txt +1 -1
  80. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "crypto";
1
+ import { createHash, randomUUID } from "crypto";
2
2
  import {
3
3
  ProtocolClient,
4
4
  normalizeTransportOptions,
@@ -65,6 +65,14 @@ class BeforeProtocolDispatchHookError extends Error {
65
65
  }
66
66
  original;
67
67
  }
68
+ class AfterProtocolDispatchHookError extends Error {
69
+ constructor(original) {
70
+ super("An SDK post-dispatch settlement hook failed.", { cause: original });
71
+ this.original = original;
72
+ this.name = "AfterProtocolDispatchHookError";
73
+ }
74
+ original;
75
+ }
68
76
  const GOVERNED_CREDENTIAL_SCAN_MAX_NODES = 1e4;
69
77
  const GOVERNED_CREDENTIAL_SCAN_MAX_DEPTH = 64;
70
78
  const GOVERNED_CALLBACK_FIELDS = /* @__PURE__ */ new Set(["push_notification_config", "reporting_webhook", "artifact_webhook"]);
@@ -230,6 +238,8 @@ function resolveIdempotencyKey(taskName, params, serverVersion) {
230
238
  return void 0;
231
239
  }
232
240
  const COMPACTED_TASK_STATE_LIMIT = 1e4;
241
+ const DEFAULT_EXTERNAL_TASK_SETTLEMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
242
+ const MAX_TIMER_DELAY_MS = 2147483647;
233
243
  const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
234
244
  "completed",
235
245
  "failed",
@@ -238,9 +248,14 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
238
248
  "governance-denied",
239
249
  "aborted"
240
250
  ]);
251
+ const MAX_PENDING_EXTERNAL_TASK_OBSERVATIONS = 8;
252
+ const MAX_RETAINED_EXTERNAL_TASK_ERROR_CHARS = 1024;
241
253
  class TaskExecutor {
242
254
  constructor(config = {}) {
243
255
  this.config = config;
256
+ if (config.externalTaskSettlementRetentionMs !== void 0 && (!Number.isSafeInteger(config.externalTaskSettlementRetentionMs) || config.externalTaskSettlementRetentionMs < 1)) {
257
+ throw new ConfigurationError("externalTaskSettlementRetentionMs must be a positive safe integer.");
258
+ }
244
259
  this.responseParser = new ProtocolResponseParser();
245
260
  if (config.enableConversationStorage) {
246
261
  this.conversationStorage = /* @__PURE__ */ new Map();
@@ -265,6 +280,14 @@ class TaskExecutor {
265
280
  }
266
281
  }
267
282
  config;
283
+ deferredTerminalPublicationTaskIds = /* @__PURE__ */ new Set();
284
+ settlementCapacityReservations = /* @__PURE__ */ new Set();
285
+ closedExternalTaskSettlementTaskIds = /* @__PURE__ */ new Set();
286
+ externalTaskSettlementHandlers = /* @__PURE__ */ new Map();
287
+ externalTaskSettlementInFlight = /* @__PURE__ */ new Map();
288
+ settledExternalTaskObservationKeys = /* @__PURE__ */ new Map();
289
+ externalTaskSettlementExpiry = /* @__PURE__ */ new Map();
290
+ pendingExternalTaskObservations = /* @__PURE__ */ new Map();
268
291
  responseParser;
269
292
  activeTasks = /* @__PURE__ */ new Map();
270
293
  compactedTaskIds = /* @__PURE__ */ new Map();
@@ -290,9 +313,43 @@ class TaskExecutor {
290
313
  *
291
314
  * @internal
292
315
  */
293
- observeExternalTaskStatus(taskId, status, result) {
294
- if (!["completed", "failed", "rejected", "canceled"].includes(status)) return;
316
+ async observeExternalTaskStatus(taskId, status, result, identity = {}) {
317
+ if (!["completed", "failed", "rejected", "canceled", "governance-denied", "aborted"].includes(status)) {
318
+ return { settled: false, result };
319
+ }
320
+ const observation = { status, result, ...identity };
321
+ const observationKey = this.externalTaskObservationKey(observation);
322
+ if (this.closedExternalTaskSettlementTaskIds.has(taskId)) {
323
+ const accepted = this.settledExternalTaskObservationKeys.get(taskId);
324
+ if (accepted?.key === observationKey) {
325
+ return {
326
+ settled: true,
327
+ duplicate: true,
328
+ status: accepted.status,
329
+ ...accepted.error !== void 0 && { error: accepted.error }
330
+ };
331
+ }
332
+ throw new Error("This task already completed durable settlement; the pushed result was not accepted.");
333
+ }
334
+ if (this.deferredTerminalPublicationTaskIds.has(taskId)) {
335
+ const handler = this.externalTaskSettlementHandlers.get(taskId);
336
+ if (handler) return this.settleExternalTaskStatus(taskId, handler, observation);
337
+ return new Promise((resolve, reject) => {
338
+ const pending = this.pendingExternalTaskObservations.get(taskId) ?? [];
339
+ if (pending.length >= MAX_PENDING_EXTERNAL_TASK_OBSERVATIONS) {
340
+ reject(new Error("Too many terminal push notifications are awaiting durable task settlement."));
341
+ return;
342
+ }
343
+ pending.push({ observation, resolve, reject });
344
+ this.pendingExternalTaskObservations.set(taskId, pending);
345
+ });
346
+ }
295
347
  this.updateTaskStatus(taskId, status, result);
348
+ return { settled: false, result };
349
+ }
350
+ /** Whether this process can safely settle or reject a durable callback for the operation. */
351
+ hasExternalTaskSettlementRoute(taskId) {
352
+ return this.deferredTerminalPublicationTaskIds.has(taskId) || this.closedExternalTaskSettlementTaskIds.has(taskId) || this.externalTaskSettlementHandlers.has(taskId);
296
353
  }
297
354
  /**
298
355
  * Run the configured pre-send schema check against the user-facing request
@@ -389,6 +446,7 @@ class TaskExecutor {
389
446
  }
390
447
  async executeTaskWithinDeadline(agent, taskName, params, inputHandler, options = {}, serverVersion, targetCapabilities, beforeProtocolDispatch) {
391
448
  if (serverVersion) this.lastKnownServerVersion = serverVersion;
449
+ params = structuredClone(params);
392
450
  const taskId = getTaskOperationId(options) ?? randomUUID();
393
451
  const startTime = Date.now();
394
452
  const workingTimeout = this.config.workingTimeout || 12e4;
@@ -411,9 +469,14 @@ class TaskExecutor {
411
469
  idempotencyKey
412
470
  };
413
471
  this.activeTasks.set(taskId, taskState);
472
+ let dispatchBoundaryOwnsTerminalState = false;
414
473
  const taskAbortListener = options.signal ? () => {
415
474
  const currentStatus = this.activeTasks.get(taskId)?.status;
416
475
  if (currentStatus && !TERMINAL_TASK_STATUSES.has(currentStatus)) {
476
+ if (dispatchBoundaryOwnsTerminalState) {
477
+ this.compactAbandonedObserverTaskState(taskId);
478
+ return;
479
+ }
417
480
  const reason = options.signal?.reason;
418
481
  this.updateTaskStatus(
419
482
  taskId,
@@ -440,6 +503,9 @@ class TaskExecutor {
440
503
  let governanceResult;
441
504
  let effectiveParams = params;
442
505
  let governanceAdjusted = false;
506
+ let dispatchCommitted = false;
507
+ let dispatchSettlement;
508
+ let dispatchSettlementStarted = false;
443
509
  try {
444
510
  await this.config.onActivity?.({
445
511
  type: "protocol_request",
@@ -498,14 +564,15 @@ class TaskExecutor {
498
564
  effectiveParams = adjustedParams;
499
565
  }
500
566
  }
567
+ const dispatchParams = structuredClone(effectiveParams);
501
568
  const initialMessage = {
502
569
  id: randomUUID(),
503
570
  role: "user",
504
- content: { tool: taskName, params: effectiveParams },
571
+ content: { tool: taskName, params: dispatchParams },
505
572
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
506
573
  metadata: { toolName: taskName, type: "request" }
507
574
  };
508
- const preparedCall = prepareProtocolToolCall(agent, effectiveParams, {
575
+ const preparedCall = prepareProtocolToolCall(agent, dispatchParams, {
509
576
  toolName: taskName,
510
577
  webhookUrl,
511
578
  webhookSecret: this.config.webhookSecret,
@@ -514,6 +581,7 @@ class TaskExecutor {
514
581
  wireAdcpVersion: this.config.wireAdcpVersion,
515
582
  versionEnvelope: this.config.versionEnvelope
516
583
  });
584
+ let webhookRegistrationPersisted = false;
517
585
  if (webhookUrl && this.config.onWebhookRegistration) {
518
586
  const registration = webhookRegistrationFromPreparedCall(agent, preparedCall);
519
587
  if (registration) {
@@ -523,16 +591,35 @@ class TaskExecutor {
523
591
  operationId: taskId,
524
592
  ...registration
525
593
  });
594
+ webhookRegistrationPersisted = true;
526
595
  }
527
596
  }
528
597
  throwIfAborted(options.signal);
529
- let dispatchCommitted = false;
530
598
  if (beforeProtocolDispatch) {
531
599
  let decision;
600
+ dispatchBoundaryOwnsTerminalState = true;
532
601
  try {
533
- decision = await beforeProtocolDispatch(effectiveParams, { governanceAdjusted });
602
+ if (this.deferredTerminalPublicationTaskIds.size + this.settlementCapacityReservations.size >= COMPACTED_TASK_STATE_LIMIT) {
603
+ throw new Error("The durable task-settlement capacity is exhausted; no mutation claim was attempted.");
604
+ }
605
+ this.settlementCapacityReservations.add(taskId);
606
+ if (webhookRegistrationPersisted) {
607
+ await this.config.onDurableSettlementRequired?.(taskId);
608
+ throwIfAborted(options.signal);
609
+ }
610
+ decision = await beforeProtocolDispatch(dispatchParams, {
611
+ governanceAdjusted,
612
+ publishSettledTaskStatus: (status, data, error) => this.publishSettledTaskStatus(taskId, status, data, error),
613
+ registerExternalTaskSettlement: (handler) => this.registerExternalTaskSettlement(taskId, handler)
614
+ });
615
+ if (decision.action === "dispatch_committed") {
616
+ this.deferredTerminalPublicationTaskIds.add(taskId);
617
+ this.retainExternalTaskSettlementState(taskId);
618
+ }
534
619
  } catch (error) {
535
620
  throw new BeforeProtocolDispatchHookError(error);
621
+ } finally {
622
+ this.settlementCapacityReservations.delete(taskId);
536
623
  }
537
624
  if (decision.action === "return") {
538
625
  const earlyResult = decision.result;
@@ -542,7 +629,10 @@ class TaskExecutor {
542
629
  return attachMatch(earlyResult);
543
630
  }
544
631
  dispatchCommitted = true;
632
+ dispatchSettlement = decision;
545
633
  }
634
+ throwIfAborted(options.signal);
635
+ const postDispatchOptions = dispatchCommitted ? { ...options, signal: void 0 } : options;
546
636
  const callOptions = {
547
637
  debugLogs,
548
638
  webhookUrl,
@@ -553,12 +643,9 @@ class TaskExecutor {
553
643
  ...this.config.wireAdcpVersion !== void 0 && { wireAdcpVersion: this.config.wireAdcpVersion },
554
644
  ...this.config.versionEnvelope !== void 0 && { versionEnvelope: this.config.versionEnvelope },
555
645
  transport: options.transport ?? this.config.transport,
556
- // Once a durable continuation claim commits, the seller call itself
557
- // must cross the network boundary even if the caller's deadline fired
558
- // while the atomic store operation was pending. The outer deadline
559
- // still rejects promptly and post-call handling marks the outcome
560
- // ambiguous for reconciliation.
561
- signal: dispatchCommitted ? void 0 : options.signal,
646
+ // Once dispatch begins for a live committed claim, post-call handling
647
+ // owns durable settlement even if the caller's deadline fires later.
648
+ signal: postDispatchOptions.signal,
562
649
  onTransportActivity: this.config.onTransportActivity,
563
650
  transportActivityContext: {
564
651
  operationId: taskId,
@@ -568,10 +655,10 @@ class TaskExecutor {
568
655
  }
569
656
  };
570
657
  const response = await withPreparedProtocolToolCall(
571
- { agent, toolName: taskName, args: effectiveParams, preparedCall },
572
- () => ProtocolClient.callTool(agent, taskName, effectiveParams, callOptions)
658
+ { agent, toolName: taskName, args: dispatchParams, preparedCall },
659
+ () => ProtocolClient.callTool(agent, taskName, dispatchParams, callOptions)
573
660
  );
574
- throwIfAborted(options.signal);
661
+ throwIfAborted(postDispatchOptions.signal);
575
662
  const respStatus = this.responseParser.getStatus(response);
576
663
  await this.config.onActivity?.({
577
664
  type: "protocol_response",
@@ -584,7 +671,7 @@ class TaskExecutor {
584
671
  payload: response,
585
672
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
586
673
  });
587
- throwIfAborted(options.signal);
674
+ throwIfAborted(postDispatchOptions.signal);
588
675
  const responseMessage = {
589
676
  id: randomUUID(),
590
677
  role: "agent",
@@ -593,22 +680,33 @@ class TaskExecutor {
593
680
  metadata: { toolName: taskName, type: "response" }
594
681
  };
595
682
  const messages = [initialMessage, responseMessage];
596
- const result = await this.handleAsyncResponse(
683
+ let result = await this.handleAsyncResponse(
597
684
  agent,
598
685
  taskId,
599
686
  taskName,
600
- effectiveParams,
687
+ dispatchParams,
601
688
  response,
602
689
  messages,
603
690
  inputHandler,
604
- options,
691
+ postDispatchOptions,
605
692
  debugLogs,
606
- startTime
693
+ startTime,
694
+ dispatchCommitted
607
695
  );
608
- throwIfAborted(options.signal);
696
+ throwIfAborted(postDispatchOptions.signal);
609
697
  if (governanceResult) {
610
698
  result.governance = governanceResult;
611
699
  }
700
+ if (dispatchSettlement?.onResult) {
701
+ dispatchSettlementStarted = true;
702
+ try {
703
+ result = await dispatchSettlement.onResult(attachMatch(result));
704
+ } catch (error) {
705
+ this.closeExternalTaskSettlement(taskId, error);
706
+ this.updateTaskStatus(taskId, "failed", void 0, error instanceof Error ? error.message : String(error));
707
+ throw new AfterProtocolDispatchHookError(error);
708
+ }
709
+ }
612
710
  if (governanceCheckId && this.governanceMiddleware) {
613
711
  const govCtx = governanceResult?.governanceContext;
614
712
  if (result.status === "completed" && govCtx) {
@@ -628,10 +726,10 @@ class TaskExecutor {
628
726
  void 0,
629
727
  debugLogs,
630
728
  govCtx,
631
- options.signal,
729
+ postDispatchOptions.signal,
632
730
  outcomeIdempotencyKey
633
731
  );
634
- throwIfAborted(options.signal);
732
+ throwIfAborted(postDispatchOptions.signal);
635
733
  if (!result.governanceOutcome) {
636
734
  result.governanceOutcomeError = "Outcome reporting to governance agent failed";
637
735
  }
@@ -649,10 +747,10 @@ class TaskExecutor {
649
747
  { message: result.error },
650
748
  debugLogs,
651
749
  govCtx,
652
- options.signal,
750
+ postDispatchOptions.signal,
653
751
  outcomeIdempotencyKey
654
752
  );
655
- throwIfAborted(options.signal);
753
+ throwIfAborted(postDispatchOptions.signal);
656
754
  if (!result.governanceOutcome) {
657
755
  result.governanceOutcomeError = "Outcome reporting to governance agent failed";
658
756
  }
@@ -660,6 +758,12 @@ class TaskExecutor {
660
758
  result.governance = { ...result.governance ?? {}, checkId: governanceCheckId };
661
759
  }
662
760
  }
761
+ if (dispatchCommitted) {
762
+ result = this.attachSettledContinuationTaskStatus(taskId, result);
763
+ if (TERMINAL_TASK_STATUSES.has(result.status)) {
764
+ this.publishSettledTaskStatus(taskId, result.status, result.data, result.error);
765
+ }
766
+ }
663
767
  if (["completed", "failed", "rejected", "canceled", "governance-denied", "aborted"].includes(result.status) && this.activeTasks.get(taskId)?.status !== result.status) {
664
768
  this.updateTaskStatus(taskId, result.status, result.data, result.error);
665
769
  }
@@ -669,6 +773,22 @@ class TaskExecutor {
669
773
  this.updateTaskStatus(taskId, "failed", void 0, error.message);
670
774
  throw error;
671
775
  }
776
+ if (error instanceof AfterProtocolDispatchHookError) throw error;
777
+ if (dispatchCommitted && dispatchSettlement?.onError && !dispatchSettlementStarted) {
778
+ try {
779
+ await dispatchSettlement.onError(error);
780
+ } catch (settlementError) {
781
+ this.closeExternalTaskSettlement(taskId, settlementError);
782
+ this.updateTaskStatus(
783
+ taskId,
784
+ "failed",
785
+ void 0,
786
+ settlementError instanceof Error ? settlementError.message : String(settlementError)
787
+ );
788
+ throw new AfterProtocolDispatchHookError(settlementError);
789
+ }
790
+ this.closeExternalTaskSettlement(taskId, error);
791
+ }
672
792
  if (isAbortOrTimeoutError(error)) {
673
793
  if (idempotencyKey && error && typeof error === "object") {
674
794
  error.idempotency_key = idempotencyKey;
@@ -719,12 +839,12 @@ class TaskExecutor {
719
839
  debug_logs: debugLogs
720
840
  };
721
841
  }
722
- async handleAsyncResponse(agent, taskId, taskName, params, response, messages, inputHandler, options = {}, debugLogs = [], startTime = Date.now()) {
842
+ async handleAsyncResponse(agent, taskId, taskName, params, response, messages, inputHandler, options = {}, debugLogs = [], startTime = Date.now(), deferTerminalTaskStatus = false) {
723
843
  const status = this.responseParser.getStatus(response);
724
844
  switch (status) {
725
845
  case ADCP_STATUS.COMPLETED:
726
846
  const completedData = this.extractResponseData(response, debugLogs, taskName);
727
- this.updateTaskStatus(taskId, "completed", completedData);
847
+ if (!deferTerminalTaskStatus) this.updateTaskStatus(taskId, "completed", completedData);
728
848
  const operationSuccess = this.isOperationSuccess(completedData, taskName);
729
849
  const validationResult = this.validateResponseSchema(completedData, taskName, debugLogs);
730
850
  const finalSuccess = operationSuccess && validationResult.valid;
@@ -780,7 +900,17 @@ class TaskExecutor {
780
900
  startTime
781
901
  );
782
902
  case ADCP_STATUS.SUBMITTED:
783
- return this.setupSubmittedTask(agent, taskId, taskName, response, messages, options, debugLogs, startTime);
903
+ return this.setupSubmittedTask(
904
+ agent,
905
+ taskId,
906
+ taskName,
907
+ response,
908
+ messages,
909
+ options,
910
+ debugLogs,
911
+ startTime,
912
+ deferTerminalTaskStatus
913
+ );
784
914
  case ADCP_STATUS.INPUT_REQUIRED:
785
915
  return this.handleInputRequired(
786
916
  agent,
@@ -792,7 +922,8 @@ class TaskExecutor {
792
922
  inputHandler,
793
923
  options,
794
924
  debugLogs,
795
- startTime
925
+ startTime,
926
+ deferTerminalTaskStatus
796
927
  );
797
928
  case ADCP_STATUS.FAILED:
798
929
  case ADCP_STATUS.REJECTED:
@@ -994,7 +1125,7 @@ class TaskExecutor {
994
1125
  /**
995
1126
  * Set up submitted task with webhook
996
1127
  */
997
- async setupSubmittedTask(agent, taskId, taskName, response, messages, options = {}, debugLogs = [], startTime = Date.now()) {
1128
+ async setupSubmittedTask(agent, taskId, taskName, response, messages, options = {}, debugLogs = [], startTime = Date.now(), deferTerminalTaskStatus = false) {
998
1129
  const partialData = this.extractResponseData(response, debugLogs, taskName);
999
1130
  let webhookUrl = response.webhookUrl;
1000
1131
  if (!webhookUrl && this.config.webhookManager) {
@@ -1018,14 +1149,16 @@ class TaskExecutor {
1018
1149
  webhookUrl,
1019
1150
  track: async (transport) => {
1020
1151
  const task = await this.getTaskStatus(agent, serverTaskId, transport ?? pollingTransport);
1021
- if (["completed", "failed", "rejected", "canceled"].includes(task.status)) {
1152
+ if (!deferTerminalTaskStatus && ["completed", "failed", "rejected", "canceled"].includes(task.status)) {
1022
1153
  this.updateTaskStatus(taskId, task.status, task.result, task.error);
1023
1154
  }
1024
1155
  return task;
1025
1156
  },
1026
1157
  waitForCompletion: async (pollInterval = 6e4, signal) => {
1027
1158
  const completed = await this.pollTaskCompletion(agent, serverTaskId, pollInterval, pollingTransport, signal);
1028
- this.updateTaskStatus(taskId, completed.status, completed.data, completed.error);
1159
+ if (!deferTerminalTaskStatus || !TERMINAL_TASK_STATUSES.has(completed.status)) {
1160
+ this.updateTaskStatus(taskId, completed.status, completed.data, completed.error);
1161
+ }
1029
1162
  return completed;
1030
1163
  }
1031
1164
  };
@@ -1049,6 +1182,193 @@ class TaskExecutor {
1049
1182
  debug_logs: debugLogs
1050
1183
  };
1051
1184
  }
1185
+ /**
1186
+ * A committed compatibility hook may replace resumable continuations with
1187
+ * wrappers that validate and durably persist their terminal result. Attach
1188
+ * local task publication only after those final wrappers return.
1189
+ */
1190
+ attachSettledContinuationTaskStatus(taskId, result) {
1191
+ const publish = (status, data, error) => {
1192
+ this.publishSettledTaskStatus(taskId, status, data, error);
1193
+ };
1194
+ if (result.submitted) {
1195
+ const submitted = result.submitted;
1196
+ result.submitted = {
1197
+ ...submitted,
1198
+ track: async (transport) => {
1199
+ const task = await submitted.track(transport);
1200
+ publish(task.status, task.result, task.error);
1201
+ return task;
1202
+ },
1203
+ waitForCompletion: async (pollInterval, signal) => {
1204
+ const completion = this.attachSettledContinuationTaskStatus(
1205
+ taskId,
1206
+ await submitted.waitForCompletion(pollInterval, signal)
1207
+ );
1208
+ publish(completion.status, completion.data, completion.error);
1209
+ return completion;
1210
+ }
1211
+ };
1212
+ }
1213
+ if (result.deferred) {
1214
+ const deferred = result.deferred;
1215
+ result.deferred = {
1216
+ ...deferred,
1217
+ resume: async (input) => {
1218
+ const completion = this.attachSettledContinuationTaskStatus(taskId, await deferred.resume(input));
1219
+ publish(completion.status, completion.data, completion.error);
1220
+ return completion;
1221
+ }
1222
+ };
1223
+ }
1224
+ return result;
1225
+ }
1226
+ publishSettledTaskStatus(taskId, status, data, error, fromExternalObservation = false) {
1227
+ if (!TERMINAL_TASK_STATUSES.has(status)) return;
1228
+ const task = this.activeTasks.get(taskId);
1229
+ if (!task) return;
1230
+ if (!fromExternalObservation) {
1231
+ this.closeExternalTaskSettlement(
1232
+ taskId,
1233
+ new Error("The task settled through its direct response; a racing pushed result was not accepted.")
1234
+ );
1235
+ }
1236
+ if (task.status !== status) this.updateTaskStatus(taskId, status, data, error);
1237
+ }
1238
+ rejectPendingExternalTaskObservations(taskId, error) {
1239
+ const pending = this.pendingExternalTaskObservations.get(taskId);
1240
+ this.pendingExternalTaskObservations.delete(taskId);
1241
+ for (const waiter of pending ?? []) waiter.reject(error);
1242
+ }
1243
+ closeExternalTaskSettlement(taskId, error) {
1244
+ this.externalTaskSettlementHandlers.delete(taskId);
1245
+ this.externalTaskSettlementInFlight.delete(taskId);
1246
+ this.closedExternalTaskSettlementTaskIds.add(taskId);
1247
+ this.rejectPendingExternalTaskObservations(taskId, error);
1248
+ }
1249
+ clearExternalTaskSettlementState(taskId) {
1250
+ this.deferredTerminalPublicationTaskIds.delete(taskId);
1251
+ this.closedExternalTaskSettlementTaskIds.delete(taskId);
1252
+ this.externalTaskSettlementHandlers.delete(taskId);
1253
+ this.externalTaskSettlementInFlight.delete(taskId);
1254
+ this.settledExternalTaskObservationKeys.delete(taskId);
1255
+ this.externalTaskSettlementExpiry.delete(taskId);
1256
+ this.rejectPendingExternalTaskObservations(
1257
+ taskId,
1258
+ new Error("The durable push-settlement retention window expired before the observation was accepted.")
1259
+ );
1260
+ }
1261
+ retainExternalTaskSettlementState(taskId) {
1262
+ const retentionMs = this.config.externalTaskSettlementRetentionMs ?? DEFAULT_EXTERNAL_TASK_SETTLEMENT_RETENTION_MS;
1263
+ const expiry = Date.now() + retentionMs;
1264
+ if ((this.externalTaskSettlementExpiry.get(taskId) ?? 0) >= expiry) return;
1265
+ this.externalTaskSettlementExpiry.set(taskId, expiry);
1266
+ const expire = () => {
1267
+ if (this.externalTaskSettlementExpiry.get(taskId) !== expiry) return;
1268
+ const remaining = expiry - Date.now();
1269
+ if (remaining > 0) {
1270
+ const timer2 = setTimeout(expire, Math.min(remaining, MAX_TIMER_DELAY_MS));
1271
+ timer2.unref?.();
1272
+ return;
1273
+ }
1274
+ this.clearExternalTaskSettlementState(taskId);
1275
+ };
1276
+ const timer = setTimeout(expire, Math.min(retentionMs, MAX_TIMER_DELAY_MS));
1277
+ timer.unref?.();
1278
+ }
1279
+ /** Release short-lived task inspection state without dropping a live durable push fence. */
1280
+ cleanupTerminalTaskInspectionState(taskId) {
1281
+ this.activeTasks.delete(taskId);
1282
+ this.compactedTaskIds.delete(taskId);
1283
+ if (this.deferredTerminalPublicationTaskIds.has(taskId) || this.closedExternalTaskSettlementTaskIds.has(taskId) || this.externalTaskSettlementHandlers.has(taskId)) {
1284
+ if (this.externalTaskSettlementExpiry.has(taskId)) return;
1285
+ }
1286
+ this.clearExternalTaskSettlementState(taskId);
1287
+ }
1288
+ registerExternalTaskSettlement(taskId, handler) {
1289
+ this.externalTaskSettlementHandlers.set(taskId, handler);
1290
+ const pending = this.pendingExternalTaskObservations.get(taskId);
1291
+ if (!pending) return;
1292
+ this.pendingExternalTaskObservations.delete(taskId);
1293
+ for (const waiter of pending) {
1294
+ void this.settleExternalTaskStatus(taskId, handler, waiter.observation).then(waiter.resolve, waiter.reject);
1295
+ }
1296
+ }
1297
+ externalTaskObservationKey(observation) {
1298
+ return createHash("sha256").update(
1299
+ canonicalize({
1300
+ status: observation.status,
1301
+ serverTaskId: observation.serverTaskId ?? null,
1302
+ taskType: observation.taskType ?? null,
1303
+ result: observation.result ?? null
1304
+ })
1305
+ ).digest("base64url");
1306
+ }
1307
+ async settleExternalTaskStatus(taskId, handler, observation) {
1308
+ const observationKey = this.externalTaskObservationKey(observation);
1309
+ const existing = this.externalTaskSettlementInFlight.get(taskId);
1310
+ if (existing) {
1311
+ if (existing.waiterCount >= MAX_PENDING_EXTERNAL_TASK_OBSERVATIONS) {
1312
+ throw new Error("Too many terminal push notifications are undergoing durable task settlement.");
1313
+ }
1314
+ existing.waiterCount += 1;
1315
+ try {
1316
+ const accepted = await existing.settlement;
1317
+ if (existing.observationKey !== observationKey) {
1318
+ throw new Error("This task already completed durable settlement; the pushed result was not accepted.");
1319
+ }
1320
+ return { ...accepted, duplicate: true };
1321
+ } finally {
1322
+ existing.waiterCount -= 1;
1323
+ }
1324
+ }
1325
+ const settlement = (async () => {
1326
+ const settled = await handler(observation);
1327
+ const settledStatus = settled.status;
1328
+ if (this.closedExternalTaskSettlementTaskIds.has(taskId)) {
1329
+ throw new Error("The task settled through its direct response; the racing pushed result was not accepted.");
1330
+ }
1331
+ if (this.settledExternalTaskObservationKeys.has(taskId)) {
1332
+ throw new Error("This task already completed durable settlement; the pushed result was not accepted.");
1333
+ }
1334
+ this.publishSettledTaskStatus(taskId, settledStatus, settled.data, settled.error, true);
1335
+ this.settledExternalTaskObservationKeys.set(taskId, {
1336
+ key: observationKey,
1337
+ status: settledStatus,
1338
+ ...settled.error !== void 0 && {
1339
+ error: settled.error.slice(0, MAX_RETAINED_EXTERNAL_TASK_ERROR_CHARS)
1340
+ }
1341
+ });
1342
+ this.closeExternalTaskSettlement(
1343
+ taskId,
1344
+ new Error("The task already completed durable external settlement; a later pushed result was not accepted.")
1345
+ );
1346
+ return {
1347
+ settled: true,
1348
+ result: settled.data,
1349
+ status: settled.status,
1350
+ ...settled.error !== void 0 && { error: settled.error }
1351
+ };
1352
+ })();
1353
+ const inFlight = { observationKey, settlement, waiterCount: 1 };
1354
+ this.externalTaskSettlementInFlight.set(taskId, inFlight);
1355
+ try {
1356
+ return await settlement;
1357
+ } finally {
1358
+ if (this.externalTaskSettlementInFlight.get(taskId) === inFlight) {
1359
+ this.externalTaskSettlementInFlight.delete(taskId);
1360
+ }
1361
+ }
1362
+ }
1363
+ /** Release abandoned caller data without publishing a premature terminal state. */
1364
+ compactAbandonedObserverTaskState(taskId) {
1365
+ const task = this.activeTasks.get(taskId);
1366
+ if (!task) return;
1367
+ task.params = void 0;
1368
+ task.messages = [];
1369
+ task.options = {};
1370
+ delete task.pendingInput;
1371
+ }
1052
1372
  /**
1053
1373
  * A working/submitted response carries everything needed to poll externally.
1054
1374
  * Keeping the original request in activeTasks after dispatch would pin
@@ -1067,10 +1387,14 @@ class TaskExecutor {
1067
1387
  this.compactedTaskIds.delete(taskId);
1068
1388
  this.compactedTaskIds.set(taskId, true);
1069
1389
  while (this.compactedTaskIds.size > COMPACTED_TASK_STATE_LIMIT) {
1070
- const oldest = this.compactedTaskIds.keys().next().value;
1390
+ const oldest = [...this.compactedTaskIds.keys()].find(
1391
+ (candidate) => !this.deferredTerminalPublicationTaskIds.has(candidate) && !this.externalTaskSettlementHandlers.has(candidate) && !this.externalTaskSettlementInFlight.has(candidate)
1392
+ );
1071
1393
  if (oldest === void 0) break;
1072
1394
  this.compactedTaskIds.delete(oldest);
1073
1395
  this.activeTasks.delete(oldest);
1396
+ this.closeExternalTaskSettlement(oldest, new Error("The compacted task was evicted before push settlement."));
1397
+ this.closedExternalTaskSettlementTaskIds.delete(oldest);
1074
1398
  }
1075
1399
  }
1076
1400
  /**
@@ -1081,7 +1405,7 @@ class TaskExecutor {
1081
1405
  * This is common for HITL (human-in-the-loop) workflows where the agent has already processed
1082
1406
  * the request and is just signaling that async approval may be needed.
1083
1407
  */
1084
- async handleInputRequired(agent, taskId, taskName, params, response, messages, inputHandler, options = {}, debugLogs = [], startTime = Date.now()) {
1408
+ async handleInputRequired(agent, taskId, taskName, params, response, messages, inputHandler, options = {}, debugLogs = [], startTime = Date.now(), deferTerminalTaskStatus = false) {
1085
1409
  const inputRequest = this.responseParser.parseInputRequest(response);
1086
1410
  if (!inputHandler) {
1087
1411
  const partialData = this.extractResponseData(response, debugLogs, taskName);
@@ -1159,7 +1483,7 @@ class TaskExecutor {
1159
1483
  const deferred = {
1160
1484
  token,
1161
1485
  question: inputRequest.question,
1162
- resume: (input) => this.resumeDeferredTask(token, input)
1486
+ resume: (input) => this.resumeDeferredTask(token, input, !deferTerminalTaskStatus)
1163
1487
  };
1164
1488
  return {
1165
1489
  success: true,
@@ -1192,7 +1516,8 @@ class TaskExecutor {
1192
1516
  // Pass handler for multi-round clarification
1193
1517
  options,
1194
1518
  debugLogs,
1195
- startTime
1519
+ startTime,
1520
+ deferTerminalTaskStatus
1196
1521
  );
1197
1522
  }
1198
1523
  /**
@@ -1411,7 +1736,7 @@ class TaskExecutor {
1411
1736
  /**
1412
1737
  * Resume a deferred task (client deferral)
1413
1738
  */
1414
- async resumeDeferredTask(token, input) {
1739
+ async resumeDeferredTask(token, input, publishTerminalTaskStatus = true) {
1415
1740
  if (!this.config.deferredStorage) {
1416
1741
  throw new Error("Deferred storage not configured");
1417
1742
  }
@@ -1428,11 +1753,15 @@ class TaskExecutor {
1428
1753
  state.contextId,
1429
1754
  input,
1430
1755
  state.messages,
1431
- void 0
1756
+ void 0,
1432
1757
  // No handler for deferred tasks - input was provided by human
1758
+ {},
1759
+ [],
1760
+ Date.now(),
1761
+ !publishTerminalTaskStatus
1433
1762
  );
1434
1763
  const remainsPaused = ["input-required", "auth-required", "deferred"].includes(resumed.status);
1435
- if (["completed", "failed", "rejected", "canceled", "governance-denied", "aborted"].includes(resumed.status) && this.activeTasks.get(state.taskId)?.status !== resumed.status) {
1764
+ if (publishTerminalTaskStatus && ["completed", "failed", "rejected", "canceled", "governance-denied", "aborted"].includes(resumed.status) && this.activeTasks.get(state.taskId)?.status !== resumed.status) {
1436
1765
  this.updateTaskStatus(state.taskId, resumed.status, resumed.data, resumed.error);
1437
1766
  }
1438
1767
  if (!remainsPaused) {
@@ -1440,7 +1769,14 @@ class TaskExecutor {
1440
1769
  }
1441
1770
  return attachMatch(resumed);
1442
1771
  } catch (error) {
1443
- this.updateTaskStatus(state.taskId, "failed", void 0, error instanceof Error ? error.message : String(error));
1772
+ if (publishTerminalTaskStatus) {
1773
+ this.updateTaskStatus(
1774
+ state.taskId,
1775
+ "failed",
1776
+ void 0,
1777
+ error instanceof Error ? error.message : String(error)
1778
+ );
1779
+ }
1444
1780
  try {
1445
1781
  await this.config.deferredStorage.delete(token);
1446
1782
  } catch {
@@ -1451,7 +1787,7 @@ class TaskExecutor {
1451
1787
  /**
1452
1788
  * Continue a task after receiving input
1453
1789
  */
1454
- async continueTaskWithInput(agent, taskId, taskName, params, contextId, input, messages, inputHandler, options = {}, debugLogs = [], startTime = Date.now()) {
1790
+ async continueTaskWithInput(agent, taskId, taskName, params, contextId, input, messages, inputHandler, options = {}, debugLogs = [], startTime = Date.now(), deferTerminalTaskStatus = false) {
1455
1791
  const inputMessage = {
1456
1792
  id: randomUUID(),
1457
1793
  role: "user",
@@ -1501,7 +1837,8 @@ class TaskExecutor {
1501
1837
  inputHandler,
1502
1838
  options,
1503
1839
  debugLogs,
1504
- startTime
1840
+ startTime,
1841
+ deferTerminalTaskStatus
1505
1842
  );
1506
1843
  }
1507
1844
  /**
@@ -1786,8 +2123,7 @@ class TaskExecutor {
1786
2123
  task.options = {};
1787
2124
  delete task.pendingInput;
1788
2125
  setTimeout(() => {
1789
- this.activeTasks.delete(taskId);
1790
- this.compactedTaskIds.delete(taskId);
2126
+ this.cleanupTerminalTaskInspectionState(taskId);
1791
2127
  }, 3e4).unref();
1792
2128
  }
1793
2129
  }
@@ -1800,6 +2136,7 @@ class TaskExecutor {
1800
2136
  }
1801
2137
  }
1802
2138
  export {
2139
+ AfterProtocolDispatchHookError,
1803
2140
  BeforeProtocolDispatchHookError,
1804
2141
  DeferredTaskError,
1805
2142
  InputRequiredError,