@adcp/sdk 6.19.1 → 7.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 (69) hide show
  1. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  2. package/dist/lib/core/ConversationTypes.d.ts +33 -2
  3. package/dist/lib/core/ConversationTypes.d.ts.map +1 -1
  4. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  5. package/dist/lib/core/SingleAgentClient.js +21 -8
  6. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  7. package/dist/lib/index.d.ts +1 -1
  8. package/dist/lib/index.d.ts.map +1 -1
  9. package/dist/lib/index.js.map +1 -1
  10. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  11. package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
  12. package/dist/lib/server/create-adcp-server.js +8 -7
  13. package/dist/lib/server/create-adcp-server.js.map +1 -1
  14. package/dist/lib/server/decisioning/runtime/from-platform.js +54 -52
  15. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  16. package/dist/lib/server/envelope-allowlist.d.ts.map +1 -1
  17. package/dist/lib/server/envelope-allowlist.js +17 -0
  18. package/dist/lib/server/envelope-allowlist.js.map +1 -1
  19. package/dist/lib/server/idempotency/store.d.ts +8 -0
  20. package/dist/lib/server/idempotency/store.d.ts.map +1 -1
  21. package/dist/lib/server/idempotency/store.js +28 -4
  22. package/dist/lib/server/idempotency/store.js.map +1 -1
  23. package/dist/lib/server/wire-spec-fields.generated.js +1 -1
  24. package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
  25. package/dist/lib/testing/compliance/comply.js +51 -15
  26. package/dist/lib/testing/compliance/comply.js.map +1 -1
  27. package/dist/lib/testing/compliance/types.d.ts +24 -0
  28. package/dist/lib/testing/compliance/types.d.ts.map +1 -1
  29. package/dist/lib/testing/storyboard/loader.d.ts.map +1 -1
  30. package/dist/lib/testing/storyboard/loader.js +17 -0
  31. package/dist/lib/testing/storyboard/loader.js.map +1 -1
  32. package/dist/lib/testing/storyboard/parallel-dispatch.d.ts +110 -0
  33. package/dist/lib/testing/storyboard/parallel-dispatch.d.ts.map +1 -0
  34. package/dist/lib/testing/storyboard/parallel-dispatch.js +221 -0
  35. package/dist/lib/testing/storyboard/parallel-dispatch.js.map +1 -0
  36. package/dist/lib/testing/storyboard/request-builder.js +1 -1
  37. package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
  38. package/dist/lib/testing/storyboard/runner.d.ts +3 -1
  39. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  40. package/dist/lib/testing/storyboard/runner.js +318 -44
  41. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  42. package/dist/lib/testing/storyboard/task-map.d.ts +1 -0
  43. package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
  44. package/dist/lib/testing/storyboard/task-map.js +7 -1
  45. package/dist/lib/testing/storyboard/task-map.js.map +1 -1
  46. package/dist/lib/testing/storyboard/types.d.ts +98 -2
  47. package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
  48. package/dist/lib/testing/storyboard/types.js +1 -0
  49. package/dist/lib/testing/storyboard/types.js.map +1 -1
  50. package/dist/lib/testing/storyboard/validations.d.ts +38 -0
  51. package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
  52. package/dist/lib/testing/storyboard/validations.js +143 -0
  53. package/dist/lib/testing/storyboard/validations.js.map +1 -1
  54. package/dist/lib/testing/types.d.ts +2 -0
  55. package/dist/lib/testing/types.d.ts.map +1 -1
  56. package/dist/lib/utils/error-extraction.d.ts +2 -1
  57. package/dist/lib/utils/error-extraction.d.ts.map +1 -1
  58. package/dist/lib/utils/error-extraction.js +28 -0
  59. package/dist/lib/utils/error-extraction.js.map +1 -1
  60. package/dist/lib/utils/request-normalizer.d.ts +1 -0
  61. package/dist/lib/utils/request-normalizer.d.ts.map +1 -1
  62. package/dist/lib/utils/request-normalizer.js +34 -11
  63. package/dist/lib/utils/request-normalizer.js.map +1 -1
  64. package/dist/lib/version.d.ts +3 -3
  65. package/dist/lib/version.d.ts.map +1 -1
  66. package/dist/lib/version.js +3 -3
  67. package/dist/lib/version.js.map +1 -1
  68. package/package.json +1 -1
  69. package/skills/build-decisioning-platform/advanced/REFERENCE.md +2 -0
@@ -29,6 +29,7 @@ const rejection_hints_1 = require("./rejection-hints");
29
29
  const shape_drift_hints_1 = require("./shape-drift-hints");
30
30
  const strict_validation_hints_1 = require("./strict-validation-hints");
31
31
  const validations_1 = require("./validations");
32
+ const parallel_dispatch_1 = require("./parallel-dispatch");
32
33
  const path_1 = require("./path");
33
34
  const redact_secrets_1 = require("../../utils/redact-secrets");
34
35
  const test_controller_1 = require("../test-controller");
@@ -242,6 +243,7 @@ function applyBranchSetGrading(phases, phaseResults, branchSetByPhaseId, contrib
242
243
  step.skip_reason = 'peer_branch_taken';
243
244
  step.skip = { reason: 'peer_branch_taken', detail };
244
245
  delete step.error;
246
+ delete step.adcp_error;
245
247
  skippedDelta++;
246
248
  regraded = true;
247
249
  }
@@ -698,6 +700,7 @@ const REQUIREMENT_TO_SKIP_REASON = {
698
700
  controller: 'missing_test_controller',
699
701
  seeded_state: 'requirement_unmet',
700
702
  real_wire: 'requirement_unmet',
703
+ webhook_receiver: 'requirement_unmet',
701
704
  };
702
705
  /**
703
706
  * Build a minimal StoryboardResult for a storyboard skipped because a
@@ -770,6 +773,10 @@ function buildRequirementUnmetResult(agentUrls, storyboard, requirement, detail)
770
773
  * - `real_wire` — always available; the runner is hitting a real wire
771
774
  * by definition. The tag is a no-op gate today, reserved for a
772
775
  * future `--mock-only` mode.
776
+ * - `webhook_receiver` — operator supplied `options.webhook_receiver`.
777
+ * Autodetected from step `sample_request` token presence (see
778
+ * `detectImplicitRequires`); authors don't write this tag manually.
779
+ * Spec: adcp-client#1678.
773
780
  */
774
781
  function checkRequires(requires, options) {
775
782
  for (const requirement of requires) {
@@ -799,10 +806,72 @@ function checkRequires(requires, options) {
799
806
  case 'real_wire':
800
807
  // Always available — reserved for a future --mock-only mode.
801
808
  break;
809
+ case 'webhook_receiver': {
810
+ if (options.webhook_receiver === undefined) {
811
+ return {
812
+ requirement,
813
+ detail: 'Storyboard references `{{runner.webhook_url:…}}` or `{{runner.webhook_base}}` but no ' +
814
+ 'webhook receiver is configured. Pass `webhook_receiver` in StoryboardRunOptions ' +
815
+ '(or `--webhook-receiver` on the CLI) to host a receiver, or omit this storyboard. ' +
816
+ 'Required by the webhook-emission universal: ' +
817
+ 'compliance/{version}/universal/webhook-emission.yaml grades not_applicable when ' +
818
+ 'no receiver is configured (prerequisites section).',
819
+ };
820
+ }
821
+ break;
822
+ }
802
823
  }
803
824
  }
804
825
  return null;
805
826
  }
827
+ /**
828
+ * Webhook-token regex used to autodetect the implicit `webhook_receiver`
829
+ * requirement. Matches `{{runner.webhook_url:<step_id>}}` and the bare
830
+ * `{{runner.webhook_base}}`. Same shape as the `MUSTACHE_TOKEN_RE` in
831
+ * context.ts but scoped to the two webhook-bearing tokens — we only care
832
+ * whether the storyboard needs a receiver, not what substitution it
833
+ * would produce. Single `[^{}]+` body avoids the polynomial-redos
834
+ * backtrack pattern flagged by CodeQL alert #49.
835
+ */
836
+ const WEBHOOK_TOKEN_RE = /\{\{runner\.(?:webhook_url:[A-Za-z0-9_]+|webhook_base)\}\}/;
837
+ /**
838
+ * Recursively scan a JSON-like value for any webhook receiver token. The
839
+ * scan only touches strings; objects and arrays are walked structurally
840
+ * so deeply-nested `push_notification_config.url` entries (or any other
841
+ * authoring pattern) are discovered without a hard-coded field list.
842
+ */
843
+ function valueContainsWebhookToken(value) {
844
+ if (typeof value === 'string') {
845
+ return WEBHOOK_TOKEN_RE.test(value);
846
+ }
847
+ if (Array.isArray(value)) {
848
+ return value.some(valueContainsWebhookToken);
849
+ }
850
+ if (value !== null && typeof value === 'object') {
851
+ return Object.values(value).some(valueContainsWebhookToken);
852
+ }
853
+ return false;
854
+ }
855
+ /**
856
+ * Return the implicit requirements a storyboard needs based on its
857
+ * structure (not its declared `requires:` list). Today: only
858
+ * `'webhook_receiver'`, autodetected from `{{runner.webhook_url:…}}` /
859
+ * `{{runner.webhook_base}}` token presence inside any step's
860
+ * `sample_request`. Token presence is the authoring contract — a
861
+ * storyboard that names the runner's webhook receiver cannot run
862
+ * without one — so authors don't need to remember to add
863
+ * `requires: [webhook_receiver]` separately. Spec: adcp-client#1678.
864
+ */
865
+ function detectImplicitRequires(storyboard) {
866
+ for (const phase of storyboard.phases) {
867
+ for (const step of phase.steps) {
868
+ if (step.sample_request && valueContainsWebhookToken(step.sample_request)) {
869
+ return ['webhook_receiver'];
870
+ }
871
+ }
872
+ }
873
+ return [];
874
+ }
806
875
  /**
807
876
  * Build a hard-failure StoryboardResult for when agent capability
808
877
  * discovery (`get_agent_info` / MCP `tools/list`) failed. Surfacing
@@ -1030,8 +1099,20 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1030
1099
  // Spec: adcp-client#1626. The default (no `requires` field) is
1031
1100
  // `[real_wire]`, which is always available — untagged storyboards
1032
1101
  // run unchanged.
1033
- if (storyboard.requires?.length) {
1034
- const unmet = checkRequires(storyboard.requires, options);
1102
+ //
1103
+ // Implicit requirements (adcp-client#1678) are unioned with the
1104
+ // declared list: today this means `'webhook_receiver'` is added when
1105
+ // any step's `sample_request` references `{{runner.webhook_url:…}}`
1106
+ // or `{{runner.webhook_base}}`. Authors do not need to retag those
1107
+ // storyboards — the token presence is the declaration. Without the
1108
+ // implicit gate, storyboards that name the receiver but run without
1109
+ // one would silently ship literal mustache tokens on the wire and
1110
+ // get rejected by 3.0-strict sellers as `INVALID_REQUEST: relative URL`.
1111
+ const declared = storyboard.requires ?? [];
1112
+ const implicit = detectImplicitRequires(storyboard);
1113
+ const allRequires = [...declared, ...implicit.filter(r => !declared.includes(r))];
1114
+ if (allRequires.length) {
1115
+ const unmet = checkRequires(allRequires, options);
1035
1116
  if (unmet) {
1036
1117
  if (!options._client)
1037
1118
  await (0, protocols_1.closeConnections)(options.protocol);
@@ -2426,7 +2507,9 @@ client, step, phaseId, context, allSteps, options, state) {
2426
2507
  // match the options. Enforcing this here (after builder + sample_request)
2427
2508
  // prevents session-key divergence across create/get/update/delete steps
2428
2509
  // when individual builders or sample_request YAML omit brand.
2429
- request = applyBrandInvariant(request, options, effectiveStep.task);
2510
+ // `step.omit_account` suppresses account synthesis for schema_validation
2511
+ // steps that deliberately test the seller's missing-account rejection path.
2512
+ request = applyBrandInvariant(request, options, effectiveStep.task, { omit_account: step.omit_account });
2430
2513
  // Per-run sandbox-bypass hint (#841). When the operator passes
2431
2514
  // `--no-sandbox` (or sets `disable_sandbox: true` programmatically), the
2432
2515
  // runner stamps `ext.adcp.disable_sandbox: true` on every outgoing
@@ -2507,16 +2590,25 @@ client, step, phaseId, context, allSteps, options, state) {
2507
2590
  // and the server never sees a missing-key request. Paired flags so the two
2508
2591
  // layers agree; see `applyIdempotencyInvariant` for the runner-level skip.
2509
2592
  const testsMissingIdempotencyKey = step.omit_idempotency_key === true && (0, idempotency_1.isMutatingTask)(effectiveStep.task);
2510
- // Defense-in-depth for the missing-key vector: when a mutating step sets
2511
- // `omit_idempotency_key: true` and `step.auth` is unset, route via
2512
- // `rawMcpProbe` anyway so no SDK-layer normalization can slip a key onto the
2513
- // wire. The SDK's `skipIdempotencyAutoInject` plumbing already honors this
2514
- // flag, but the raw-HTTP path removes the escape hatch entirely. A2A and
2515
- // oauth stay on the SDK path their dispatch can't be replicated here
2516
- // (A2A uses a different envelope; oauth needs refresh semantics).
2593
+ // Analogous to `testsMissingIdempotencyKey`: when a step sets
2594
+ // `omit_account: true` the runner has already suppressed account synthesis
2595
+ // in `applyBrandInvariant` (above ordering is load-bearing: this must
2596
+ // come after `applyBrandInvariant` so the comment "above" stays accurate
2597
+ // if either block is reordered). Track the flag here so the raw-probe
2598
+ // defense-in-depth path is also triggered and no SDK-layer normalization
2599
+ // can silently re-inject an account before the wire call.
2600
+ const testsMissingAccount = step.omit_account === true && effectiveStep.task === 'create_media_buy';
2601
+ // Defense-in-depth for the missing-field vectors: when a step sets
2602
+ // `omit_idempotency_key: true` or `omit_account: true` and `step.auth` is
2603
+ // unset, route via `rawMcpProbe` anyway so no SDK-layer normalization can
2604
+ // slip the missing field onto the wire. The SDK's `skipIdempotencyAutoInject`
2605
+ // / `skipAccountValidation` plumbing already honors these flags, but the
2606
+ // raw-HTTP path removes the escape hatch entirely. A2A and oauth stay on
2607
+ // the SDK path — their dispatch can't be replicated here (A2A uses a
2608
+ // different envelope; oauth needs refresh semantics).
2517
2609
  const rawProbeHeaders = step.auth !== undefined
2518
2610
  ? authHeadersForStep(step.auth, options)
2519
- : testsMissingIdempotencyKey && options.protocol !== 'a2a'
2611
+ : (testsMissingIdempotencyKey || testsMissingAccount) && options.protocol !== 'a2a'
2520
2612
  ? defaultAuthHeadersForRawProbe(options)
2521
2613
  : undefined;
2522
2614
  const useRawProbe = rawProbeHeaders !== undefined;
@@ -2525,6 +2617,83 @@ client, step, phaseId, context, allSteps, options, state) {
2525
2617
  let httpResult;
2526
2618
  let responseRecord;
2527
2619
  let a2aEnvelope;
2620
+ let crossResponses;
2621
+ // Parallel-dispatch fan-out: when the storyboard step declares
2622
+ // `parallel_dispatch`, the runner fires N concurrent dispatches against
2623
+ // the same agent with the same idempotency_key (default) and grades the
2624
+ // cross-response set instead of a single response. Gated on the
2625
+ // `parallel_dispatch_runner` test-kit contract — runners (or runs) without
2626
+ // it grade the step `not_applicable` so older runners don't fail on
2627
+ // newer storyboard contracts.
2628
+ if (step.parallel_dispatch && !useRawProbe) {
2629
+ const contractsInScope = new Set(options.contracts ?? []);
2630
+ if (!contractsInScope.has(parallel_dispatch_1.PARALLEL_DISPATCH_CONTRACT)) {
2631
+ const next = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
2632
+ const detail = `Test-kit contract "${parallel_dispatch_1.PARALLEL_DISPATCH_CONTRACT}" is not configured on this runner; concurrent-retry grading requires it.`;
2633
+ return {
2634
+ step_id: step.id,
2635
+ phase_id: phaseId,
2636
+ title: step.title,
2637
+ task: step.task,
2638
+ passed: true,
2639
+ skipped: true,
2640
+ skip_reason: 'not_applicable',
2641
+ skip: buildSkip('not_applicable', detail),
2642
+ duration_ms: 0,
2643
+ validations: [],
2644
+ context,
2645
+ next,
2646
+ extraction: { path: 'none' },
2647
+ };
2648
+ }
2649
+ const specError = (0, parallel_dispatch_1.validateParallelDispatchSpec)(step.parallel_dispatch);
2650
+ if (specError) {
2651
+ const next = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
2652
+ return {
2653
+ step_id: step.id,
2654
+ phase_id: phaseId,
2655
+ title: step.title,
2656
+ task: step.task,
2657
+ passed: false,
2658
+ duration_ms: 0,
2659
+ validations: [
2660
+ {
2661
+ check: 'parallel_dispatch_misconfigured',
2662
+ passed: false,
2663
+ description: specError,
2664
+ json_pointer: null,
2665
+ expected: 'a well-formed parallel_dispatch spec',
2666
+ actual: step.parallel_dispatch,
2667
+ schema_id: null,
2668
+ schema_url: null,
2669
+ },
2670
+ ],
2671
+ context,
2672
+ error: specError,
2673
+ next,
2674
+ extraction: { path: 'none' },
2675
+ };
2676
+ }
2677
+ if (step.parallel_dispatch.mode === 'distributed') {
2678
+ const next = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
2679
+ const detail = 'parallel_dispatch.mode: distributed is not implemented in @adcp/sdk; use process_local for in-process grading.';
2680
+ return {
2681
+ step_id: step.id,
2682
+ phase_id: phaseId,
2683
+ title: step.title,
2684
+ task: step.task,
2685
+ passed: true,
2686
+ skipped: true,
2687
+ skip_reason: 'not_applicable',
2688
+ skip: buildSkip('not_applicable', detail),
2689
+ duration_ms: 0,
2690
+ validations: [],
2691
+ context,
2692
+ next,
2693
+ extraction: { path: 'none' },
2694
+ };
2695
+ }
2696
+ }
2528
2697
  // Capture the ISO timestamp immediately before the step's AdCP request
2529
2698
  // dispatch. `upstream_traffic` validations use this as the default
2530
2699
  // `since_timestamp` window bound when querying the controller. Recorded
@@ -2583,40 +2752,91 @@ client, step, phaseId, context, allSteps, options, state) {
2583
2752
  // lands, key the capture off the negotiated transport instead.
2584
2753
  const captureA2a = options.protocol === 'a2a';
2585
2754
  let a2aCaptures;
2586
- const dispatch = () => (0, task_map_1.executeStoryboardTask)(client, effectiveStep.task, request, {
2587
- skipIdempotencyAutoInject: testsMissingIdempotencyKey,
2588
- });
2589
- const run = await (0, client_1.runStep)(step.title, effectiveStep.task, async () => {
2590
- if (!captureA2a)
2591
- return dispatch();
2592
- try {
2593
- const { result: dispatchResult, captures } = await (0, rawResponseCapture_1.withRawResponseCapture)(dispatch);
2594
- a2aCaptures = captures;
2595
- return dispatchResult;
2596
- }
2597
- catch (err) {
2598
- // `withRawResponseCapture` attaches partial captures to the
2599
- // thrown error so we still get the wire-shape envelope when
2600
- // the SDK threw mid-parse (e.g. agent emitted malformed JSON).
2601
- // Bare-throw cases (network errors, no captures attached)
2602
- // leave `a2aCaptures` undefined and the validator self-skips.
2603
- const partial = (0, rawResponseCapture_1.getCapturesFromError)(err);
2604
- if (partial)
2605
- a2aCaptures = partial;
2606
- throw err;
2755
+ if (step.parallel_dispatch) {
2756
+ // Fan out N concurrent dispatches via the SDK client. All dispatches
2757
+ // share the runner-minted idempotency_key (default) so the seller
2758
+ // sees one logical request and resolves the race deterministically.
2759
+ //
2760
+ // Known limitation: `dispatchWithBarrier` uses `Promise.race` against a
2761
+ // timer; when the barrier wins, the underlying SDK request is NOT
2762
+ // aborted it continues to completion against the seller. The runner
2763
+ // reports the dispatch as `timed_out`, but a late-arriving success
2764
+ // can still land on the seller's idempotency cache. Storyboard
2765
+ // authors writing follow-up steps that observe seller state after a
2766
+ // barrier timeout should account for this race.
2767
+ const started = Date.now();
2768
+ crossResponses = await (0, parallel_dispatch_1.runParallelDispatches)(client, effectiveStep.task, request, {
2769
+ spec: step.parallel_dispatch,
2770
+ keyMinter: idempotency_1.generateIdempotencyKey,
2771
+ correlationPrefix: step.id,
2772
+ });
2773
+ const durationMs = Date.now() - started;
2774
+ // Representative TaskResult is ALWAYS `dispatches[0]` — pinning the
2775
+ // representative to a fixed index keeps the aggregation loop's `i=1`
2776
+ // start aligned (every dispatch graded exactly once). Picking the
2777
+ // "best" resolved arm would double-count whichever dispatch won and
2778
+ // skip dispatches[0] from per-response grading entirely.
2779
+ const firstDispatch = crossResponses.dispatches[0];
2780
+ const allTimedOut = crossResponses.dispatches.every(d => d.timed_out);
2781
+ const barrierTimeoutError = 'parallel_dispatch_barrier_timeout: no dispatch resolved within barrier_timeout_ms';
2782
+ taskResult = firstDispatch?.taskResult ?? {
2783
+ success: false,
2784
+ ...(allTimedOut && { error: barrierTimeoutError }),
2785
+ };
2786
+ // Pass/fail is derived from the cross-response set rather than any
2787
+ // single arm: the step passes when at least one dispatch resolved
2788
+ // (cross-response validations then grade the race outcome). All-
2789
+ // timed-out is a hard failure regardless of validations.
2790
+ stepResult = {
2791
+ duration_ms: durationMs,
2792
+ passed: !allTimedOut && crossResponses.resolved.length > 0,
2793
+ ...(allTimedOut && { error: barrierTimeoutError }),
2794
+ };
2795
+ if (taskResult) {
2796
+ responseRecord = {
2797
+ transport: options.protocol === 'a2a' ? 'a2a' : 'mcp',
2798
+ payload: (0, redact_secrets_1.redactSecrets)(taskResult.data ?? taskResult.error ?? null),
2799
+ duration_ms: durationMs,
2800
+ };
2607
2801
  }
2608
- });
2609
- taskResult = run.result;
2610
- stepResult = run.step;
2611
- if (captureA2a && a2aCaptures) {
2612
- a2aEnvelope = parseLastA2aMessageSendCapture(a2aCaptures);
2613
2802
  }
2614
- if (taskResult) {
2615
- responseRecord = {
2616
- transport: options.protocol === 'a2a' ? 'a2a' : 'mcp',
2617
- payload: (0, redact_secrets_1.redactSecrets)(taskResult.data ?? taskResult.error ?? null),
2618
- duration_ms: stepResult.duration_ms,
2619
- };
2803
+ else {
2804
+ const dispatch = () => (0, task_map_1.executeStoryboardTask)(client, effectiveStep.task, request, {
2805
+ skipIdempotencyAutoInject: testsMissingIdempotencyKey,
2806
+ skipAccountValidation: testsMissingAccount,
2807
+ });
2808
+ const run = await (0, client_1.runStep)(step.title, effectiveStep.task, async () => {
2809
+ if (!captureA2a)
2810
+ return dispatch();
2811
+ try {
2812
+ const { result: dispatchResult, captures } = await (0, rawResponseCapture_1.withRawResponseCapture)(dispatch);
2813
+ a2aCaptures = captures;
2814
+ return dispatchResult;
2815
+ }
2816
+ catch (err) {
2817
+ // `withRawResponseCapture` attaches partial captures to the
2818
+ // thrown error so we still get the wire-shape envelope when
2819
+ // the SDK threw mid-parse (e.g. agent emitted malformed JSON).
2820
+ // Bare-throw cases (network errors, no captures attached)
2821
+ // leave `a2aCaptures` undefined and the validator self-skips.
2822
+ const partial = (0, rawResponseCapture_1.getCapturesFromError)(err);
2823
+ if (partial)
2824
+ a2aCaptures = partial;
2825
+ throw err;
2826
+ }
2827
+ });
2828
+ taskResult = run.result;
2829
+ stepResult = run.step;
2830
+ if (captureA2a && a2aCaptures) {
2831
+ a2aEnvelope = parseLastA2aMessageSendCapture(a2aCaptures);
2832
+ }
2833
+ if (taskResult) {
2834
+ responseRecord = {
2835
+ transport: options.protocol === 'a2a' ? 'a2a' : 'mcp',
2836
+ payload: (0, redact_secrets_1.redactSecrets)(taskResult.data ?? taskResult.error ?? null),
2837
+ duration_ms: stepResult.duration_ms,
2838
+ };
2839
+ }
2620
2840
  }
2621
2841
  }
2622
2842
  const requestRecord = {
@@ -2662,6 +2882,14 @@ client, step, phaseId, context, allSteps, options, state) {
2662
2882
  // Step passes when the task fails (returns an error)
2663
2883
  passed = !taskResult?.success || !!stepResult.error;
2664
2884
  }
2885
+ else if (crossResponses) {
2886
+ // Parallel-dispatch step: pass/fail is driven by the cross-response
2887
+ // set, not the representative arm. The representative is pinned to
2888
+ // `dispatches[0]` (which may itself have failed under the race) so
2889
+ // gating on its `.success` would false-fail steps where later arms
2890
+ // resolved correctly. Validations grade the actual race outcome.
2891
+ passed = stepResult.passed;
2892
+ }
2665
2893
  else {
2666
2894
  passed = stepResult.passed && (taskResult?.success ?? false);
2667
2895
  }
@@ -2701,6 +2929,7 @@ client, step, phaseId, context, allSteps, options, state) {
2701
2929
  ...(a2aEnvelope && { a2aEnvelope }),
2702
2930
  ...(upstreamTraffic && { upstreamTraffic }),
2703
2931
  ...(step.sample_request && { storyboardStep: { sample_request: step.sample_request } }),
2932
+ ...(crossResponses && { crossResponses }),
2704
2933
  ...(() => {
2705
2934
  // Walk back through the run's captured A2A envelopes and use
2706
2935
  // the most recent prior step's envelope as the comparison
@@ -2722,6 +2951,43 @@ client, step, phaseId, context, allSteps, options, state) {
2722
2951
  })(),
2723
2952
  };
2724
2953
  validations = (0, validations_1.runValidations)(resolvedValidations, vctx);
2954
+ // Parallel-dispatch aggregation: per-response checks (`response_schema`,
2955
+ // `field_present`, `error_code`, etc.) declared by the storyboard MUST
2956
+ // grade against every dispatch's resolved response, not just the
2957
+ // representative one. Re-run each non-cross-response validation against
2958
+ // each dispatch's TaskResult and append the additional results so the
2959
+ // step's overall pass/fail reflects the full fan-out. The first run
2960
+ // (above) already covers dispatch[0]; this loop covers dispatches[1..N].
2961
+ if (crossResponses && crossResponses.dispatches.length > 1) {
2962
+ const perResponseValidations = resolvedValidations.filter(v => v.check !== 'cross_response_field_equal' && v.check !== 'cross_response_count_distinct');
2963
+ for (let i = 1; i < crossResponses.dispatches.length; i++) {
2964
+ const d = crossResponses.dispatches[i];
2965
+ if (!d || !d.taskResult)
2966
+ continue;
2967
+ const dispatchCtx = {
2968
+ ...vctx,
2969
+ taskResult: d.taskResult,
2970
+ // Per-dispatch responseRecord stays minimal — the redacted payload
2971
+ // captures enough for failure attribution without bloating success.
2972
+ ...(responseRecord && {
2973
+ response: {
2974
+ ...responseRecord,
2975
+ payload: (0, redact_secrets_1.redactSecrets)(d.taskResult.data ?? d.taskResult.error ?? null),
2976
+ duration_ms: d.duration_ms,
2977
+ },
2978
+ }),
2979
+ };
2980
+ // Drop crossResponses on the per-dispatch context so cross-response
2981
+ // checks don't re-fire here (they already ran once above on the
2982
+ // representative dispatch and produced their single result).
2983
+ delete dispatchCtx.crossResponses;
2984
+ const dispatchResults = (0, validations_1.runValidations)(perResponseValidations, dispatchCtx).map(r => ({
2985
+ ...r,
2986
+ description: `[dispatch ${d.correlation_id}] ${r.description}`,
2987
+ }));
2988
+ validations.push(...dispatchResults);
2989
+ }
2990
+ }
2725
2991
  }
2726
2992
  // Persist the captured A2A envelope keyed by step id so cross-step
2727
2993
  // validators (`a2a_context_continuity`) on subsequent steps can
@@ -2916,6 +3182,7 @@ client, step, phaseId, context, allSteps, options, state) {
2916
3182
  context_provenance: Object.fromEntries(runState.contextProvenance),
2917
3183
  }),
2918
3184
  error: step.expect_error ? undefined : truncateError(stepResult.error || taskResult?.error),
3185
+ ...(!step.expect_error && taskResult?.adcp_error && { adcp_error: taskResult.adcp_error }),
2919
3186
  next,
2920
3187
  request: requestRecord,
2921
3188
  ...(responseRecord && { response_record: responseRecord }),
@@ -3333,7 +3600,7 @@ function evalContributesIf(expr, priorStepResults) {
3333
3600
  * the function fails open and injects as before. Schema checks use raw JSON
3334
3601
  * reads, not AJV internals.
3335
3602
  */
3336
- function applyBrandInvariant(request, options, taskName) {
3603
+ function applyBrandInvariant(request, options, taskName, stepFlags) {
3337
3604
  // Only force the invariant when the caller has actually supplied a brand.
3338
3605
  // Storyboards that don't exercise brand-scoped tools (e.g. security
3339
3606
  // probes) legitimately run without one and should pass through unchanged.
@@ -3349,6 +3616,13 @@ function applyBrandInvariant(request, options, taskName) {
3349
3616
  const result = { ...request };
3350
3617
  if (topBrandOk)
3351
3618
  result.brand = brand;
3619
+ // When a storyboard step sets `omit_account: true` it is deliberately
3620
+ // testing the seller's missing-account rejection path. Skip all account
3621
+ // synthesis — both the natural-key-merge branch (existing account on the
3622
+ // request) and the synthetic-construction branch (no account on the
3623
+ // request) — so the request reaches the wire exactly as authored.
3624
+ if (stepFlags?.omit_account)
3625
+ return result;
3352
3626
  if ('account' in request) {
3353
3627
  // Caller sent an account — merge brand in only when it's a plain object
3354
3628
  // using AccountReference's natural-key variant (`{brand, operator, sandbox?}`).