@dogpile/sdk 0.3.1 → 0.4.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 (56) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +1 -0
  3. package/dist/browser/index.js +1595 -54
  4. package/dist/browser/index.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/providers/openai-compatible.d.ts +11 -0
  8. package/dist/providers/openai-compatible.d.ts.map +1 -1
  9. package/dist/providers/openai-compatible.js +87 -2
  10. package/dist/providers/openai-compatible.js.map +1 -1
  11. package/dist/runtime/cancellation.d.ts +26 -0
  12. package/dist/runtime/cancellation.d.ts.map +1 -1
  13. package/dist/runtime/cancellation.js +38 -1
  14. package/dist/runtime/cancellation.js.map +1 -1
  15. package/dist/runtime/coordinator.d.ts +74 -1
  16. package/dist/runtime/coordinator.d.ts.map +1 -1
  17. package/dist/runtime/coordinator.js +932 -25
  18. package/dist/runtime/coordinator.js.map +1 -1
  19. package/dist/runtime/decisions.d.ts +25 -3
  20. package/dist/runtime/decisions.d.ts.map +1 -1
  21. package/dist/runtime/decisions.js +241 -3
  22. package/dist/runtime/decisions.js.map +1 -1
  23. package/dist/runtime/defaults.d.ts +37 -1
  24. package/dist/runtime/defaults.d.ts.map +1 -1
  25. package/dist/runtime/defaults.js +347 -0
  26. package/dist/runtime/defaults.js.map +1 -1
  27. package/dist/runtime/engine.d.ts.map +1 -1
  28. package/dist/runtime/engine.js +254 -24
  29. package/dist/runtime/engine.js.map +1 -1
  30. package/dist/runtime/sequential.d.ts.map +1 -1
  31. package/dist/runtime/sequential.js +8 -1
  32. package/dist/runtime/sequential.js.map +1 -1
  33. package/dist/runtime/validation.d.ts +10 -0
  34. package/dist/runtime/validation.d.ts.map +1 -1
  35. package/dist/runtime/validation.js +73 -0
  36. package/dist/runtime/validation.js.map +1 -1
  37. package/dist/types/events.d.ts +329 -8
  38. package/dist/types/events.d.ts.map +1 -1
  39. package/dist/types/replay.d.ts +5 -1
  40. package/dist/types/replay.d.ts.map +1 -1
  41. package/dist/types.d.ts +131 -5
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/index.ts +10 -0
  46. package/src/providers/openai-compatible.ts +82 -3
  47. package/src/runtime/cancellation.ts +59 -1
  48. package/src/runtime/coordinator.ts +1170 -25
  49. package/src/runtime/decisions.ts +307 -4
  50. package/src/runtime/defaults.ts +376 -0
  51. package/src/runtime/engine.ts +363 -24
  52. package/src/runtime/sequential.ts +9 -1
  53. package/src/runtime/validation.ts +81 -0
  54. package/src/types/events.ts +359 -8
  55. package/src/types/replay.ts +12 -1
  56. package/src/types.ts +147 -3
@@ -193,6 +193,27 @@ function addCost(left, right) {
193
193
  totalTokens: left.totalTokens + right.totalTokens
194
194
  };
195
195
  }
196
+ function resolveOnChildFailure(runOption, engineOption) {
197
+ return runOption ?? engineOption ?? "continue";
198
+ }
199
+ /**
200
+ * Walk a parent's events and accumulate the cost contributed by every
201
+ * sub-run (BUDGET-03 / D-06). Internal helper — not part of the public surface.
202
+ *
203
+ * - `sub-run-completed` events contribute `event.subResult.cost`.
204
+ * - `sub-run-failed` events contribute `event.partialCost` (real provider
205
+ * spend captured before the throw).
206
+ *
207
+ * Used by the `parent-rollup-drift` parity check in
208
+ * {@link recomputeAccountingFromTrace} to verify the parent's recorded
209
+ * accounting equals `localOnly + Σ children` recursively.
210
+ */
211
+ function accumulateSubRunCost(events) {
212
+ let total = emptyCost();
213
+ for (const event of events) if (event.type === "sub-run-completed") total = addCost(total, event.subResult.cost);
214
+ else if (event.type === "sub-run-failed") total = addCost(total, event.partialCost);
215
+ return total;
216
+ }
196
217
  function createTranscriptLink(transcript) {
197
218
  return {
198
219
  kind: "trace-transcript",
@@ -291,7 +312,14 @@ function createReplayTraceBudgetStateChanges(events) {
291
312
  case "model-response":
292
313
  case "model-output-chunk":
293
314
  case "tool-call":
294
- case "tool-result": return [];
315
+ case "tool-result":
316
+ case "sub-run-started":
317
+ case "sub-run-completed":
318
+ case "sub-run-failed":
319
+ case "sub-run-parent-aborted":
320
+ case "sub-run-budget-clamped":
321
+ case "sub-run-queued":
322
+ case "sub-run-concurrency-clamped": return [];
295
323
  }
296
324
  });
297
325
  }
@@ -388,6 +416,24 @@ function createReplayTraceProtocolDecision(protocol, event, eventIndex, options
388
416
  output: event.output,
389
417
  cost: event.cost
390
418
  };
419
+ case "sub-run-started": return {
420
+ ...base,
421
+ input: event.intent
422
+ };
423
+ case "sub-run-completed": return {
424
+ ...base,
425
+ output: event.subResult.output,
426
+ cost: event.subResult.cost
427
+ };
428
+ case "sub-run-failed": return { ...base };
429
+ case "sub-run-parent-aborted": return { ...base };
430
+ case "sub-run-budget-clamped": return { ...base };
431
+ case "sub-run-queued": return {
432
+ ...base,
433
+ childRunId: event.childRunId,
434
+ queuePosition: event.queuePosition
435
+ };
436
+ case "sub-run-concurrency-clamped": return { ...base };
391
437
  }
392
438
  }
393
439
  function defaultProtocolDecision(event) {
@@ -402,6 +448,13 @@ function defaultProtocolDecision(event) {
402
448
  case "broadcast": return "collect-broadcast-round";
403
449
  case "budget-stop": return "stop-for-budget";
404
450
  case "final": return "finalize-output";
451
+ case "sub-run-started": return "start-sub-run";
452
+ case "sub-run-completed": return "complete-sub-run";
453
+ case "sub-run-failed": return "fail-sub-run";
454
+ case "sub-run-parent-aborted": return "mark-sub-run-parent-aborted";
455
+ case "sub-run-budget-clamped": return "mark-sub-run-budget-clamped";
456
+ case "sub-run-queued": return "queue-sub-run";
457
+ case "sub-run-concurrency-clamped": return "mark-sub-run-concurrency-clamped";
405
458
  }
406
459
  }
407
460
  function eventAgentScope(event) {
@@ -467,6 +520,216 @@ function canonicalizeRunResult(result) {
467
520
  function stableJsonStringify(value) {
468
521
  return JSON.stringify(canonicalizeSerializable(value));
469
522
  }
523
+ /**
524
+ * The eight numeric fields recursively verified by `recomputeAccountingFromTrace`.
525
+ *
526
+ * These are the only summable scalars on `RunAccounting`. Non-numeric fields
527
+ * (`kind`, `tier`, `budget`, `termination`, `budgetStateChanges`) and derived
528
+ * ratios (`usdCapUtilization`, `totalTokenCapUtilization`) are NOT in this set.
529
+ */
530
+ var RECOMPUTE_FIELD_ORDER = [
531
+ "cost.usd",
532
+ "cost.inputTokens",
533
+ "cost.outputTokens",
534
+ "cost.totalTokens",
535
+ "usage.usd",
536
+ "usage.inputTokens",
537
+ "usage.outputTokens",
538
+ "usage.totalTokens"
539
+ ];
540
+ var USD_FIELDS = new Set(["cost.usd", "usage.usd"]);
541
+ var FLOAT_EPSILON = 1e-9;
542
+ function readNumericField(accounting, field) {
543
+ switch (field) {
544
+ case "cost.usd": return accounting.cost.usd;
545
+ case "cost.inputTokens": return accounting.cost.inputTokens;
546
+ case "cost.outputTokens": return accounting.cost.outputTokens;
547
+ case "cost.totalTokens": return accounting.cost.totalTokens;
548
+ case "usage.usd": return accounting.usage.usd;
549
+ case "usage.inputTokens": return accounting.usage.inputTokens;
550
+ case "usage.outputTokens": return accounting.usage.outputTokens;
551
+ case "usage.totalTokens": return accounting.usage.totalTokens;
552
+ }
553
+ }
554
+ function fieldsEqual(field, a, b) {
555
+ if (USD_FIELDS.has(field)) return Math.abs(a - b) < FLOAT_EPSILON;
556
+ return a === b;
557
+ }
558
+ function firstDifferingField(recorded, recomputed) {
559
+ for (const field of RECOMPUTE_FIELD_ORDER) {
560
+ const a = readNumericField(recorded, field);
561
+ const b = readNumericField(recomputed, field);
562
+ if (!fieldsEqual(field, a, b)) return {
563
+ field,
564
+ recorded: a,
565
+ recomputed: b
566
+ };
567
+ }
568
+ return null;
569
+ }
570
+ function buildLocalAccounting(trace) {
571
+ return createRunAccounting({
572
+ tier: trace.tier,
573
+ ...trace.budget.caps ? { budget: trace.budget.caps } : {},
574
+ ...trace.budget.termination ? { termination: trace.budget.termination } : {},
575
+ cost: trace.finalOutput.cost,
576
+ events: trace.events
577
+ });
578
+ }
579
+ function lastCostBearingEventCost(events) {
580
+ for (let index = events.length - 1; index >= 0; index -= 1) {
581
+ const event = events[index];
582
+ if (event === void 0) continue;
583
+ if (event.type === "final" || event.type === "agent-turn" || event.type === "broadcast" || event.type === "budget-stop") return event.cost;
584
+ }
585
+ return null;
586
+ }
587
+ /**
588
+ * Recompute a parent's `RunAccounting` from a saved `Trace` for replay-time
589
+ * tamper detection.
590
+ *
591
+ * @remarks
592
+ * Returns the parent's local `RunAccounting` (built the same way `replay()`
593
+ * builds it today, from `trace.finalOutput.cost` and `trace.events`). While
594
+ * walking events, every `sub-run-completed` is recursed into and the
595
+ * recomputed child accounting is compared field-by-field to the recorded
596
+ * `event.subResult.accounting`. A mismatch on any of the eight enumerated
597
+ * numeric fields throws `DogpileError({ code: "invalid-configuration" })`
598
+ * with `detail.reason: "trace-accounting-mismatch"` and a concrete
599
+ * `detail.field` identifying the first differing numeric.
600
+ *
601
+ * Pure: no provider calls, no I/O, no clock reads.
602
+ *
603
+ * Non-summed fields (`kind`, `tier`, `budget`, `termination`,
604
+ * `budgetStateChanges`) and derived ratios (`usdCapUtilization`,
605
+ * `totalTokenCapUtilization`) are not in the comparison set.
606
+ */
607
+ function recomputeAccountingFromTrace(trace) {
608
+ const local = buildLocalAccounting(trace);
609
+ const lastEventCost = lastCostBearingEventCost(trace.events);
610
+ if (lastEventCost !== null) {
611
+ const drift = firstDifferingField(local, createRunAccounting({
612
+ tier: trace.tier,
613
+ ...trace.budget.caps ? { budget: trace.budget.caps } : {},
614
+ ...trace.budget.termination ? { termination: trace.budget.termination } : {},
615
+ cost: lastEventCost,
616
+ events: trace.events
617
+ }));
618
+ if (drift !== null) throw new DogpileError({
619
+ code: "invalid-configuration",
620
+ message: `Trace accounting mismatch at parent run ${trace.runId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
621
+ retryable: false,
622
+ detail: {
623
+ kind: "trace-validation",
624
+ reason: "trace-accounting-mismatch",
625
+ eventIndex: -1,
626
+ childRunId: trace.runId,
627
+ field: drift.field,
628
+ recorded: drift.recorded,
629
+ recomputed: drift.recomputed
630
+ }
631
+ });
632
+ }
633
+ for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
634
+ const event = trace.events[eventIndex];
635
+ if (event === void 0) continue;
636
+ if (event.type === "sub-run-completed") {
637
+ const childRecordedRollup = createRunAccounting({
638
+ tier: trace.tier,
639
+ cost: event.subResult.cost,
640
+ events: []
641
+ });
642
+ const childRecordedAccounting = event.subResult.accounting;
643
+ const drift = firstDifferingField(childRecordedAccounting, childRecordedRollup);
644
+ if (drift !== null) throw new DogpileError({
645
+ code: "invalid-configuration",
646
+ message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded} on accounting, ${drift.recomputed} on subResult.cost.`,
647
+ retryable: false,
648
+ detail: {
649
+ kind: "trace-validation",
650
+ reason: "trace-accounting-mismatch",
651
+ subReason: "parent-rollup-drift",
652
+ eventIndex,
653
+ childRunId: event.childRunId,
654
+ field: drift.field,
655
+ recorded: drift.recorded,
656
+ recomputed: drift.recomputed
657
+ }
658
+ });
659
+ } else if (event.type === "sub-run-failed") {
660
+ const partialFromTrace = lastCostBearingEventCost(event.partialTrace.events) ?? emptyCost();
661
+ const drift = firstDifferingField(createRunAccounting({
662
+ tier: trace.tier,
663
+ cost: event.partialCost,
664
+ events: []
665
+ }), createRunAccounting({
666
+ tier: trace.tier,
667
+ cost: partialFromTrace,
668
+ events: []
669
+ }));
670
+ if (drift !== null) throw new DogpileError({
671
+ code: "invalid-configuration",
672
+ message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: partialCost field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed} from partialTrace events.`,
673
+ retryable: false,
674
+ detail: {
675
+ kind: "trace-validation",
676
+ reason: "trace-accounting-mismatch",
677
+ subReason: "parent-rollup-drift",
678
+ eventIndex,
679
+ childRunId: event.childRunId,
680
+ field: drift.field,
681
+ recorded: drift.recorded,
682
+ recomputed: drift.recomputed
683
+ }
684
+ });
685
+ }
686
+ }
687
+ const subRunTotal = accumulateSubRunCost(trace.events);
688
+ const parentTotal = trace.finalOutput.cost;
689
+ for (const field of RECOMPUTE_FIELD_ORDER) {
690
+ if (field.startsWith("usage.")) continue;
691
+ const [, key] = field.split(".");
692
+ const parentValue = parentTotal[key];
693
+ const childValue = subRunTotal[key];
694
+ if (childValue - parentValue > FLOAT_EPSILON) throw new DogpileError({
695
+ code: "invalid-configuration",
696
+ message: `Trace parent-rollup mismatch at run ${trace.runId}: field "${field}" Σ children ${childValue} exceeds parent recorded ${parentValue}.`,
697
+ retryable: false,
698
+ detail: {
699
+ kind: "trace-validation",
700
+ reason: "trace-accounting-mismatch",
701
+ subReason: "parent-rollup-drift",
702
+ eventIndex: -1,
703
+ childRunId: trace.runId,
704
+ field,
705
+ recorded: parentValue,
706
+ recomputed: childValue
707
+ }
708
+ });
709
+ }
710
+ for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
711
+ const event = trace.events[eventIndex];
712
+ if (event === void 0 || event.type !== "sub-run-completed") continue;
713
+ const childRecomputed = recomputeAccountingFromTrace(event.subResult.trace);
714
+ const childRecorded = event.subResult.accounting;
715
+ const drift = firstDifferingField(childRecorded, childRecomputed);
716
+ if (drift !== null) throw new DogpileError({
717
+ code: "invalid-configuration",
718
+ message: `Trace accounting mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
719
+ retryable: false,
720
+ detail: {
721
+ kind: "trace-validation",
722
+ reason: "trace-accounting-mismatch",
723
+ eventIndex,
724
+ childRunId: event.childRunId,
725
+ field: drift.field,
726
+ recorded: drift.recorded,
727
+ recomputed: drift.recomputed
728
+ }
729
+ });
730
+ }
731
+ return local;
732
+ }
470
733
  function canonicalizeSerializable(value) {
471
734
  if (Array.isArray(value)) return value.map((item) => canonicalizeSerializable(item));
472
735
  if (typeof value === "number") {
@@ -485,6 +748,24 @@ function canonicalizeSerializable(value) {
485
748
  }
486
749
  //#endregion
487
750
  //#region src/runtime/cancellation.ts
751
+ /**
752
+ * Classify an abort signal's reason into the BUDGET-01 / BUDGET-02
753
+ * `detail.reason` discriminator.
754
+ *
755
+ * - `"timeout"` when the reason is a {@link DogpileError} with `code === "timeout"`
756
+ * (matches the parent-deadline abort path in `engine.ts:createTimeoutAbortLifecycle`).
757
+ * - `"parent-aborted"` for every other reason — explicit caller abort, plain
758
+ * `Error`, `undefined`, or arbitrary primitive.
759
+ */
760
+ function classifyAbortReason(signalReasonOrError) {
761
+ if (DogpileError.isInstance(signalReasonOrError) && signalReasonOrError.code === "timeout") return "timeout";
762
+ return "parent-aborted";
763
+ }
764
+ function classifyChildTimeoutSource(_error, context) {
765
+ if (context.isProviderError) return "provider";
766
+ if (context.decisionTimeoutMs !== void 0 || context.engineDefaultTimeoutMs !== void 0) return "engine";
767
+ return "provider";
768
+ }
488
769
  function throwIfAborted(signal, providerId) {
489
770
  if (!signal?.aborted) return;
490
771
  throw createAbortErrorFromSignal(signal, providerId);
@@ -501,7 +782,7 @@ function createAbortError(providerId, detail, cause) {
501
782
  }
502
783
  function createAbortErrorFromSignal(signal, providerId) {
503
784
  if (DogpileError.isInstance(signal.reason)) return signal.reason;
504
- return createAbortError(providerId, void 0, signal.reason);
785
+ return createAbortError(providerId, { reason: classifyAbortReason(signal.reason) }, signal.reason);
505
786
  }
506
787
  function createTimeoutError(providerId, timeoutMs) {
507
788
  return new DogpileError({
@@ -512,23 +793,223 @@ function createTimeoutError(providerId, timeoutMs) {
512
793
  detail: { timeoutMs }
513
794
  });
514
795
  }
796
+ function createEngineDeadlineTimeoutError(providerId, timeoutMs) {
797
+ return new DogpileError({
798
+ code: "provider-timeout",
799
+ message: `The child engine deadline expired after ${timeoutMs}ms.`,
800
+ retryable: true,
801
+ providerId,
802
+ detail: {
803
+ timeoutMs,
804
+ source: "engine"
805
+ }
806
+ });
807
+ }
515
808
  //#endregion
516
809
  //#region src/runtime/decisions.ts
517
- function parseAgentDecision(output) {
810
+ var PROTOCOL_NAMES = [
811
+ "coordinator",
812
+ "sequential",
813
+ "broadcast",
814
+ "shared"
815
+ ];
816
+ function parseAgentDecision(output, context = {}) {
817
+ const delegateBlock = matchDelegateBlock(output);
818
+ if (delegateBlock !== void 0) return parseDelegateDecision(delegateBlock, context);
819
+ return parseParticipateDecision(output);
820
+ }
821
+ function isParticipatingDecision(decision) {
822
+ if (decision === void 0 || isDelegateDecisionArray(decision) || decision.type !== "participate") return false;
823
+ return decision.participation !== "abstain";
824
+ }
825
+ function isDelegateDecisionArray(decision) {
826
+ return Array.isArray(decision);
827
+ }
828
+ function parseParticipateDecision(output) {
518
829
  const selectedRole = matchLine(output, /^role_selected:\s*(.+)$/imu);
519
830
  const participation = matchLine(output, /^participation:\s*(contribute|abstain)$/imu);
520
831
  const rationale = matchLine(output, /^rationale:\s*(.+)$/imu);
521
832
  const contribution = matchContribution(output);
522
833
  if (!selectedRole || !participation || !isAgentParticipation(participation) || !rationale || !contribution) return;
523
834
  return {
835
+ type: "participate",
524
836
  selectedRole,
525
837
  participation,
526
838
  rationale,
527
839
  contribution
528
840
  };
529
841
  }
530
- function isParticipatingDecision(decision) {
531
- return decision?.participation !== "abstain";
842
+ /**
843
+ * Locate a `delegate:` line followed by a fenced JSON block in the agent's
844
+ * output. Returns the raw JSON text inside the fence, or `undefined` when no
845
+ * delegate block is present. Tolerates ```` ```json ```` and bare ```` ``` ````.
846
+ */
847
+ function matchDelegateBlock(output) {
848
+ return output.match(/^delegate:\s*\r?\n\s*```(?:json)?\s*\r?\n([\s\S]*?)\r?\n\s*```/imu)?.[1];
849
+ }
850
+ function parseDelegateDecision(jsonText, context) {
851
+ let parsed;
852
+ try {
853
+ parsed = JSON.parse(jsonText);
854
+ } catch (error) {
855
+ throwInvalidDelegate({
856
+ path: "decision",
857
+ message: `delegate JSON did not parse: ${error instanceof Error ? error.message : String(error)}`,
858
+ expected: "valid JSON object",
859
+ received: truncate(jsonText)
860
+ });
861
+ }
862
+ if (Array.isArray(parsed)) {
863
+ if (parsed.length === 0) throwInvalidDelegate({
864
+ path: "decision",
865
+ message: "delegate array must not be empty.",
866
+ expected: "array with 1..8 delegate objects",
867
+ received: "empty array"
868
+ });
869
+ return parsed.map((item) => parseSingleDelegateObject(item, context));
870
+ }
871
+ return parseSingleDelegateObject(parsed, context);
872
+ }
873
+ function parseSingleDelegateObject(parsed, context) {
874
+ if (parsed === null || typeof parsed !== "object") throwInvalidDelegate({
875
+ path: "decision",
876
+ message: "delegate decision must be a JSON object.",
877
+ expected: "object",
878
+ received: describe(parsed)
879
+ });
880
+ const record = parsed;
881
+ const protocol = record["protocol"];
882
+ if (typeof protocol !== "string" || !PROTOCOL_NAMES.includes(protocol)) throwInvalidDelegate({
883
+ path: "decision.protocol",
884
+ message: `protocol "${describe(protocol)}" is not a known coordination protocol.`,
885
+ expected: PROTOCOL_NAMES.join(" | "),
886
+ received: describe(protocol)
887
+ });
888
+ const intentRaw = record["intent"];
889
+ const intent = typeof intentRaw === "string" ? intentRaw.trim() : "";
890
+ if (intent.length === 0) throwInvalidDelegate({
891
+ path: "decision.intent",
892
+ message: "delegate decision must include a non-empty intent string.",
893
+ expected: "non-empty string",
894
+ received: describe(intentRaw)
895
+ });
896
+ const result = {
897
+ type: "delegate",
898
+ protocol,
899
+ intent
900
+ };
901
+ if (record["model"] !== void 0) {
902
+ const model = record["model"];
903
+ if (typeof model !== "string" || model.length === 0) throwInvalidDelegate({
904
+ path: "decision.model",
905
+ message: "delegate decision model must be a non-empty string when present.",
906
+ expected: "non-empty string",
907
+ received: describe(model)
908
+ });
909
+ if (context.parentProviderId !== void 0 && model !== context.parentProviderId) throwInvalidDelegate({
910
+ path: "decision.model",
911
+ message: `delegate decision model "${model}" does not match parent provider id "${context.parentProviderId}".`,
912
+ expected: context.parentProviderId,
913
+ received: model
914
+ });
915
+ result.model = model;
916
+ }
917
+ if (record["budget"] !== void 0) result.budget = parseDelegateBudget(record["budget"]);
918
+ if (record["maxConcurrentChildren"] !== void 0) {
919
+ const value = record["maxConcurrentChildren"];
920
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) throwInvalidDelegate({
921
+ path: "decision.maxConcurrentChildren",
922
+ message: "delegate decision maxConcurrentChildren must be a positive integer when present.",
923
+ expected: "integer >= 1",
924
+ received: describe(value)
925
+ });
926
+ result.maxConcurrentChildren = value;
927
+ }
928
+ if (context.currentDepth !== void 0 && context.maxDepth !== void 0) {
929
+ if (context.currentDepth + 1 > context.maxDepth) throw depthOverflowError(context.currentDepth, context.maxDepth);
930
+ }
931
+ return result;
932
+ }
933
+ /**
934
+ * Build the canonical depth-overflow `DogpileError`. Used by the parser (this
935
+ * file) and the coordinator dispatcher; kept here so both call sites produce
936
+ * the exact same error shape (D-14, D-15).
937
+ */
938
+ function depthOverflowError(currentDepth, maxDepth) {
939
+ return new DogpileError({
940
+ code: "invalid-configuration",
941
+ message: `Depth overflow: cannot dispatch sub-run at depth ${currentDepth + 1} (maxDepth = ${maxDepth}).`,
942
+ retryable: false,
943
+ detail: {
944
+ kind: "delegate-validation",
945
+ path: "decision.protocol",
946
+ reason: "depth-overflow",
947
+ currentDepth,
948
+ maxDepth
949
+ }
950
+ });
951
+ }
952
+ /**
953
+ * Dispatcher-time depth gate. Throws the same error shape the parser uses; the
954
+ * dual gate (parser + dispatcher) defends against any TOCTOU window between
955
+ * decision parsing and child-run spin-up (D-14).
956
+ */
957
+ function assertDepthWithinLimit(currentDepth, maxDepth) {
958
+ if (currentDepth + 1 > maxDepth) throw depthOverflowError(currentDepth, maxDepth);
959
+ }
960
+ function parseDelegateBudget(raw) {
961
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throwInvalidDelegate({
962
+ path: "decision.budget",
963
+ message: "delegate decision budget must be an object.",
964
+ expected: "object",
965
+ received: describe(raw)
966
+ });
967
+ const record = raw;
968
+ const budget = {};
969
+ if (record["timeoutMs"] !== void 0) {
970
+ const value = record["timeoutMs"];
971
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throwInvalidDelegate({
972
+ path: "decision.budget.timeoutMs",
973
+ message: "delegate decision budget.timeoutMs must be a non-negative integer.",
974
+ expected: "integer >= 0",
975
+ received: describe(value)
976
+ });
977
+ budget.timeoutMs = value;
978
+ }
979
+ if (record["maxTokens"] !== void 0) {
980
+ const value = record["maxTokens"];
981
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throwInvalidDelegate({
982
+ path: "decision.budget.maxTokens",
983
+ message: "delegate decision budget.maxTokens must be a non-negative integer.",
984
+ expected: "integer >= 0",
985
+ received: describe(value)
986
+ });
987
+ budget.maxTokens = value;
988
+ }
989
+ if (record["maxIterations"] !== void 0) {
990
+ const value = record["maxIterations"];
991
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throwInvalidDelegate({
992
+ path: "decision.budget.maxIterations",
993
+ message: "delegate decision budget.maxIterations must be a non-negative integer.",
994
+ expected: "integer >= 0",
995
+ received: describe(value)
996
+ });
997
+ budget.maxIterations = value;
998
+ }
999
+ return budget;
1000
+ }
1001
+ function throwInvalidDelegate(failure) {
1002
+ throw new DogpileError({
1003
+ code: "invalid-configuration",
1004
+ message: `Invalid Dogpile configuration at ${failure.path}: ${failure.message}`,
1005
+ retryable: false,
1006
+ detail: {
1007
+ kind: "delegate-validation",
1008
+ path: failure.path,
1009
+ expected: failure.expected,
1010
+ received: failure.received
1011
+ }
1012
+ });
532
1013
  }
533
1014
  function matchLine(output, pattern) {
534
1015
  return output.match(pattern)?.[1]?.trim();
@@ -540,6 +1021,15 @@ function matchContribution(output) {
540
1021
  function isAgentParticipation(value) {
541
1022
  return value === "contribute" || value === "abstain";
542
1023
  }
1024
+ function describe(value) {
1025
+ if (value === null) return "null";
1026
+ if (Array.isArray(value)) return "array";
1027
+ if (typeof value === "string") return JSON.stringify(value).slice(0, 200);
1028
+ return typeof value;
1029
+ }
1030
+ function truncate(value) {
1031
+ return value.length > 200 ? `${value.slice(0, 200)}…` : value;
1032
+ }
543
1033
  //#endregion
544
1034
  //#region src/runtime/model.ts
545
1035
  async function generateModelTurn(options) {
@@ -997,6 +1487,7 @@ var budgetTiers = [
997
1487
  "balanced",
998
1488
  "quality"
999
1489
  ];
1490
+ var onChildFailureModes = ["continue", "abort"];
1000
1491
  /**
1001
1492
  * Validate high-level caller options before any protocol execution starts.
1002
1493
  */
@@ -1015,11 +1506,25 @@ function validateDogpileOptions(options) {
1015
1506
  validateOptionalFunction(options.evaluate, "evaluate");
1016
1507
  validateOptionalSeed(options.seed, "seed");
1017
1508
  validateOptionalAbortSignal(options.signal, "signal");
1509
+ validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
1510
+ validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
1511
+ validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
1512
+ validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
1018
1513
  }
1019
1514
  function validateMissionIntent(intent, path = "intent") {
1020
1515
  validateNonEmptyString(intent, path, "intent is required.");
1021
1516
  }
1022
1517
  /**
1518
+ * Validate per-call run/stream options (`Engine.run(intent, options)` / `Engine.stream(...)`).
1519
+ */
1520
+ function validateRunCallOptions(options, path = "options") {
1521
+ if (options === void 0) return;
1522
+ const record = requireRecord(options, path);
1523
+ validateOptionalNonNegativeInteger(record.maxDepth, `${path}.maxDepth`);
1524
+ validateOptionalPositiveInteger(record.maxConcurrentChildren, `${path}.maxConcurrentChildren`);
1525
+ validateOptionalOnChildFailure(record.onChildFailure, `${path}.onChildFailure`);
1526
+ }
1527
+ /**
1023
1528
  * Validate low-level engine configuration before normalizing reusable controls.
1024
1529
  */
1025
1530
  function validateEngineOptions(options) {
@@ -1036,6 +1541,10 @@ function validateEngineOptions(options) {
1036
1541
  validateOptionalFunction(options.evaluate, "evaluate");
1037
1542
  validateOptionalSeed(options.seed, "seed");
1038
1543
  validateOptionalAbortSignal(options.signal, "signal");
1544
+ validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
1545
+ validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
1546
+ validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
1547
+ validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
1039
1548
  }
1040
1549
  function validateProtocolSelection(value, path) {
1041
1550
  if (typeof value === "string") {
@@ -1083,6 +1592,23 @@ function validateBudgetTier(value, path) {
1083
1592
  actual: value
1084
1593
  });
1085
1594
  }
1595
+ function validateOptionalOnChildFailure(value, path) {
1596
+ if (value === void 0) return;
1597
+ if (value === "continue" || value === "abort") return;
1598
+ throw new DogpileError({
1599
+ code: "invalid-configuration",
1600
+ message: `Invalid onChildFailure: expected "continue" or "abort", got ${JSON.stringify(value)}`,
1601
+ retryable: false,
1602
+ detail: {
1603
+ kind: "configuration-validation",
1604
+ path,
1605
+ rule: "enum",
1606
+ expected: onChildFailureModes.join(" | "),
1607
+ received: describeValue(value),
1608
+ reason: "invalid-on-child-failure"
1609
+ }
1610
+ });
1611
+ }
1086
1612
  /**
1087
1613
  * Validate configured model provider definitions at registration boundaries.
1088
1614
  */
@@ -1092,6 +1618,21 @@ function validateModelProviderRegistration(value, path = "model") {
1092
1618
  validateFunction(record.generate, `${path}.generate`);
1093
1619
  validateOptionalFunction(record.stream, `${path}.stream`);
1094
1620
  }
1621
+ /**
1622
+ * Engine-time defense-in-depth check that a provider's optional
1623
+ * `metadata.locality` is one of the two valid values when present (Phase 3 D-03).
1624
+ * Catches user-implemented providers that bypass TypeScript checks.
1625
+ */
1626
+ function validateProviderLocality(provider, pathPrefix = "model") {
1627
+ const loc = provider.metadata?.locality;
1628
+ if (loc !== void 0 && loc !== "local" && loc !== "remote") invalidConfiguration({
1629
+ path: `${pathPrefix}.metadata.locality`,
1630
+ rule: "enum",
1631
+ message: `${pathPrefix}.metadata.locality must be "local" or "remote" when provided.`,
1632
+ expected: "\"local\" | \"remote\"",
1633
+ actual: loc
1634
+ });
1635
+ }
1095
1636
  function validateOptionalAgents(value, path) {
1096
1637
  if (value === void 0) return;
1097
1638
  if (!Array.isArray(value)) invalidConfiguration({
@@ -1371,6 +1912,16 @@ function validateOptionalNonNegativeInteger(value, path) {
1371
1912
  actual: value
1372
1913
  });
1373
1914
  }
1915
+ function validateOptionalPositiveFiniteNumber(value, path) {
1916
+ if (value === void 0) return;
1917
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) invalidConfiguration({
1918
+ path,
1919
+ rule: "positive-finite-number",
1920
+ message: "value must be a positive finite number.",
1921
+ expected: "finite number > 0",
1922
+ actual: value
1923
+ });
1924
+ }
1374
1925
  function validateOptionalNonNegativeNumber(value, path) {
1375
1926
  if (value === void 0) return;
1376
1927
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) invalidConfiguration({
@@ -2608,19 +3159,74 @@ function responseCost$3(response) {
2608
3159
  }
2609
3160
  //#endregion
2610
3161
  //#region src/runtime/coordinator.ts
3162
+ /**
3163
+ * Hard-coded loop guard for the delegate dispatch in the coordinator plan
3164
+ * turn. After this many consecutive delegate decisions the coordinator throws
3165
+ * `invalid-configuration` (T-03-01). Not a public option.
3166
+ */
3167
+ var MAX_DISPATCH_PER_TURN = 8;
3168
+ var DEFAULT_MAX_CONCURRENT_CHILDREN$1 = 4;
3169
+ function createSemaphore(maxConcurrent) {
3170
+ let inFlight = 0;
3171
+ const waiters = [];
3172
+ return {
3173
+ acquire() {
3174
+ if (inFlight < maxConcurrent) {
3175
+ inFlight += 1;
3176
+ return Promise.resolve();
3177
+ }
3178
+ return new Promise((resolve) => {
3179
+ waiters.push(() => {
3180
+ inFlight += 1;
3181
+ resolve();
3182
+ });
3183
+ });
3184
+ },
3185
+ release() {
3186
+ inFlight -= 1;
3187
+ const next = waiters.shift();
3188
+ if (next !== void 0) next();
3189
+ },
3190
+ get inFlight() {
3191
+ return inFlight;
3192
+ },
3193
+ get queued() {
3194
+ return waiters.length;
3195
+ }
3196
+ };
3197
+ }
3198
+ /**
3199
+ * Walk the coordinator's active provider set and return the FIRST provider
3200
+ * whose metadata.locality === "local", or undefined if none found.
3201
+ *
3202
+ * Walk order (forward-compat): options.model first, then options.agents in
3203
+ * declaration order. AgentSpec has no `model` field today (Phase 3 D-11
3204
+ * forward-compat scaffolding); the agent walk uses optional chaining and
3205
+ * effectively no-ops until a future phase adds AgentSpec.model.
3206
+ */
3207
+ function findFirstLocalProvider(options) {
3208
+ if (options.model.metadata?.locality === "local") return options.model;
3209
+ for (const agent of options.agents) {
3210
+ const agentModel = agent.model;
3211
+ if (agentModel?.metadata?.locality === "local") return agentModel;
3212
+ }
3213
+ }
2611
3214
  async function runCoordinator(options) {
2612
3215
  const runId = createRunId();
2613
3216
  const events = [];
2614
3217
  const transcript = [];
2615
3218
  const protocolDecisions = [];
2616
3219
  const providerCalls = [];
3220
+ const dispatchedChildren = /* @__PURE__ */ new Map();
2617
3221
  let totalCost = emptyCost();
3222
+ let concurrencyClampEmitted = false;
2618
3223
  const maxTurns = options.protocol.maxTurns ?? options.agents.length;
2619
3224
  const activeAgents = options.agents.slice(0, maxTurns);
2620
3225
  const coordinator = activeAgents[0];
2621
3226
  const startedAtMs = nowMs();
2622
3227
  let stopped = false;
2623
3228
  let termination;
3229
+ let triggeringFailureForAbortMode;
2624
3230
  const wrapUpHint = createWrapUpHintController({
2625
3231
  protocol: options.protocol,
2626
3232
  tier: options.tier,
@@ -2636,6 +3242,51 @@ async function runCoordinator(options) {
2636
3242
  const recordProtocolDecision = (event, decisionOptions) => {
2637
3243
  protocolDecisions.push(createReplayTraceProtocolDecision("coordinator", event, events.length - 1, decisionOptions));
2638
3244
  };
3245
+ const drainOnParentAbort = (reasonSource) => {
3246
+ const reason = classifyAbortReason(reasonSource);
3247
+ for (const child of dispatchedChildren.values()) {
3248
+ if (child.closed) continue;
3249
+ const partialCost = child.started ? lastCostBearingEventCost(child.childEvents) ?? emptyCost() : emptyCost();
3250
+ const partialTrace = buildPartialTrace({
3251
+ childRunId: child.childRunId,
3252
+ events: [...child.childEvents],
3253
+ startedAtMs: child.startedAtMs,
3254
+ protocol: child.decision.protocol,
3255
+ tier: options.tier,
3256
+ modelProviderId: options.model.id,
3257
+ agents: options.agents,
3258
+ intent: child.decision.intent,
3259
+ temperature: options.temperature,
3260
+ ...child.childTimeoutMs !== void 0 ? { childTimeoutMs: child.childTimeoutMs } : {},
3261
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
3262
+ });
3263
+ const failedEvent = {
3264
+ type: "sub-run-failed",
3265
+ runId,
3266
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3267
+ childRunId: child.childRunId,
3268
+ parentRunId: runId,
3269
+ parentDecisionId: child.parentDecisionId,
3270
+ parentDecisionArrayIndex: child.parentDecisionArrayIndex,
3271
+ error: child.started ? {
3272
+ code: "aborted",
3273
+ message: "Parent run aborted.",
3274
+ detail: { reason }
3275
+ } : {
3276
+ code: "aborted",
3277
+ message: "Sibling delegate failed; queued delegate never started.",
3278
+ detail: { reason: "sibling-failed" }
3279
+ },
3280
+ partialTrace,
3281
+ partialCost
3282
+ };
3283
+ child.closed = true;
3284
+ totalCost = addCost(totalCost, partialCost);
3285
+ emit(failedEvent);
3286
+ recordProtocolDecision(failedEvent);
3287
+ }
3288
+ };
3289
+ options.registerAbortDrain?.(drainOnParentAbort);
2639
3290
  const toolExecutor = createRuntimeToolExecutor({
2640
3291
  runId,
2641
3292
  protocol: "coordinator",
@@ -2666,24 +3317,231 @@ async function runCoordinator(options) {
2666
3317
  }
2667
3318
  if (coordinator) {
2668
3319
  if (!stopIfNeeded()) {
2669
- totalCost = await runCoordinatorTurn({
2670
- agent: coordinator,
2671
- coordinator,
2672
- input: buildCoordinatorPlanInput(options.intent, coordinator),
2673
- phase: "plan",
2674
- options,
2675
- runId,
2676
- transcript,
2677
- totalCost,
2678
- providerCalls,
2679
- toolExecutor,
2680
- toolAvailability,
2681
- events,
2682
- startedAtMs,
2683
- wrapUpHint,
2684
- emit,
2685
- recordProtocolDecision
2686
- });
3320
+ let dispatchInput = buildCoordinatorPlanInput(options.intent, coordinator);
3321
+ let dispatchCount = 0;
3322
+ while (true) {
3323
+ const turnOutcome = await runCoordinatorTurn({
3324
+ agent: coordinator,
3325
+ coordinator,
3326
+ input: dispatchInput,
3327
+ phase: "plan",
3328
+ options,
3329
+ runId,
3330
+ transcript,
3331
+ totalCost,
3332
+ providerCalls,
3333
+ toolExecutor,
3334
+ toolAvailability,
3335
+ events,
3336
+ startedAtMs,
3337
+ wrapUpHint,
3338
+ emit,
3339
+ recordProtocolDecision
3340
+ });
3341
+ totalCost = turnOutcome.totalCost;
3342
+ if (turnOutcome.decision === void 0) break;
3343
+ const delegates = Array.isArray(turnOutcome.decision) ? turnOutcome.decision : turnOutcome.decision.type === "delegate" ? [turnOutcome.decision] : [];
3344
+ if (delegates.length === 0) break;
3345
+ if (dispatchCount + delegates.length > MAX_DISPATCH_PER_TURN) throw new DogpileError({
3346
+ code: "invalid-configuration",
3347
+ message: `Coordinator plan turn delegated ${delegates.length} more children after ${dispatchCount}; max is ${MAX_DISPATCH_PER_TURN}.`,
3348
+ retryable: false,
3349
+ detail: {
3350
+ kind: "delegate-validation",
3351
+ path: "decision",
3352
+ reason: "loop-guard-exceeded",
3353
+ maxDispatchPerTurn: MAX_DISPATCH_PER_TURN
3354
+ }
3355
+ });
3356
+ const parentDecisionId = String(events.length - 1);
3357
+ const parentDepth = options.currentDepth ?? 0;
3358
+ const decisionMax = delegates.reduce((max, delegate) => Math.min(max, delegate.maxConcurrentChildren ?? Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
3359
+ let effectiveForTurn = Math.min(options.effectiveMaxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN$1, decisionMax);
3360
+ const requestedMax = effectiveForTurn;
3361
+ const localProvider = findFirstLocalProvider(options);
3362
+ if (localProvider !== void 0) {
3363
+ effectiveForTurn = 1;
3364
+ if (!concurrencyClampEmitted) {
3365
+ const clampEvent = {
3366
+ type: "sub-run-concurrency-clamped",
3367
+ runId,
3368
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3369
+ requestedMax,
3370
+ effectiveMax: 1,
3371
+ reason: "local-provider-detected",
3372
+ providerId: localProvider.id
3373
+ };
3374
+ emit(clampEvent);
3375
+ recordProtocolDecision(clampEvent);
3376
+ concurrencyClampEmitted = true;
3377
+ }
3378
+ }
3379
+ const semaphore = createSemaphore(effectiveForTurn);
3380
+ const childRunIds = delegates.map(() => createRunId());
3381
+ const dispatchedForTurn = delegates.map((delegate, index) => {
3382
+ const childRunId = childRunIds[index];
3383
+ if (childRunId === void 0) throw new Error("missing child run id");
3384
+ const dispatchedChild = {
3385
+ childRunId,
3386
+ decision: delegate,
3387
+ parentDecisionId,
3388
+ parentDecisionArrayIndex: index,
3389
+ parentDepth,
3390
+ controller: new AbortController(),
3391
+ removeParentListener: void 0,
3392
+ childEvents: [],
3393
+ started: false,
3394
+ closed: false,
3395
+ startedAtMs: Date.now(),
3396
+ childTimeoutMs: void 0,
3397
+ failure: void 0
3398
+ };
3399
+ dispatchedChildren.set(childRunId, dispatchedChild);
3400
+ return dispatchedChild;
3401
+ });
3402
+ const dispatchResults = [];
3403
+ let firstFailureIndex;
3404
+ const tasks = delegates.map(async (delegate, index) => {
3405
+ const childRunId = childRunIds[index];
3406
+ if (childRunId === void 0) throw new Error("missing child run id");
3407
+ if (semaphore.inFlight >= effectiveForTurn) {
3408
+ const queuedEvent = {
3409
+ type: "sub-run-queued",
3410
+ runId,
3411
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3412
+ childRunId,
3413
+ parentRunId: runId,
3414
+ parentDecisionId,
3415
+ parentDecisionArrayIndex: index,
3416
+ protocol: delegate.protocol,
3417
+ intent: delegate.intent,
3418
+ depth: parentDepth + 1,
3419
+ queuePosition: semaphore.queued
3420
+ };
3421
+ emit(queuedEvent);
3422
+ recordProtocolDecision(queuedEvent);
3423
+ }
3424
+ await semaphore.acquire();
3425
+ try {
3426
+ const dispatchedChild = dispatchedForTurn[index];
3427
+ if (!dispatchedChild) throw new Error("missing dispatched child");
3428
+ if (firstFailureIndex !== void 0) {
3429
+ if (dispatchedChild.closed) {
3430
+ dispatchResults.push({
3431
+ index,
3432
+ result: {
3433
+ nextInput: "",
3434
+ taggedText: `[sub-run ${childRunId}]: skipped because the parent run aborted`,
3435
+ completedAtMs: Date.now()
3436
+ }
3437
+ });
3438
+ return;
3439
+ }
3440
+ const partialCost = emptyCost();
3441
+ const partialTrace = buildPartialTrace({
3442
+ childRunId,
3443
+ events: [],
3444
+ startedAtMs: Date.now(),
3445
+ protocol: delegate.protocol,
3446
+ tier: options.tier,
3447
+ modelProviderId: options.model.id,
3448
+ agents: options.agents,
3449
+ intent: delegate.intent,
3450
+ temperature: options.temperature,
3451
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
3452
+ });
3453
+ const failedEvent = {
3454
+ type: "sub-run-failed",
3455
+ runId,
3456
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3457
+ childRunId,
3458
+ parentRunId: runId,
3459
+ parentDecisionId,
3460
+ parentDecisionArrayIndex: index,
3461
+ error: {
3462
+ code: "aborted",
3463
+ message: "Sibling delegate failed; queued delegate never started.",
3464
+ detail: { reason: "sibling-failed" }
3465
+ },
3466
+ partialTrace,
3467
+ partialCost
3468
+ };
3469
+ emit(failedEvent);
3470
+ recordProtocolDecision(failedEvent);
3471
+ dispatchedChild.closed = true;
3472
+ dispatchResults.push({
3473
+ index,
3474
+ result: {
3475
+ nextInput: "",
3476
+ taggedText: `[sub-run ${childRunId}]: skipped because a sibling delegate failed`,
3477
+ completedAtMs: Date.now()
3478
+ }
3479
+ });
3480
+ return;
3481
+ }
3482
+ const result = await dispatchDelegate({
3483
+ decision: delegate,
3484
+ childRunId,
3485
+ parentDecisionId,
3486
+ parentDecisionArrayIndex: index,
3487
+ parentDepth,
3488
+ parentRunId: runId,
3489
+ options,
3490
+ transcript,
3491
+ emit,
3492
+ recordProtocolDecision,
3493
+ recordSubRunCost: (cost) => {
3494
+ totalCost = addCost(totalCost, cost);
3495
+ },
3496
+ dispatchedChild
3497
+ });
3498
+ dispatchResults.push({
3499
+ index,
3500
+ result
3501
+ });
3502
+ } catch (error) {
3503
+ firstFailureIndex ??= index;
3504
+ const failure = dispatchedForTurn[index]?.failure;
3505
+ if (delegates.length === 1 && (options.onChildFailure === "abort" || failure === void 0 || isDelegateValidationError(error))) throw error;
3506
+ let taggedText = `[sub-run ${childRunId} failed]: ${error instanceof Error ? error.message : String(error)}`;
3507
+ if (failure) {
3508
+ const error = failure.error;
3509
+ taggedText = `[sub-run ${childRunId} failed | code=${error.code} | spent=$${failure.partialCost.usd.toFixed(3)}]: ${error.message}`;
3510
+ }
3511
+ dispatchResults.push({
3512
+ index,
3513
+ result: {
3514
+ nextInput: "",
3515
+ taggedText,
3516
+ completedAtMs: Date.now()
3517
+ }
3518
+ });
3519
+ } finally {
3520
+ semaphore.release();
3521
+ }
3522
+ });
3523
+ const firstRejected = (await Promise.allSettled(tasks)).find((result) => result.status === "rejected");
3524
+ if (firstRejected?.status === "rejected" && delegates.length === 1 && (options.onChildFailure === "abort" || dispatchResults.length === 0)) throw firstRejected.reason;
3525
+ dispatchResults.sort((a, b) => a.result.completedAtMs - b.result.completedAtMs);
3526
+ const taggedResults = dispatchResults.map((entry) => entry.result.taggedText).join("\n\n");
3527
+ const currentWaveFailures = dispatchedForTurn.map((child) => child.failure).filter((failure) => failure !== void 0);
3528
+ if (options.onChildFailure === "abort" && currentWaveFailures.length > 0) {
3529
+ triggeringFailureForAbortMode ??= currentWaveFailures[0];
3530
+ break;
3531
+ }
3532
+ const failuresSection = buildFailuresSection(currentWaveFailures);
3533
+ const coordinatorAgent = options.agents[0] ?? {
3534
+ id: "coordinator",
3535
+ role: "coordinator"
3536
+ };
3537
+ dispatchInput = [
3538
+ buildCoordinatorPlanInput(options.intent, coordinatorAgent),
3539
+ taggedResults,
3540
+ failuresSection,
3541
+ "Using the sub-run results above, decide the next step (participate or delegate)."
3542
+ ].filter((section) => Boolean(section)).join("\n\n");
3543
+ dispatchCount += delegates.length;
3544
+ }
2687
3545
  stopIfNeeded();
2688
3546
  }
2689
3547
  if (!stopIfNeeded()) {
@@ -2741,7 +3599,7 @@ async function runCoordinator(options) {
2741
3599
  stopIfNeeded();
2742
3600
  }
2743
3601
  if (!stopIfNeeded()) {
2744
- totalCost = await runCoordinatorTurn({
3602
+ const synthesisOutcome = await runCoordinatorTurn({
2745
3603
  agent: coordinator,
2746
3604
  coordinator,
2747
3605
  input: buildFinalSynthesisInput(options.intent, transcript, coordinator),
@@ -2759,6 +3617,17 @@ async function runCoordinator(options) {
2759
3617
  emit,
2760
3618
  recordProtocolDecision
2761
3619
  });
3620
+ totalCost = synthesisOutcome.totalCost;
3621
+ if (Array.isArray(synthesisOutcome.decision) || synthesisOutcome.decision?.type === "delegate") throw new DogpileError({
3622
+ code: "invalid-configuration",
3623
+ message: "Coordinator final-synthesis turn cannot emit a delegate decision in Phase 1",
3624
+ retryable: false,
3625
+ detail: {
3626
+ kind: "delegate-validation",
3627
+ path: "decision",
3628
+ phase: "final-synthesis"
3629
+ }
3630
+ });
2762
3631
  stopIfNeeded();
2763
3632
  }
2764
3633
  }
@@ -2811,6 +3680,7 @@ async function runCoordinator(options) {
2811
3680
  cost: totalCost,
2812
3681
  transcript: createTranscriptLink(transcript)
2813
3682
  }),
3683
+ ...triggeringFailureForAbortMode !== void 0 ? { triggeringFailureForAbortMode } : {},
2814
3684
  events,
2815
3685
  transcript
2816
3686
  },
@@ -2868,6 +3738,9 @@ async function runCoordinator(options) {
2868
3738
  recordProtocolDecision(event, { transcriptEntryCount: transcript.length });
2869
3739
  }
2870
3740
  }
3741
+ function isDelegateValidationError(error) {
3742
+ return DogpileError.isInstance(error) && error.code === "invalid-configuration" && error.detail?.["kind"] === "delegate-validation";
3743
+ }
2871
3744
  async function runCoordinatorTurn(turn) {
2872
3745
  throwIfAborted(turn.options.signal, turn.options.model.id);
2873
3746
  const request = {
@@ -2911,7 +3784,11 @@ async function runCoordinatorTurn(turn) {
2911
3784
  turn.providerCalls.push(call);
2912
3785
  }
2913
3786
  });
2914
- const decision = parseAgentDecision(response.text);
3787
+ const decision = parseAgentDecision(response.text, {
3788
+ parentProviderId: turn.options.model.id,
3789
+ currentDepth: turn.options.currentDepth ?? 0,
3790
+ maxDepth: turn.options.effectiveMaxDepth ?? Number.POSITIVE_INFINITY
3791
+ });
2915
3792
  const totalCost = addCost(turn.totalCost, responseCost$2(response));
2916
3793
  const toolCalls = await executeModelResponseToolRequests({
2917
3794
  response,
@@ -2947,7 +3824,10 @@ async function runCoordinatorTurn(turn) {
2947
3824
  phase: turn.phase,
2948
3825
  transcriptEntryCount: turn.transcript.length
2949
3826
  });
2950
- return totalCost;
3827
+ return {
3828
+ totalCost,
3829
+ decision
3830
+ };
2951
3831
  }
2952
3832
  async function runCoordinatorWorkerTurn(turn) {
2953
3833
  throwIfAborted(turn.options.signal, turn.options.model.id);
@@ -2992,7 +3872,21 @@ async function runCoordinatorWorkerTurn(turn) {
2992
3872
  turn.providerCallSlots[turn.providerCallIndex] = call;
2993
3873
  }
2994
3874
  });
2995
- const decision = parseAgentDecision(response.text);
3875
+ const decision = parseAgentDecision(response.text, {
3876
+ parentProviderId: turn.options.model.id,
3877
+ currentDepth: turn.options.currentDepth ?? 0,
3878
+ maxDepth: turn.options.effectiveMaxDepth ?? Number.POSITIVE_INFINITY
3879
+ });
3880
+ if (Array.isArray(decision) || decision?.type === "delegate") throw new DogpileError({
3881
+ code: "invalid-configuration",
3882
+ message: "Workers cannot emit delegate decisions in Phase 1",
3883
+ retryable: false,
3884
+ detail: {
3885
+ kind: "delegate-validation",
3886
+ path: "decision",
3887
+ phase: "worker"
3888
+ }
3889
+ });
2996
3890
  const toolCalls = await executeModelResponseToolRequests({
2997
3891
  response,
2998
3892
  executor: turn.toolExecutor,
@@ -3019,6 +3913,30 @@ function buildSystemPrompt$2(agent, coordinator) {
3019
3913
  function buildCoordinatorPlanInput(intent, coordinator) {
3020
3914
  return `Mission: ${intent}\nCoordinator ${coordinator.id}: assign the work, name the plan, and provide the first contribution.`;
3021
3915
  }
3916
+ function buildFailuresSection(failures) {
3917
+ if (failures.length === 0) return null;
3918
+ return [
3919
+ "## Sub-run failures since last decision",
3920
+ "",
3921
+ "```json",
3922
+ JSON.stringify(failures, null, 2),
3923
+ "```"
3924
+ ].join("\n");
3925
+ }
3926
+ function dispatchWaveFailureFromEvent(intent, event) {
3927
+ const reason = typeof event.error.detail?.["reason"] === "string" ? event.error.detail["reason"] : void 0;
3928
+ if (reason === "sibling-failed" || reason === "parent-aborted") return;
3929
+ return {
3930
+ childRunId: event.childRunId,
3931
+ intent,
3932
+ error: {
3933
+ code: event.error.code,
3934
+ message: event.error.message,
3935
+ ...reason !== void 0 ? { detail: { reason } } : {}
3936
+ },
3937
+ partialCost: { usd: event.partialCost.usd }
3938
+ };
3939
+ }
3022
3940
  function buildWorkerInput(intent, transcript, coordinator) {
3023
3941
  const prior = transcript.map((entry) => `${entry.role} (${entry.agentId}): ${entry.output}`).join("\n\n");
3024
3942
  return `Mission: ${intent}\n\nCoordinator: ${coordinator.id}\nPrior contributions:\n${prior}\n\nFollow the coordinator-managed plan and provide your assigned contribution.`;
@@ -3035,6 +3953,382 @@ function responseCost$2(response) {
3035
3953
  totalTokens: response.usage?.totalTokens ?? 0
3036
3954
  };
3037
3955
  }
3956
+ /**
3957
+ * Dispatch a single delegate decision as a recursive sub-run.
3958
+ *
3959
+ * D-11: child reuses the parent provider object verbatim.
3960
+ * D-16: `recursive: true` flag set when both parent and child protocol are
3961
+ * `coordinator`.
3962
+ * D-17: tagged result text appended to the next coordinator prompt.
3963
+ * D-18: synthetic transcript entry pushed for replay/provenance.
3964
+ *
3965
+ * On thrown error from the child engine, builds `partialTrace` from a locally
3966
+ * tee'd `childEvents` buffer — `runProtocol`'s error contract is unchanged.
3967
+ */
3968
+ async function dispatchDelegate(input) {
3969
+ const { decision, options } = input;
3970
+ if (options.effectiveMaxDepth !== void 0) assertDepthWithinLimit(input.parentDepth, options.effectiveMaxDepth);
3971
+ const childRunId = input.childRunId ?? createRunId();
3972
+ const recursive = decision.protocol === "coordinator";
3973
+ const decisionTimeoutMs = decision.budget?.timeoutMs;
3974
+ const parentDeadlineMs = options.parentDeadlineMs;
3975
+ const remainingMs = parentDeadlineMs !== void 0 ? Math.max(0, parentDeadlineMs - Date.now()) : void 0;
3976
+ if (parentDeadlineMs !== void 0 && remainingMs === 0) throw new DogpileError({
3977
+ code: "aborted",
3978
+ message: "Parent deadline elapsed before sub-run dispatch.",
3979
+ retryable: false,
3980
+ providerId: options.model.id,
3981
+ detail: { reason: "timeout" }
3982
+ });
3983
+ let childTimeoutMs;
3984
+ let clampedFrom;
3985
+ if (remainingMs !== void 0) if (decisionTimeoutMs !== void 0) if (decisionTimeoutMs > remainingMs) {
3986
+ clampedFrom = decisionTimeoutMs;
3987
+ childTimeoutMs = remainingMs;
3988
+ } else childTimeoutMs = decisionTimeoutMs;
3989
+ else childTimeoutMs = remainingMs;
3990
+ else if (decisionTimeoutMs !== void 0) childTimeoutMs = decisionTimeoutMs;
3991
+ else if (options.defaultSubRunTimeoutMs !== void 0) childTimeoutMs = options.defaultSubRunTimeoutMs;
3992
+ if (!options.runProtocol) throw new DogpileError({
3993
+ code: "invalid-configuration",
3994
+ message: "Coordinator delegate dispatch requires the engine `runProtocol` callback. Use `Dogpile.run` / `createEngine` rather than calling `runCoordinator` directly when delegate is in play.",
3995
+ retryable: false,
3996
+ detail: {
3997
+ kind: "delegate-validation",
3998
+ path: "runProtocol"
3999
+ }
4000
+ });
4001
+ const childEvents = input.dispatchedChild.childEvents;
4002
+ const parentEmit = input.emit;
4003
+ const teedEmit = (event) => {
4004
+ childEvents.push(event);
4005
+ if (input.dispatchedChild.closed) return;
4006
+ if (options.streamEvents && options.emit) {
4007
+ const inbound = event.parentRunIds;
4008
+ options.emit({
4009
+ ...event,
4010
+ parentRunIds: [input.parentRunId, ...inbound ?? []]
4011
+ });
4012
+ }
4013
+ };
4014
+ const childStartedAt = Date.now();
4015
+ input.dispatchedChild.startedAtMs = childStartedAt;
4016
+ if (clampedFrom !== void 0 && childTimeoutMs !== void 0) {
4017
+ const clampEvent = {
4018
+ type: "sub-run-budget-clamped",
4019
+ runId: input.parentRunId,
4020
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4021
+ childRunId,
4022
+ parentRunId: input.parentRunId,
4023
+ parentDecisionId: input.parentDecisionId,
4024
+ requestedTimeoutMs: clampedFrom,
4025
+ clampedTimeoutMs: childTimeoutMs,
4026
+ reason: "exceeded-parent-remaining"
4027
+ };
4028
+ input.emit(clampEvent);
4029
+ input.recordProtocolDecision(clampEvent);
4030
+ }
4031
+ const startEvent = {
4032
+ type: "sub-run-started",
4033
+ runId: input.parentRunId,
4034
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4035
+ childRunId,
4036
+ parentRunId: input.parentRunId,
4037
+ parentDecisionId: input.parentDecisionId,
4038
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
4039
+ protocol: decision.protocol,
4040
+ intent: decision.intent,
4041
+ depth: input.parentDepth + 1,
4042
+ ...recursive ? { recursive: true } : {}
4043
+ };
4044
+ parentEmit(startEvent);
4045
+ input.recordProtocolDecision(startEvent);
4046
+ const parentSignal = options.signal;
4047
+ let removeParentAbortListener;
4048
+ if (parentSignal !== void 0) if (parentSignal.aborted) input.dispatchedChild.controller.abort(parentSignal.reason);
4049
+ else {
4050
+ const handler = () => {
4051
+ input.dispatchedChild.controller.abort(parentSignal.reason);
4052
+ };
4053
+ parentSignal.addEventListener("abort", handler, { once: true });
4054
+ removeParentAbortListener = () => {
4055
+ parentSignal.removeEventListener("abort", handler);
4056
+ };
4057
+ }
4058
+ input.dispatchedChild.removeParentListener = removeParentAbortListener;
4059
+ input.dispatchedChild.started = true;
4060
+ input.dispatchedChild.childTimeoutMs = childTimeoutMs;
4061
+ const childDeadlineReason = childTimeoutMs !== void 0 && parentDeadlineMs === void 0 ? createEngineDeadlineTimeoutError(options.model.id, childTimeoutMs) : void 0;
4062
+ const childDeadlineTimer = childDeadlineReason !== void 0 ? setTimeout(() => {
4063
+ input.dispatchedChild.controller.abort(childDeadlineReason);
4064
+ }, childTimeoutMs) : void 0;
4065
+ const childOptions = {
4066
+ intent: decision.intent,
4067
+ protocol: decision.protocol,
4068
+ tier: options.tier,
4069
+ model: options.model,
4070
+ agents: options.agents,
4071
+ tools: options.tools,
4072
+ temperature: options.temperature,
4073
+ ...childTimeoutMs !== void 0 ? { budget: { timeoutMs: childTimeoutMs } } : {},
4074
+ signal: input.dispatchedChild.controller.signal,
4075
+ emit: teedEmit,
4076
+ ...options.streamEvents !== void 0 ? { streamEvents: options.streamEvents } : {},
4077
+ currentDepth: input.parentDepth + 1,
4078
+ ...options.effectiveMaxDepth !== void 0 ? { effectiveMaxDepth: options.effectiveMaxDepth } : {},
4079
+ ...options.effectiveMaxConcurrentChildren !== void 0 ? { effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren } : {},
4080
+ ...options.onChildFailure !== void 0 ? { onChildFailure: options.onChildFailure } : {},
4081
+ ...parentDeadlineMs !== void 0 ? { parentDeadlineMs } : {},
4082
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {}
4083
+ };
4084
+ let subResult;
4085
+ try {
4086
+ subResult = await options.runProtocol(childOptions);
4087
+ } catch (error) {
4088
+ if (childDeadlineTimer !== void 0) clearTimeout(childDeadlineTimer);
4089
+ removeParentAbortListener?.();
4090
+ if (input.dispatchedChild.closed) {
4091
+ const enrichedError = enrichAbortErrorWithParentReason(error, parentSignal);
4092
+ if (DogpileError.isInstance(enrichedError)) throw enrichedError;
4093
+ throw error;
4094
+ }
4095
+ const failedDecision = {
4096
+ type: "delegate",
4097
+ protocol: decision.protocol,
4098
+ intent: decision.intent,
4099
+ ...decision.model !== void 0 ? { model: decision.model } : {},
4100
+ ...decision.budget !== void 0 ? { budget: decision.budget } : {}
4101
+ };
4102
+ const partialTrace = buildPartialTrace({
4103
+ childRunId,
4104
+ events: childEvents,
4105
+ startedAtMs: childStartedAt,
4106
+ protocol: decision.protocol,
4107
+ tier: options.tier,
4108
+ modelProviderId: options.model.id,
4109
+ agents: options.agents,
4110
+ intent: decision.intent,
4111
+ temperature: options.temperature,
4112
+ ...childTimeoutMs !== void 0 ? { childTimeoutMs } : {},
4113
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
4114
+ });
4115
+ const enrichedError = enrichProviderTimeoutSource(enrichAbortErrorWithParentReason(error, parentSignal), {
4116
+ ...decisionTimeoutMs !== void 0 ? { decisionTimeoutMs } : {},
4117
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { engineDefaultTimeoutMs: options.defaultSubRunTimeoutMs } : {}
4118
+ });
4119
+ if (DogpileError.isInstance(enrichedError)) options.failureInstancesByChildRunId?.set(childRunId, enrichedError);
4120
+ const errorPayload = errorPayloadFromUnknown(enrichedError, failedDecision);
4121
+ const partialCost = lastCostBearingEventCost(childEvents) ?? emptyCost();
4122
+ input.recordSubRunCost(partialCost);
4123
+ const failEvent = {
4124
+ type: "sub-run-failed",
4125
+ runId: input.parentRunId,
4126
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4127
+ childRunId,
4128
+ parentRunId: input.parentRunId,
4129
+ parentDecisionId: input.parentDecisionId,
4130
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
4131
+ error: errorPayload,
4132
+ partialTrace,
4133
+ partialCost
4134
+ };
4135
+ parentEmit(failEvent);
4136
+ input.recordProtocolDecision(failEvent);
4137
+ input.dispatchedChild.closed = true;
4138
+ input.dispatchedChild.failure = dispatchWaveFailureFromEvent(decision.intent, failEvent);
4139
+ if (DogpileError.isInstance(enrichedError)) throw enrichedError;
4140
+ throw new DogpileError({
4141
+ code: "invalid-configuration",
4142
+ message: error instanceof Error ? error.message : String(error),
4143
+ retryable: false,
4144
+ detail: {
4145
+ kind: "delegate-validation",
4146
+ path: "decision",
4147
+ reason: "child-run-failed"
4148
+ }
4149
+ });
4150
+ }
4151
+ if (childDeadlineTimer !== void 0) clearTimeout(childDeadlineTimer);
4152
+ removeParentAbortListener?.();
4153
+ input.recordSubRunCost(subResult.cost);
4154
+ const completedEvent = {
4155
+ type: "sub-run-completed",
4156
+ runId: input.parentRunId,
4157
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4158
+ childRunId,
4159
+ parentRunId: input.parentRunId,
4160
+ parentDecisionId: input.parentDecisionId,
4161
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
4162
+ subResult
4163
+ };
4164
+ parentEmit(completedEvent);
4165
+ input.recordProtocolDecision(completedEvent);
4166
+ input.dispatchedChild.closed = true;
4167
+ if (parentSignal?.aborted) {
4168
+ const abortMarker = {
4169
+ type: "sub-run-parent-aborted",
4170
+ runId: input.parentRunId,
4171
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4172
+ childRunId,
4173
+ parentRunId: input.parentRunId,
4174
+ reason: "parent-aborted"
4175
+ };
4176
+ parentEmit(abortMarker);
4177
+ input.recordProtocolDecision(abortMarker);
4178
+ throw enrichAbortErrorWithParentReason(createAbortErrorFromSignal(parentSignal, options.model.id), parentSignal);
4179
+ }
4180
+ const decisionAsJson = {
4181
+ type: "delegate",
4182
+ protocol: decision.protocol,
4183
+ intent: decision.intent,
4184
+ ...decision.model !== void 0 ? { model: decision.model } : {},
4185
+ ...decision.budget !== void 0 ? { budget: decision.budget } : {}
4186
+ };
4187
+ const taggedText = renderSubRunResult(childRunId, subResult);
4188
+ input.transcript.push({
4189
+ agentId: `sub-run:${childRunId}`,
4190
+ role: "delegate-result",
4191
+ input: JSON.stringify(decisionAsJson),
4192
+ output: taggedText
4193
+ });
4194
+ const coordinatorAgent = options.agents[0];
4195
+ return {
4196
+ nextInput: `${buildCoordinatorPlanInput(input.options.intent, coordinatorAgent ?? {
4197
+ id: "coordinator",
4198
+ role: "coordinator"
4199
+ })}\n\n${taggedText}\n\nUsing the sub-run result above, decide the next step (participate or delegate).`,
4200
+ taggedText,
4201
+ completedAtMs: Date.now()
4202
+ };
4203
+ }
4204
+ /**
4205
+ * D-17 prompt-injection helper. Renders a child `RunResult` as the canonical
4206
+ * tagged-result block injected into the parent coordinator's next prompt.
4207
+ *
4208
+ * Format:
4209
+ * `[sub-run <childRunId>]: <output>`
4210
+ * `[sub-run <childRunId> stats]: turns=<N> costUsd=<X> durationMs=<Y>`
4211
+ *
4212
+ * The stats line is a soft contract — field names stable, ordering stable.
4213
+ */
4214
+ function renderSubRunResult(childRunId, subResult) {
4215
+ const turns = subResult.transcript.length;
4216
+ const costUsd = subResult.cost.usd ?? 0;
4217
+ const startedAt = subResult.trace.events[0]?.at;
4218
+ const endedAt = subResult.trace.events.at(-1)?.at;
4219
+ const durationMs = startedAt && endedAt ? Math.max(0, Date.parse(endedAt) - Date.parse(startedAt)) : 0;
4220
+ return [`[sub-run ${childRunId}]: ${subResult.output}`, `[sub-run ${childRunId} stats]: turns=${turns} costUsd=${costUsd} durationMs=${durationMs}`].join("\n");
4221
+ }
4222
+ /**
4223
+ * Build a JSON-serializable {@link Trace} for `sub-run-failed.partialTrace`
4224
+ * from a buffered tee of child emits. Keeps `runProtocol`'s error contract
4225
+ * unchanged — Plan 03 step 8.
4226
+ */
4227
+ function buildPartialTrace(input) {
4228
+ const protocolName = typeof input.protocol === "string" ? input.protocol : input.protocol.kind;
4229
+ const protocolConfig = typeof input.protocol === "string" ? { kind: input.protocol } : input.protocol;
4230
+ return {
4231
+ schemaVersion: "1.0",
4232
+ runId: input.childRunId,
4233
+ protocol: protocolName,
4234
+ tier: input.tier,
4235
+ modelProviderId: input.modelProviderId,
4236
+ agentsUsed: input.agents,
4237
+ inputs: createReplayTraceRunInputs({
4238
+ intent: input.intent,
4239
+ protocol: protocolConfig,
4240
+ tier: input.tier,
4241
+ modelProviderId: input.modelProviderId,
4242
+ agents: input.agents,
4243
+ temperature: input.temperature
4244
+ }),
4245
+ budget: createReplayTraceBudget({
4246
+ tier: input.tier,
4247
+ ...input.childTimeoutMs !== void 0 ? { caps: { timeoutMs: input.childTimeoutMs } } : {}
4248
+ }),
4249
+ budgetStateChanges: createReplayTraceBudgetStateChanges(input.events),
4250
+ seed: createReplayTraceSeed(input.seed),
4251
+ protocolDecisions: [],
4252
+ providerCalls: [],
4253
+ finalOutput: {
4254
+ kind: "replay-trace-final-output",
4255
+ output: "",
4256
+ cost: emptyCost(),
4257
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4258
+ transcript: createTranscriptLink([])
4259
+ },
4260
+ events: input.events,
4261
+ transcript: []
4262
+ };
4263
+ }
4264
+ /**
4265
+ * BUDGET-01 / D-08: when a child sub-run threw because the parent's signal
4266
+ * aborted, lock the `detail.reason` discriminator on the resulting
4267
+ * `code: "aborted"` error. Preserves any pre-existing detail keys (e.g.,
4268
+ * `detail.status: "cancelled"` attached by `createStreamCancellationError`).
4269
+ *
4270
+ * No-op when:
4271
+ * - parent.signal is undefined or not aborted (child failure was unrelated)
4272
+ * - error is not a DogpileError with `code: "aborted"`
4273
+ * - error already has a `detail.reason` set (preserve upstream classification)
4274
+ */
4275
+ function enrichAbortErrorWithParentReason(error, parentSignal) {
4276
+ if (parentSignal === void 0 || !parentSignal.aborted) return error;
4277
+ if (!DogpileError.isInstance(error) || error.code !== "aborted") return error;
4278
+ const existingDetail = error.detail ?? {};
4279
+ if (existingDetail["reason"] !== void 0) return error;
4280
+ const reason = classifyAbortReason(parentSignal.reason);
4281
+ return new DogpileError({
4282
+ code: "aborted",
4283
+ message: error.message,
4284
+ retryable: error.retryable ?? false,
4285
+ ...error.providerId !== void 0 ? { providerId: error.providerId } : {},
4286
+ detail: {
4287
+ ...existingDetail,
4288
+ reason
4289
+ },
4290
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
4291
+ });
4292
+ }
4293
+ function enrichProviderTimeoutSource(error, context) {
4294
+ if (!DogpileError.isInstance(error) || error.code !== "provider-timeout") return error;
4295
+ const existingDetail = error.detail ?? {};
4296
+ if (existingDetail["source"] !== void 0) return error;
4297
+ const source = classifyChildTimeoutSource(error, {
4298
+ ...context,
4299
+ isProviderError: true
4300
+ });
4301
+ return new DogpileError({
4302
+ code: "provider-timeout",
4303
+ message: error.message,
4304
+ retryable: error.retryable ?? true,
4305
+ ...error.providerId !== void 0 ? { providerId: error.providerId } : {},
4306
+ detail: {
4307
+ ...existingDetail,
4308
+ source
4309
+ },
4310
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
4311
+ });
4312
+ }
4313
+ function errorPayloadFromUnknown(error, failedDecision) {
4314
+ if (DogpileError.isInstance(error)) {
4315
+ const detail = {
4316
+ ...error.detail ?? {},
4317
+ failedDecision
4318
+ };
4319
+ return {
4320
+ code: error.code,
4321
+ message: error.message,
4322
+ ...error.providerId !== void 0 ? { providerId: error.providerId } : {},
4323
+ detail
4324
+ };
4325
+ }
4326
+ return {
4327
+ code: "invalid-configuration",
4328
+ message: error instanceof Error ? error.message : String(error),
4329
+ detail: { failedDecision }
4330
+ };
4331
+ }
3038
4332
  //#endregion
3039
4333
  //#region src/runtime/sequential.ts
3040
4334
  async function runSequential(options) {
@@ -3173,7 +4467,8 @@ async function runSequential(options) {
3173
4467
  });
3174
4468
  if (stopIfNeeded()) break;
3175
4469
  }
3176
- const output = [...transcript].reverse().find((entry) => isParticipatingDecision(entry.decision))?.output ?? "";
4470
+ const reversed = [...transcript].reverse();
4471
+ const output = reversed.find((entry) => isParticipatingDecision(entry.decision))?.output ?? reversed.find((entry) => entry.decision === void 0)?.output ?? "";
3177
4472
  throwIfAborted(options.signal, options.model.id);
3178
4473
  const final = {
3179
4474
  type: "final",
@@ -3573,6 +4868,8 @@ function responseCost(response) {
3573
4868
  }
3574
4869
  //#endregion
3575
4870
  //#region src/runtime/engine.ts
4871
+ var DEFAULT_MAX_DEPTH = 4;
4872
+ var DEFAULT_MAX_CONCURRENT_CHILDREN = 4;
3576
4873
  var defaultHighLevelProtocol = "sequential";
3577
4874
  var defaultHighLevelTier = "balanced";
3578
4875
  /**
@@ -3593,9 +4890,20 @@ function createEngine(options) {
3593
4890
  const temperature = options.temperature ?? tierTemperature(options.tier);
3594
4891
  const agents = orderAgentsForTemperature(options.agents ?? defaultAgents(), temperature, options.seed);
3595
4892
  const terminate = options.terminate ?? (options.budget ? conditionFromBudget(options.budget) : void 0);
4893
+ const engineMaxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
4894
+ const engineMaxConcurrentChildren = options.maxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN;
4895
+ const engineOnChildFailure = options.onChildFailure;
3596
4896
  return {
3597
- run(intent) {
4897
+ run(intent, runOptions) {
3598
4898
  validateMissionIntent(intent);
4899
+ validateRunCallOptions(runOptions);
4900
+ validateProviderLocality(options.model, "model");
4901
+ const effectiveMaxDepth = Math.min(engineMaxDepth, runOptions?.maxDepth ?? Number.POSITIVE_INFINITY);
4902
+ assertRunDoesNotRaiseEngineMax("maxConcurrentChildren", runOptions?.maxConcurrentChildren, engineMaxConcurrentChildren);
4903
+ const effectiveMaxConcurrentChildren = Math.min(engineMaxConcurrentChildren, runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY);
4904
+ const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
4905
+ const startedAtMs = Date.now();
4906
+ const parentDeadlineMs = options.budget?.timeoutMs !== void 0 ? startedAtMs + options.budget.timeoutMs : void 0;
3599
4907
  return runNonStreamingProtocol({
3600
4908
  intent,
3601
4909
  protocol,
@@ -3609,11 +4917,23 @@ function createEngine(options) {
3609
4917
  ...options.signal !== void 0 ? { signal: options.signal } : {},
3610
4918
  ...terminate ? { terminate } : {},
3611
4919
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {},
3612
- ...options.evaluate ? { evaluate: options.evaluate } : {}
4920
+ ...options.evaluate ? { evaluate: options.evaluate } : {},
4921
+ currentDepth: 0,
4922
+ effectiveMaxDepth,
4923
+ effectiveMaxConcurrentChildren,
4924
+ onChildFailure,
4925
+ ...parentDeadlineMs !== void 0 ? { parentDeadlineMs } : {},
4926
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {}
3613
4927
  });
3614
4928
  },
3615
- stream(intent) {
4929
+ stream(intent, runOptions) {
3616
4930
  validateMissionIntent(intent);
4931
+ validateRunCallOptions(runOptions);
4932
+ validateProviderLocality(options.model, "model");
4933
+ const effectiveMaxDepth = Math.min(engineMaxDepth, runOptions?.maxDepth ?? Number.POSITIVE_INFINITY);
4934
+ assertRunDoesNotRaiseEngineMax("maxConcurrentChildren", runOptions?.maxConcurrentChildren, engineMaxConcurrentChildren);
4935
+ const effectiveMaxConcurrentChildren = Math.min(engineMaxConcurrentChildren, runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY);
4936
+ const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
3617
4937
  const pendingEvents = [];
3618
4938
  const pendingResolvers = [];
3619
4939
  const emittedEvents = [];
@@ -3630,7 +4950,10 @@ function createEngine(options) {
3630
4950
  const abortRace = createAbortRace(abortController.signal, options.model.id);
3631
4951
  let complete = false;
3632
4952
  let lastRunId = "";
4953
+ let rootRunId;
3633
4954
  let pendingFinalEvent;
4955
+ let activeAbortDrain;
4956
+ const failureInstancesByChildRunId = /* @__PURE__ */ new Map();
3634
4957
  let status = "running";
3635
4958
  let resolveResult;
3636
4959
  let rejectResult;
@@ -3676,6 +4999,8 @@ function createEngine(options) {
3676
4999
  async function execute() {
3677
5000
  if (status !== "running") return;
3678
5001
  try {
5002
+ const streamStartedAtMs = Date.now();
5003
+ const streamParentDeadlineMs = options.budget?.timeoutMs !== void 0 ? streamStartedAtMs + options.budget.timeoutMs : void 0;
3679
5004
  const baseResult = await abortRace.run(runProtocol({
3680
5005
  intent,
3681
5006
  protocol,
@@ -3688,17 +5013,32 @@ function createEngine(options) {
3688
5013
  ...options.seed !== void 0 ? { seed: options.seed } : {},
3689
5014
  signal: abortController.signal,
3690
5015
  ...terminate ? { terminate } : {},
5016
+ currentDepth: 0,
5017
+ effectiveMaxDepth,
5018
+ effectiveMaxConcurrentChildren,
5019
+ onChildFailure,
5020
+ ...streamParentDeadlineMs !== void 0 ? { parentDeadlineMs: streamParentDeadlineMs } : {},
5021
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {},
5022
+ streamEvents: true,
3691
5023
  emit(event) {
3692
5024
  if (status !== "running") return;
5025
+ const parentRunIds = event.parentRunIds;
5026
+ if (rootRunId === void 0 && parentRunIds === void 0) rootRunId = event.runId;
3693
5027
  lastRunId = event.runId;
3694
- if (event.type === "final") {
5028
+ if (event.type === "final" && event.runId === rootRunId) {
3695
5029
  pendingFinalEvent = event;
3696
5030
  return;
3697
5031
  }
3698
5032
  publish(event);
3699
- }
5033
+ },
5034
+ registerAbortDrain(drain) {
5035
+ activeAbortDrain = drain;
5036
+ },
5037
+ failureInstancesByChildRunId
3700
5038
  }));
3701
5039
  if (status !== "running") return;
5040
+ const terminalThrow = resolveRuntimeTerminalThrow(baseResult.trace, failureInstancesByChildRunId);
5041
+ if (terminalThrow) throw terminalThrow;
3702
5042
  const finalizedResult = await abortRace.run(applyRunEvaluation(baseResult, options.evaluate));
3703
5043
  if (status !== "running") return;
3704
5044
  const finalEvent = finalizedResult.trace.events.at(-1);
@@ -3711,6 +5051,10 @@ function createEngine(options) {
3711
5051
  if (isStreamHandleStatus(status, "cancelled")) return;
3712
5052
  const runtimeError = timeoutLifecycle.translateError(error);
3713
5053
  status = isCancellationError(runtimeError) ? "cancelled" : "failed";
5054
+ if (shouldPublishAborted(runtimeError)) {
5055
+ activeAbortDrain?.(runtimeError);
5056
+ publish(createStreamAbortedEvent(runtimeError, lastRunId));
5057
+ }
3714
5058
  publish(createStreamErrorEvent(runtimeError, lastRunId));
3715
5059
  closeStream();
3716
5060
  rejectResult(runtimeError);
@@ -3719,15 +5063,18 @@ function createEngine(options) {
3719
5063
  function cancelRun(cause) {
3720
5064
  if (status !== "running") return;
3721
5065
  const error = createStreamCancellationError(options.model.id, cause);
3722
- status = "cancelled";
3723
5066
  abortController.abort(error);
5067
+ activeAbortDrain?.(error);
5068
+ publish(createStreamAbortedEvent(error, lastRunId));
3724
5069
  publish(createStreamErrorEvent(error, lastRunId));
5070
+ status = "cancelled";
3725
5071
  closeStream();
3726
5072
  rejectResult(error);
3727
5073
  }
3728
5074
  function closeStream() {
3729
5075
  if (complete) return;
3730
5076
  complete = true;
5077
+ failureInstancesByChildRunId.clear();
3731
5078
  removeCallerAbortListener();
3732
5079
  timeoutLifecycle.cleanup();
3733
5080
  abortRace.cleanup();
@@ -3783,7 +5130,8 @@ function createNonStreamingAbortLifecycle(options) {
3783
5130
  const timeoutLifecycle = createTimeoutAbortLifecycle({
3784
5131
  abortController,
3785
5132
  timeoutMs: options.timeoutMs,
3786
- providerId: options.providerId
5133
+ providerId: options.providerId,
5134
+ timeoutErrorSource: options.timeoutErrorSource ?? "runtime"
3787
5135
  });
3788
5136
  const abortRace = createAbortRace(abortController.signal, options.providerId);
3789
5137
  const removeCallerAbortListener = wireCallerAbortSignal(options.callerSignal, abortController, () => {
@@ -3811,7 +5159,11 @@ function createTimeoutAbortLifecycle(options) {
3811
5159
  },
3812
5160
  cleanup() {}
3813
5161
  };
3814
- const timeoutError = createTimeoutError(options.providerId, options.timeoutMs);
5162
+ const timeoutSource = classifyChildTimeoutSource(void 0, {
5163
+ ...options.timeoutErrorSource === "engine" ? { engineDefaultTimeoutMs: options.timeoutMs } : {},
5164
+ isProviderError: false
5165
+ });
5166
+ const timeoutError = options.timeoutErrorSource === "engine" && timeoutSource === "engine" ? createEngineDeadlineTimeoutError(options.providerId, options.timeoutMs) : createTimeoutError(options.providerId, options.timeoutMs);
3815
5167
  const timeoutId = setTimeout(() => {
3816
5168
  options.abortController.abort(timeoutError);
3817
5169
  }, options.timeoutMs);
@@ -3879,6 +5231,23 @@ function timeoutMsFromTermination(condition) {
3879
5231
  function readAbortSignalReason(signal) {
3880
5232
  return signal?.aborted ? signal.reason : void 0;
3881
5233
  }
5234
+ function createStreamAbortedEvent(error, runId) {
5235
+ return {
5236
+ type: "aborted",
5237
+ runId,
5238
+ at: (/* @__PURE__ */ new Date()).toISOString(),
5239
+ reason: streamAbortedReason(error)
5240
+ };
5241
+ }
5242
+ function shouldPublishAborted(error) {
5243
+ return DogpileError.isInstance(error) && (error.code === "aborted" || error.code === "timeout");
5244
+ }
5245
+ function streamAbortedReason(error) {
5246
+ if (DogpileError.isInstance(error)) {
5247
+ if (error.code === "timeout" || error.detail?.["reason"] === "timeout") return "timeout";
5248
+ }
5249
+ return "parent-aborted";
5250
+ }
3882
5251
  function createStreamErrorEvent(error, runId) {
3883
5252
  if (DogpileError.isInstance(error)) return {
3884
5253
  type: "error",
@@ -3911,10 +5280,12 @@ function dogpileErrorStreamDetail(error) {
3911
5280
  return detail;
3912
5281
  }
3913
5282
  async function runNonStreamingProtocol(options) {
5283
+ const failureInstancesByChildRunId = /* @__PURE__ */ new Map();
3914
5284
  const abortLifecycle = createNonStreamingAbortLifecycle({
3915
5285
  callerSignal: options.signal,
3916
5286
  timeoutMs: runtimeTimeoutMs(options),
3917
- providerId: options.model.id
5287
+ providerId: options.model.id,
5288
+ timeoutErrorSource: options.currentDepth !== void 0 && options.currentDepth > 0 && options.parentDeadlineMs === void 0 ? "engine" : "runtime"
3918
5289
  });
3919
5290
  try {
3920
5291
  const emittedEvents = [];
@@ -3923,7 +5294,8 @@ async function runNonStreamingProtocol(options) {
3923
5294
  ...abortLifecycle.signal !== void 0 ? { signal: abortLifecycle.signal } : {},
3924
5295
  emit(event) {
3925
5296
  emittedEvents.push(event);
3926
- }
5297
+ },
5298
+ failureInstancesByChildRunId
3927
5299
  }));
3928
5300
  const events = emittedEvents.length > 0 ? emittedEvents : result.trace.events;
3929
5301
  const trace = {
@@ -3944,10 +5316,13 @@ async function runNonStreamingProtocol(options) {
3944
5316
  eventLog: createRunEventLog(trace.runId, trace.protocol, events),
3945
5317
  trace
3946
5318
  };
5319
+ const terminalThrow = resolveRuntimeTerminalThrow(runResult.trace, failureInstancesByChildRunId);
5320
+ if (terminalThrow) throw terminalThrow;
3947
5321
  return canonicalizeRunResult(await abortLifecycle.run(applyRunEvaluation(runResult, options.evaluate)));
3948
5322
  } catch (error) {
3949
5323
  throw abortLifecycle.translateError(error);
3950
5324
  } finally {
5325
+ failureInstancesByChildRunId.clear();
3951
5326
  abortLifecycle.cleanup();
3952
5327
  }
3953
5328
  }
@@ -4022,7 +5397,20 @@ function runProtocol(options) {
4022
5397
  ...options.signal !== void 0 ? { signal: options.signal } : {},
4023
5398
  ...options.terminate ? { terminate: options.terminate } : {},
4024
5399
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {},
4025
- ...options.emit ? { emit: options.emit } : {}
5400
+ ...options.emit ? { emit: options.emit } : {},
5401
+ ...options.streamEvents !== void 0 ? { streamEvents: options.streamEvents } : {},
5402
+ currentDepth: options.currentDepth ?? 0,
5403
+ effectiveMaxDepth: options.effectiveMaxDepth ?? Infinity,
5404
+ effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN,
5405
+ onChildFailure: options.onChildFailure ?? "continue",
5406
+ ...options.parentDeadlineMs !== void 0 ? { parentDeadlineMs: options.parentDeadlineMs } : {},
5407
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {},
5408
+ ...options.registerAbortDrain !== void 0 ? { registerAbortDrain: options.registerAbortDrain } : {},
5409
+ ...options.failureInstancesByChildRunId !== void 0 ? { failureInstancesByChildRunId: options.failureInstancesByChildRunId } : {},
5410
+ runProtocol: (childInput) => runProtocol({
5411
+ ...childInput,
5412
+ protocol: normalizeProtocol(childInput.protocol)
5413
+ })
4026
5414
  });
4027
5415
  case "shared": return runShared({
4028
5416
  intent: options.intent,
@@ -4089,6 +5477,9 @@ function stream(options) {
4089
5477
  function replay(trace) {
4090
5478
  const cost = trace.finalOutput.cost;
4091
5479
  const lastEvent = trace.events.at(-1);
5480
+ const accounting = recomputeAccountingFromTrace(trace);
5481
+ const replayThrow = resolveReplayTerminalThrow(trace);
5482
+ if (replayThrow) throw replayThrow;
4092
5483
  const baseResult = {
4093
5484
  output: trace.finalOutput.output,
4094
5485
  eventLog: createRunEventLog(trace.runId, trace.protocol, trace.events),
@@ -4103,13 +5494,7 @@ function replay(trace) {
4103
5494
  agentsUsed: trace.agentsUsed,
4104
5495
  events: trace.events
4105
5496
  }),
4106
- accounting: createRunAccounting({
4107
- tier: trace.tier,
4108
- ...trace.budget.caps ? { budget: trace.budget.caps } : {},
4109
- ...trace.budget.termination ? { termination: trace.budget.termination } : {},
4110
- cost,
4111
- events: trace.events
4112
- }),
5497
+ accounting,
4113
5498
  cost
4114
5499
  };
4115
5500
  if (lastEvent?.type !== "final") return baseResult;
@@ -4119,6 +5504,64 @@ function replay(trace) {
4119
5504
  ...lastEvent.evaluation !== void 0 ? { evaluation: lastEvent.evaluation } : {}
4120
5505
  };
4121
5506
  }
5507
+ function resolveRuntimeTerminalThrow(trace, failureInstancesByChildRunId) {
5508
+ if (trace.triggeringFailureForAbortMode !== void 0) return failureInstancesByChildRunId.get(trace.triggeringFailureForAbortMode.childRunId) ?? null;
5509
+ const finalEvent = trace.events.at(-1);
5510
+ if (finalEvent?.type !== "final" || finalEvent.termination === void 0) return null;
5511
+ const lastFailure = findLastRealFailure(trace.events, failureInstancesByChildRunId);
5512
+ if (lastFailure === null) return null;
5513
+ if (hasFinalSynthesisAfterEvent(trace, lastFailure.eventIndex)) return null;
5514
+ return lastFailure.error;
5515
+ }
5516
+ function findLastRealFailure(events, failureInstancesByChildRunId) {
5517
+ for (let index = events.length - 1; index >= 0; index -= 1) {
5518
+ const event = events[index];
5519
+ if (event?.type !== "sub-run-failed") continue;
5520
+ const instance = failureInstancesByChildRunId.get(event.childRunId);
5521
+ if (instance) return {
5522
+ error: instance,
5523
+ eventIndex: index
5524
+ };
5525
+ }
5526
+ return null;
5527
+ }
5528
+ function resolveReplayTerminalThrow(trace) {
5529
+ if (trace.triggeringFailureForAbortMode !== void 0) return dogpileErrorFromSerializedPayload(trace.triggeringFailureForAbortMode.error);
5530
+ const finalEvent = trace.events.at(-1);
5531
+ if (finalEvent?.type !== "final" || finalEvent.termination === void 0) return null;
5532
+ const lastFailure = reconstructLastRealFailure(trace.events);
5533
+ if (lastFailure === null) return null;
5534
+ if (hasFinalSynthesisAfterEvent(trace, lastFailure.eventIndex)) return null;
5535
+ return lastFailure.error;
5536
+ }
5537
+ function reconstructLastRealFailure(events) {
5538
+ for (let index = events.length - 1; index >= 0; index -= 1) {
5539
+ const event = events[index];
5540
+ if (event?.type !== "sub-run-failed" || isSyntheticSubRunFailure(event)) continue;
5541
+ return {
5542
+ error: dogpileErrorFromSerializedPayload(event.error),
5543
+ eventIndex: index
5544
+ };
5545
+ }
5546
+ return null;
5547
+ }
5548
+ function hasFinalSynthesisAfterEvent(trace, eventIndex) {
5549
+ return trace.protocolDecisions.some((decision) => {
5550
+ return decision.phase === "final-synthesis" && decision.eventIndex > eventIndex;
5551
+ });
5552
+ }
5553
+ function isSyntheticSubRunFailure(event) {
5554
+ const reason = event.error.detail?.["reason"];
5555
+ return reason === "sibling-failed" || reason === "parent-aborted";
5556
+ }
5557
+ function dogpileErrorFromSerializedPayload(input) {
5558
+ return new DogpileError({
5559
+ code: input.code,
5560
+ message: input.message,
5561
+ ...input.providerId !== void 0 ? { providerId: input.providerId } : {},
5562
+ ...input.detail !== void 0 ? { detail: input.detail } : {}
5563
+ });
5564
+ }
4122
5565
  /**
4123
5566
  * Replay a saved completed trace as a stream without invoking a model provider.
4124
5567
  *
@@ -4129,20 +5572,22 @@ function replay(trace) {
4129
5572
  * replay remains storage-free and provider-free.
4130
5573
  */
4131
5574
  function replayStream(trace) {
5575
+ const result = Promise.resolve(replay(trace));
5576
+ const replayEvents = replayStreamEvents(trace);
4132
5577
  return {
4133
5578
  get status() {
4134
5579
  return "completed";
4135
5580
  },
4136
- result: Promise.resolve(replay(trace)),
5581
+ result,
4137
5582
  cancel() {},
4138
5583
  subscribe(subscriber) {
4139
- for (const event of trace.events) subscriber(event);
5584
+ for (const event of replayEvents) subscriber(event);
4140
5585
  return { unsubscribe() {} };
4141
5586
  },
4142
5587
  [Symbol.asyncIterator]() {
4143
5588
  let index = 0;
4144
5589
  return { next() {
4145
- const event = trace.events[index];
5590
+ const event = replayEvents[index];
4146
5591
  if (event) {
4147
5592
  index += 1;
4148
5593
  return Promise.resolve({
@@ -4158,6 +5603,22 @@ function replayStream(trace) {
4158
5603
  }
4159
5604
  };
4160
5605
  }
5606
+ function replayStreamEvents(trace, parentRunIds = []) {
5607
+ const events = [];
5608
+ for (const event of trace.events) {
5609
+ if (event.type === "sub-run-completed") events.push(...replayStreamEvents(event.subResult.trace, [...parentRunIds, trace.runId]));
5610
+ events.push(wrapReplayStreamEvent(event, parentRunIds));
5611
+ }
5612
+ return events;
5613
+ }
5614
+ function wrapReplayStreamEvent(event, parentRunIds) {
5615
+ if (parentRunIds.length === 0) return event;
5616
+ const inbound = event.parentRunIds;
5617
+ return {
5618
+ ...event,
5619
+ parentRunIds: [...parentRunIds, ...inbound ?? []]
5620
+ };
5621
+ }
4161
5622
  function wireCallerAbortSignal(callerSignal, abortController, cancelRun) {
4162
5623
  if (!callerSignal) return () => {};
4163
5624
  const cancelFromCaller = () => {
@@ -4181,7 +5642,10 @@ function createStreamCancellationError(providerId, cause) {
4181
5642
  retryable: false,
4182
5643
  providerId,
4183
5644
  ...cause !== void 0 ? { cause } : {},
4184
- detail: { status: "cancelled" }
5645
+ detail: {
5646
+ status: "cancelled",
5647
+ reason: "parent-aborted"
5648
+ }
4185
5649
  });
4186
5650
  }
4187
5651
  function isCancellationError(error) {
@@ -4195,6 +5659,20 @@ function withHighLevelDefaults(options) {
4195
5659
  tier: options.tier ?? defaultHighLevelTier
4196
5660
  };
4197
5661
  }
5662
+ function assertRunDoesNotRaiseEngineMax(path, runValue, engineValue) {
5663
+ if (runValue === void 0 || runValue <= engineValue) return;
5664
+ throw new DogpileError({
5665
+ code: "invalid-configuration",
5666
+ message: `${path} cannot raise the engine ceiling (${engineValue}).`,
5667
+ retryable: false,
5668
+ detail: {
5669
+ kind: "configuration-validation",
5670
+ path,
5671
+ expected: `integer <= ${engineValue}`,
5672
+ actual: runValue
5673
+ }
5674
+ });
5675
+ }
4198
5676
  /**
4199
5677
  * Branded high-level SDK namespace.
4200
5678
  *
@@ -4221,6 +5699,8 @@ function createOpenAICompatibleProvider(options) {
4221
5699
  validateOptions(options);
4222
5700
  const providerId = options.id ?? `openai-compatible:${options.model}`;
4223
5701
  const fetchImplementation = options.fetch ?? globalThis.fetch?.bind(globalThis);
5702
+ const detectedLocality = classifyHostLocality(new URL(String(options.baseURL ?? defaultBaseURL)).hostname);
5703
+ const resolvedLocality = options.locality === "local" ? "local" : options.locality === "remote" ? "remote" : detectedLocality;
4224
5704
  if (!fetchImplementation) throw new DogpileError({
4225
5705
  code: "invalid-configuration",
4226
5706
  message: "createOpenAICompatibleProvider() requires a fetch implementation in this runtime.",
@@ -4234,6 +5714,7 @@ function createOpenAICompatibleProvider(options) {
4234
5714
  });
4235
5715
  return {
4236
5716
  id: providerId,
5717
+ metadata: { locality: resolvedLocality },
4237
5718
  async generate(request) {
4238
5719
  let response;
4239
5720
  try {
@@ -4246,9 +5727,11 @@ function createOpenAICompatibleProvider(options) {
4246
5727
  } catch (error) {
4247
5728
  throw normalizeFetchError(error, providerId);
4248
5729
  }
4249
- const payload = await readJson(response, providerId);
4250
- if (!response.ok) throw createProviderError(response, payload, providerId);
4251
- const completion = asChatCompletionResponse(payload, providerId);
5730
+ if (!response.ok) {
5731
+ const payload = await readJsonLenient(response);
5732
+ throw createProviderError(response, payload, providerId);
5733
+ }
5734
+ const completion = asChatCompletionResponse(await readJson(response, providerId), providerId);
4252
5735
  const text = readAssistantText(completion, providerId);
4253
5736
  const usage = normalizeUsage(completion.usage);
4254
5737
  const finishReason = normalizeFinishReason(completion.choices?.[0]?.finish_reason);
@@ -4277,6 +5760,22 @@ function validateOptions(options) {
4277
5760
  if (options.fetch !== void 0 && typeof options.fetch !== "function") throwInvalid("fetch", "a fetch-compatible function when provided");
4278
5761
  if (options.maxOutputTokens !== void 0 && (!Number.isInteger(options.maxOutputTokens) || options.maxOutputTokens <= 0)) throwInvalid("maxOutputTokens", "a positive integer when provided");
4279
5762
  if (options.costEstimator !== void 0 && typeof options.costEstimator !== "function") throwInvalid("costEstimator", "a function when provided");
5763
+ if (options.locality !== void 0 && options.locality !== "local" && options.locality !== "remote") throwInvalid("locality", "\"local\" | \"remote\" when provided");
5764
+ if (options.locality === "remote") {
5765
+ const baseURL = new URL(String(options.baseURL ?? defaultBaseURL));
5766
+ if (classifyHostLocality(baseURL.hostname) === "local") throw new DogpileError({
5767
+ code: "invalid-configuration",
5768
+ message: `locality "remote" cannot be set when baseURL resolves to a local host (${baseURL.hostname}).`,
5769
+ retryable: false,
5770
+ detail: {
5771
+ kind: "configuration-validation",
5772
+ path: "locality",
5773
+ expected: "\"local\" (or omit to auto-detect)",
5774
+ reason: "remote-override-on-local-host",
5775
+ host: baseURL.hostname
5776
+ }
5777
+ });
5778
+ }
4280
5779
  }
4281
5780
  function throwInvalid(path, expected) {
4282
5781
  throw new DogpileError({
@@ -4290,6 +5789,38 @@ function throwInvalid(path, expected) {
4290
5789
  }
4291
5790
  });
4292
5791
  }
5792
+ /**
5793
+ * Classify a URL hostname as "local" or "remote" per Phase 3 D-02.
5794
+ * Local: localhost, *.local mDNS, IPv4 loopback (127.0.0.0/8), RFC1918
5795
+ * (10/8, 172.16/12, 192.168/16), link-local (169.254/16), IPv6 loopback (::1),
5796
+ * IPv6 ULA (fc00::/7), IPv6 link-local (fe80::/10).
5797
+ *
5798
+ * Pure function: no I/O, no side effects. Exported for tests and future reuse.
5799
+ */
5800
+ function classifyHostLocality(host) {
5801
+ const lower = host.toLowerCase().replace(/^\[|\]$/g, "");
5802
+ const mappedIpv4 = ipv4MappedToDottedQuad(lower);
5803
+ if (mappedIpv4 !== void 0) return classifyHostLocality(mappedIpv4);
5804
+ if (lower === "localhost") return "local";
5805
+ if (lower.endsWith(".local")) return "local";
5806
+ if (/^127(?:\.\d{1,3}){3}$/.test(lower)) return "local";
5807
+ if (/^10(?:\.\d{1,3}){3}$/.test(lower)) return "local";
5808
+ if (/^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/.test(lower)) return "local";
5809
+ if (/^192\.168(?:\.\d{1,3}){2}$/.test(lower)) return "local";
5810
+ if (/^169\.254(?:\.\d{1,3}){2}$/.test(lower)) return "local";
5811
+ if (lower === "::1") return "local";
5812
+ if (/^f[cd][0-9a-f]{2}:/.test(lower)) return "local";
5813
+ if (/^fe[89ab][0-9a-f]?:/.test(lower)) return "local";
5814
+ return "remote";
5815
+ }
5816
+ function ipv4MappedToDottedQuad(host) {
5817
+ const match = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
5818
+ if (match === null) return;
5819
+ const high = Number.parseInt(match[1] ?? "", 16);
5820
+ const low = Number.parseInt(match[2] ?? "", 16);
5821
+ if (!Number.isFinite(high) || !Number.isFinite(low)) return;
5822
+ return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
5823
+ }
4293
5824
  function createURL(options) {
4294
5825
  const baseURL = new URL(String(options.baseURL ?? defaultBaseURL));
4295
5826
  const path = options.path ?? defaultPath;
@@ -4339,6 +5870,13 @@ async function readJson(response, providerId) {
4339
5870
  });
4340
5871
  }
4341
5872
  }
5873
+ async function readJsonLenient(response) {
5874
+ try {
5875
+ return await response.json();
5876
+ } catch {
5877
+ return;
5878
+ }
5879
+ }
4342
5880
  function asChatCompletionResponse(payload, providerId) {
4343
5881
  if (!isRecord(payload)) throw new DogpileError({
4344
5882
  code: "provider-invalid-response",
@@ -4409,14 +5947,17 @@ function responseMetadata(response) {
4409
5947
  });
4410
5948
  }
4411
5949
  function createProviderError(response, payload, providerId) {
5950
+ const code = codeForStatus(response.status);
5951
+ const timeoutSource = code === "provider-timeout" ? { source: "provider" } : {};
4412
5952
  return new DogpileError({
4413
- code: codeForStatus(response.status),
5953
+ code,
4414
5954
  message: providerResponseErrorMessage(response, payload),
4415
5955
  retryable: response.status === 408 || response.status === 429 || response.status >= 500,
4416
5956
  providerId,
4417
5957
  detail: removeUndefined({
4418
5958
  statusCode: response.status,
4419
5959
  statusText: response.statusText,
5960
+ ...timeoutSource,
4420
5961
  response: isJsonValue(payload) ? payload : void 0
4421
5962
  })
4422
5963
  });