@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.
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/ConversationTypes.d.ts +33 -2
- package/dist/lib/core/ConversationTypes.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +21 -8
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +8 -7
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +54 -52
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/envelope-allowlist.d.ts.map +1 -1
- package/dist/lib/server/envelope-allowlist.js +17 -0
- package/dist/lib/server/envelope-allowlist.js.map +1 -1
- package/dist/lib/server/idempotency/store.d.ts +8 -0
- package/dist/lib/server/idempotency/store.d.ts.map +1 -1
- package/dist/lib/server/idempotency/store.js +28 -4
- package/dist/lib/server/idempotency/store.js.map +1 -1
- package/dist/lib/server/wire-spec-fields.generated.js +1 -1
- package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
- package/dist/lib/testing/compliance/comply.js +51 -15
- package/dist/lib/testing/compliance/comply.js.map +1 -1
- package/dist/lib/testing/compliance/types.d.ts +24 -0
- package/dist/lib/testing/compliance/types.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/loader.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/loader.js +17 -0
- package/dist/lib/testing/storyboard/loader.js.map +1 -1
- package/dist/lib/testing/storyboard/parallel-dispatch.d.ts +110 -0
- package/dist/lib/testing/storyboard/parallel-dispatch.d.ts.map +1 -0
- package/dist/lib/testing/storyboard/parallel-dispatch.js +221 -0
- package/dist/lib/testing/storyboard/parallel-dispatch.js.map +1 -0
- package/dist/lib/testing/storyboard/request-builder.js +1 -1
- package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
- package/dist/lib/testing/storyboard/runner.d.ts +3 -1
- package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/runner.js +318 -44
- package/dist/lib/testing/storyboard/runner.js.map +1 -1
- package/dist/lib/testing/storyboard/task-map.d.ts +1 -0
- package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/task-map.js +7 -1
- package/dist/lib/testing/storyboard/task-map.js.map +1 -1
- package/dist/lib/testing/storyboard/types.d.ts +98 -2
- package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/types.js +1 -0
- package/dist/lib/testing/storyboard/types.js.map +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +38 -0
- package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/validations.js +143 -0
- package/dist/lib/testing/storyboard/validations.js.map +1 -1
- package/dist/lib/testing/types.d.ts +2 -0
- package/dist/lib/testing/types.d.ts.map +1 -1
- package/dist/lib/utils/error-extraction.d.ts +2 -1
- package/dist/lib/utils/error-extraction.d.ts.map +1 -1
- package/dist/lib/utils/error-extraction.js +28 -0
- package/dist/lib/utils/error-extraction.js.map +1 -1
- package/dist/lib/utils/request-normalizer.d.ts +1 -0
- package/dist/lib/utils/request-normalizer.d.ts.map +1 -1
- package/dist/lib/utils/request-normalizer.js +34 -11
- package/dist/lib/utils/request-normalizer.js.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.js.map +1 -1
- package/package.json +1 -1
- 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
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2511
|
-
// `
|
|
2512
|
-
// `
|
|
2513
|
-
//
|
|
2514
|
-
//
|
|
2515
|
-
//
|
|
2516
|
-
//
|
|
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
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
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
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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?}`).
|