@dogpile/sdk 0.3.1 → 0.5.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 (101) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +1 -0
  3. package/dist/browser/index.js +2328 -237
  4. package/dist/browser/index.js.map +1 -1
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/providers/openai-compatible.d.ts +11 -0
  10. package/dist/providers/openai-compatible.d.ts.map +1 -1
  11. package/dist/providers/openai-compatible.js +88 -2
  12. package/dist/providers/openai-compatible.js.map +1 -1
  13. package/dist/runtime/audit.d.ts +42 -0
  14. package/dist/runtime/audit.d.ts.map +1 -0
  15. package/dist/runtime/audit.js +73 -0
  16. package/dist/runtime/audit.js.map +1 -0
  17. package/dist/runtime/broadcast.d.ts.map +1 -1
  18. package/dist/runtime/broadcast.js +39 -36
  19. package/dist/runtime/broadcast.js.map +1 -1
  20. package/dist/runtime/cancellation.d.ts +26 -0
  21. package/dist/runtime/cancellation.d.ts.map +1 -1
  22. package/dist/runtime/cancellation.js +38 -1
  23. package/dist/runtime/cancellation.js.map +1 -1
  24. package/dist/runtime/coordinator.d.ts +79 -1
  25. package/dist/runtime/coordinator.d.ts.map +1 -1
  26. package/dist/runtime/coordinator.js +979 -61
  27. package/dist/runtime/coordinator.js.map +1 -1
  28. package/dist/runtime/decisions.d.ts +25 -3
  29. package/dist/runtime/decisions.d.ts.map +1 -1
  30. package/dist/runtime/decisions.js +241 -3
  31. package/dist/runtime/decisions.js.map +1 -1
  32. package/dist/runtime/defaults.d.ts +37 -1
  33. package/dist/runtime/defaults.d.ts.map +1 -1
  34. package/dist/runtime/defaults.js +359 -4
  35. package/dist/runtime/defaults.js.map +1 -1
  36. package/dist/runtime/engine.d.ts +17 -4
  37. package/dist/runtime/engine.d.ts.map +1 -1
  38. package/dist/runtime/engine.js +770 -35
  39. package/dist/runtime/engine.js.map +1 -1
  40. package/dist/runtime/health.d.ts +51 -0
  41. package/dist/runtime/health.d.ts.map +1 -0
  42. package/dist/runtime/health.js +85 -0
  43. package/dist/runtime/health.js.map +1 -0
  44. package/dist/runtime/introspection.d.ts +96 -0
  45. package/dist/runtime/introspection.d.ts.map +1 -0
  46. package/dist/runtime/introspection.js +31 -0
  47. package/dist/runtime/introspection.js.map +1 -0
  48. package/dist/runtime/metrics.d.ts +44 -0
  49. package/dist/runtime/metrics.d.ts.map +1 -0
  50. package/dist/runtime/metrics.js +12 -0
  51. package/dist/runtime/metrics.js.map +1 -0
  52. package/dist/runtime/model.d.ts.map +1 -1
  53. package/dist/runtime/model.js +34 -7
  54. package/dist/runtime/model.js.map +1 -1
  55. package/dist/runtime/provenance.d.ts +25 -0
  56. package/dist/runtime/provenance.d.ts.map +1 -0
  57. package/dist/runtime/provenance.js +13 -0
  58. package/dist/runtime/provenance.js.map +1 -0
  59. package/dist/runtime/sequential.d.ts.map +1 -1
  60. package/dist/runtime/sequential.js +47 -37
  61. package/dist/runtime/sequential.js.map +1 -1
  62. package/dist/runtime/shared.d.ts.map +1 -1
  63. package/dist/runtime/shared.js +39 -36
  64. package/dist/runtime/shared.js.map +1 -1
  65. package/dist/runtime/tracing.d.ts +31 -0
  66. package/dist/runtime/tracing.d.ts.map +1 -0
  67. package/dist/runtime/tracing.js +18 -0
  68. package/dist/runtime/tracing.js.map +1 -0
  69. package/dist/runtime/validation.d.ts +10 -0
  70. package/dist/runtime/validation.d.ts.map +1 -1
  71. package/dist/runtime/validation.js +73 -0
  72. package/dist/runtime/validation.js.map +1 -1
  73. package/dist/types/events.d.ts +339 -12
  74. package/dist/types/events.d.ts.map +1 -1
  75. package/dist/types/replay.d.ts +7 -1
  76. package/dist/types/replay.d.ts.map +1 -1
  77. package/dist/types.d.ts +255 -6
  78. package/dist/types.d.ts.map +1 -1
  79. package/dist/types.js.map +1 -1
  80. package/package.json +39 -1
  81. package/src/index.ts +15 -0
  82. package/src/providers/openai-compatible.ts +83 -3
  83. package/src/runtime/audit.ts +121 -0
  84. package/src/runtime/broadcast.ts +40 -37
  85. package/src/runtime/cancellation.ts +59 -1
  86. package/src/runtime/coordinator.ts +1221 -61
  87. package/src/runtime/decisions.ts +307 -4
  88. package/src/runtime/defaults.ts +389 -4
  89. package/src/runtime/engine.ts +1004 -35
  90. package/src/runtime/health.ts +136 -0
  91. package/src/runtime/introspection.ts +122 -0
  92. package/src/runtime/metrics.ts +45 -0
  93. package/src/runtime/model.ts +38 -6
  94. package/src/runtime/provenance.ts +43 -0
  95. package/src/runtime/sequential.ts +49 -38
  96. package/src/runtime/shared.ts +40 -37
  97. package/src/runtime/tracing.ts +35 -0
  98. package/src/runtime/validation.ts +81 -0
  99. package/src/types/events.ts +369 -12
  100. package/src/types/replay.ts +14 -1
  101. package/src/types.ts +279 -4
@@ -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",
@@ -241,8 +262,8 @@ function createRunMetadata(options) {
241
262
  tier: options.tier,
242
263
  modelProviderId: options.modelProviderId,
243
264
  agentsUsed: options.agentsUsed,
244
- startedAt: firstEvent?.at ?? "",
245
- completedAt: lastEvent?.at ?? ""
265
+ startedAt: eventTimestamp$1(firstEvent) ?? "",
266
+ completedAt: eventTimestamp$1(lastEvent) ?? ""
246
267
  };
247
268
  }
248
269
  function createReplayTraceRunInputs(options) {
@@ -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
  }
@@ -314,7 +342,7 @@ function createReplayTraceProtocolDecision(protocol, event, eventIndex, options
314
342
  eventType: event.type,
315
343
  protocol,
316
344
  decision: options.decision ?? defaultProtocolDecision(event),
317
- at: event.at,
345
+ at: eventTimestamp$1(event),
318
346
  ...options.turn !== void 0 ? { turn: options.turn } : {},
319
347
  ...options.phase !== void 0 ? { phase: options.phase } : {},
320
348
  ...options.round !== void 0 ? { round: options.round } : {},
@@ -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) {
@@ -422,7 +475,7 @@ function createReplayTraceFinalOutput(output, event) {
422
475
  kind: "replay-trace-final-output",
423
476
  output,
424
477
  cost: emptyCost(),
425
- completedAt: event.at,
478
+ completedAt: eventTimestamp$1(event),
426
479
  transcript: {
427
480
  kind: "trace-transcript",
428
481
  entryCount: 0,
@@ -430,6 +483,11 @@ function createReplayTraceFinalOutput(output, event) {
430
483
  }
431
484
  };
432
485
  }
486
+ function eventTimestamp$1(event) {
487
+ if (event === void 0) return void 0;
488
+ if ("at" in event) return event.at;
489
+ return event.type === "model-response" ? event.completedAt : event.startedAt;
490
+ }
433
491
  function nextProviderCallId(runId, providerCalls) {
434
492
  return `${runId}:provider-call:${providerCalls.length + 1}`;
435
493
  }
@@ -456,6 +514,7 @@ function canonicalizeRunResult(result) {
456
514
  cost: canonicalizeSerializable(result.cost),
457
515
  ...result.evaluation !== void 0 ? { evaluation: canonicalizeSerializable(result.evaluation) } : {},
458
516
  eventLog,
517
+ health: canonicalizeSerializable(result.health),
459
518
  metadata: canonicalizeSerializable(result.metadata),
460
519
  output: result.output,
461
520
  ...result.quality !== void 0 ? { quality: canonicalizeSerializable(result.quality) } : {},
@@ -467,6 +526,216 @@ function canonicalizeRunResult(result) {
467
526
  function stableJsonStringify(value) {
468
527
  return JSON.stringify(canonicalizeSerializable(value));
469
528
  }
529
+ /**
530
+ * The eight numeric fields recursively verified by `recomputeAccountingFromTrace`.
531
+ *
532
+ * These are the only summable scalars on `RunAccounting`. Non-numeric fields
533
+ * (`kind`, `tier`, `budget`, `termination`, `budgetStateChanges`) and derived
534
+ * ratios (`usdCapUtilization`, `totalTokenCapUtilization`) are NOT in this set.
535
+ */
536
+ var RECOMPUTE_FIELD_ORDER = [
537
+ "cost.usd",
538
+ "cost.inputTokens",
539
+ "cost.outputTokens",
540
+ "cost.totalTokens",
541
+ "usage.usd",
542
+ "usage.inputTokens",
543
+ "usage.outputTokens",
544
+ "usage.totalTokens"
545
+ ];
546
+ var USD_FIELDS = new Set(["cost.usd", "usage.usd"]);
547
+ var FLOAT_EPSILON = 1e-9;
548
+ function readNumericField(accounting, field) {
549
+ switch (field) {
550
+ case "cost.usd": return accounting.cost.usd;
551
+ case "cost.inputTokens": return accounting.cost.inputTokens;
552
+ case "cost.outputTokens": return accounting.cost.outputTokens;
553
+ case "cost.totalTokens": return accounting.cost.totalTokens;
554
+ case "usage.usd": return accounting.usage.usd;
555
+ case "usage.inputTokens": return accounting.usage.inputTokens;
556
+ case "usage.outputTokens": return accounting.usage.outputTokens;
557
+ case "usage.totalTokens": return accounting.usage.totalTokens;
558
+ }
559
+ }
560
+ function fieldsEqual(field, a, b) {
561
+ if (USD_FIELDS.has(field)) return Math.abs(a - b) < FLOAT_EPSILON;
562
+ return a === b;
563
+ }
564
+ function firstDifferingField(recorded, recomputed) {
565
+ for (const field of RECOMPUTE_FIELD_ORDER) {
566
+ const a = readNumericField(recorded, field);
567
+ const b = readNumericField(recomputed, field);
568
+ if (!fieldsEqual(field, a, b)) return {
569
+ field,
570
+ recorded: a,
571
+ recomputed: b
572
+ };
573
+ }
574
+ return null;
575
+ }
576
+ function buildLocalAccounting(trace) {
577
+ return createRunAccounting({
578
+ tier: trace.tier,
579
+ ...trace.budget.caps ? { budget: trace.budget.caps } : {},
580
+ ...trace.budget.termination ? { termination: trace.budget.termination } : {},
581
+ cost: trace.finalOutput.cost,
582
+ events: trace.events
583
+ });
584
+ }
585
+ function lastCostBearingEventCost(events) {
586
+ for (let index = events.length - 1; index >= 0; index -= 1) {
587
+ const event = events[index];
588
+ if (event === void 0) continue;
589
+ if (event.type === "final" || event.type === "agent-turn" || event.type === "broadcast" || event.type === "budget-stop") return event.cost;
590
+ }
591
+ return null;
592
+ }
593
+ /**
594
+ * Recompute a parent's `RunAccounting` from a saved `Trace` for replay-time
595
+ * tamper detection.
596
+ *
597
+ * @remarks
598
+ * Returns the parent's local `RunAccounting` (built the same way `replay()`
599
+ * builds it today, from `trace.finalOutput.cost` and `trace.events`). While
600
+ * walking events, every `sub-run-completed` is recursed into and the
601
+ * recomputed child accounting is compared field-by-field to the recorded
602
+ * `event.subResult.accounting`. A mismatch on any of the eight enumerated
603
+ * numeric fields throws `DogpileError({ code: "invalid-configuration" })`
604
+ * with `detail.reason: "trace-accounting-mismatch"` and a concrete
605
+ * `detail.field` identifying the first differing numeric.
606
+ *
607
+ * Pure: no provider calls, no I/O, no clock reads.
608
+ *
609
+ * Non-summed fields (`kind`, `tier`, `budget`, `termination`,
610
+ * `budgetStateChanges`) and derived ratios (`usdCapUtilization`,
611
+ * `totalTokenCapUtilization`) are not in the comparison set.
612
+ */
613
+ function recomputeAccountingFromTrace(trace) {
614
+ const local = buildLocalAccounting(trace);
615
+ const lastEventCost = lastCostBearingEventCost(trace.events);
616
+ if (lastEventCost !== null) {
617
+ const drift = firstDifferingField(local, createRunAccounting({
618
+ tier: trace.tier,
619
+ ...trace.budget.caps ? { budget: trace.budget.caps } : {},
620
+ ...trace.budget.termination ? { termination: trace.budget.termination } : {},
621
+ cost: lastEventCost,
622
+ events: trace.events
623
+ }));
624
+ if (drift !== null) throw new DogpileError({
625
+ code: "invalid-configuration",
626
+ message: `Trace accounting mismatch at parent run ${trace.runId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
627
+ retryable: false,
628
+ detail: {
629
+ kind: "trace-validation",
630
+ reason: "trace-accounting-mismatch",
631
+ eventIndex: -1,
632
+ childRunId: trace.runId,
633
+ field: drift.field,
634
+ recorded: drift.recorded,
635
+ recomputed: drift.recomputed
636
+ }
637
+ });
638
+ }
639
+ for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
640
+ const event = trace.events[eventIndex];
641
+ if (event === void 0) continue;
642
+ if (event.type === "sub-run-completed") {
643
+ const childRecordedRollup = createRunAccounting({
644
+ tier: trace.tier,
645
+ cost: event.subResult.cost,
646
+ events: []
647
+ });
648
+ const childRecordedAccounting = event.subResult.accounting;
649
+ const drift = firstDifferingField(childRecordedAccounting, childRecordedRollup);
650
+ if (drift !== null) throw new DogpileError({
651
+ code: "invalid-configuration",
652
+ message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded} on accounting, ${drift.recomputed} on subResult.cost.`,
653
+ retryable: false,
654
+ detail: {
655
+ kind: "trace-validation",
656
+ reason: "trace-accounting-mismatch",
657
+ subReason: "parent-rollup-drift",
658
+ eventIndex,
659
+ childRunId: event.childRunId,
660
+ field: drift.field,
661
+ recorded: drift.recorded,
662
+ recomputed: drift.recomputed
663
+ }
664
+ });
665
+ } else if (event.type === "sub-run-failed") {
666
+ const partialFromTrace = lastCostBearingEventCost(event.partialTrace.events) ?? emptyCost();
667
+ const drift = firstDifferingField(createRunAccounting({
668
+ tier: trace.tier,
669
+ cost: event.partialCost,
670
+ events: []
671
+ }), createRunAccounting({
672
+ tier: trace.tier,
673
+ cost: partialFromTrace,
674
+ events: []
675
+ }));
676
+ if (drift !== null) throw new DogpileError({
677
+ code: "invalid-configuration",
678
+ message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: partialCost field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed} from partialTrace events.`,
679
+ retryable: false,
680
+ detail: {
681
+ kind: "trace-validation",
682
+ reason: "trace-accounting-mismatch",
683
+ subReason: "parent-rollup-drift",
684
+ eventIndex,
685
+ childRunId: event.childRunId,
686
+ field: drift.field,
687
+ recorded: drift.recorded,
688
+ recomputed: drift.recomputed
689
+ }
690
+ });
691
+ }
692
+ }
693
+ const subRunTotal = accumulateSubRunCost(trace.events);
694
+ const parentTotal = trace.finalOutput.cost;
695
+ for (const field of RECOMPUTE_FIELD_ORDER) {
696
+ if (field.startsWith("usage.")) continue;
697
+ const [, key] = field.split(".");
698
+ const parentValue = parentTotal[key];
699
+ const childValue = subRunTotal[key];
700
+ if (childValue - parentValue > FLOAT_EPSILON) throw new DogpileError({
701
+ code: "invalid-configuration",
702
+ message: `Trace parent-rollup mismatch at run ${trace.runId}: field "${field}" Σ children ${childValue} exceeds parent recorded ${parentValue}.`,
703
+ retryable: false,
704
+ detail: {
705
+ kind: "trace-validation",
706
+ reason: "trace-accounting-mismatch",
707
+ subReason: "parent-rollup-drift",
708
+ eventIndex: -1,
709
+ childRunId: trace.runId,
710
+ field,
711
+ recorded: parentValue,
712
+ recomputed: childValue
713
+ }
714
+ });
715
+ }
716
+ for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
717
+ const event = trace.events[eventIndex];
718
+ if (event === void 0 || event.type !== "sub-run-completed") continue;
719
+ const childRecomputed = recomputeAccountingFromTrace(event.subResult.trace);
720
+ const childRecorded = event.subResult.accounting;
721
+ const drift = firstDifferingField(childRecorded, childRecomputed);
722
+ if (drift !== null) throw new DogpileError({
723
+ code: "invalid-configuration",
724
+ message: `Trace accounting mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
725
+ retryable: false,
726
+ detail: {
727
+ kind: "trace-validation",
728
+ reason: "trace-accounting-mismatch",
729
+ eventIndex,
730
+ childRunId: event.childRunId,
731
+ field: drift.field,
732
+ recorded: drift.recorded,
733
+ recomputed: drift.recomputed
734
+ }
735
+ });
736
+ }
737
+ return local;
738
+ }
470
739
  function canonicalizeSerializable(value) {
471
740
  if (Array.isArray(value)) return value.map((item) => canonicalizeSerializable(item));
472
741
  if (typeof value === "number") {
@@ -484,7 +753,96 @@ function canonicalizeSerializable(value) {
484
753
  return output;
485
754
  }
486
755
  //#endregion
756
+ //#region src/runtime/health.ts
757
+ /**
758
+ * Default health thresholds used for `result.health` auto-computation.
759
+ *
760
+ * Both threshold-gated anomalies (runaway-turns, budget-near-miss) are suppressed
761
+ * by default. Only threshold-free anomalies (empty-contribution) can fire on the
762
+ * auto-compute path.
763
+ */
764
+ var DEFAULT_HEALTH_THRESHOLDS = Object.freeze({});
765
+ /**
766
+ * Compute a health summary from a completed run trace.
767
+ *
768
+ * Pure function - no side effects, no I/O, no storage access. Deterministic:
769
+ * given the same trace and thresholds, always produces the same result.
770
+ *
771
+ * @param trace - Completed run trace (from RunResult.trace or a stored trace).
772
+ * @param thresholds - Optional threshold overrides. Defaults to DEFAULT_HEALTH_THRESHOLDS.
773
+ */
774
+ function computeHealth(trace, thresholds = DEFAULT_HEALTH_THRESHOLDS) {
775
+ assertFiniteNonNegativeThreshold(thresholds.runawayTurns, "runawayTurns");
776
+ assertBudgetNearMissThreshold(thresholds.budgetNearMissPct);
777
+ const turnEvents = trace.events.filter((event) => event.type === "agent-turn");
778
+ const agentIds = new Set(turnEvents.map((event) => event.agentId));
779
+ const totalTurns = turnEvents.length;
780
+ const agentCount = agentIds.size;
781
+ const maxUsd = trace.budget.caps?.maxUsd;
782
+ const finalCost = trace.finalOutput.cost.usd;
783
+ const budgetUtilizationPct = maxUsd !== void 0 ? maxUsd === 0 ? finalCost === 0 ? 0 : 100 : finalCost / maxUsd * 100 : null;
784
+ const anomalies = [];
785
+ if (thresholds.runawayTurns !== void 0) for (const agentId of agentIds) {
786
+ const count = turnEvents.filter((event) => event.agentId === agentId).length;
787
+ if (count > thresholds.runawayTurns) anomalies.push({
788
+ code: "runaway-turns",
789
+ severity: "error",
790
+ value: count,
791
+ threshold: thresholds.runawayTurns,
792
+ agentId
793
+ });
794
+ }
795
+ if (thresholds.budgetNearMissPct !== void 0 && budgetUtilizationPct !== null) {
796
+ if (budgetUtilizationPct >= thresholds.budgetNearMissPct) anomalies.push({
797
+ code: "budget-near-miss",
798
+ severity: "warning",
799
+ value: budgetUtilizationPct,
800
+ threshold: thresholds.budgetNearMissPct
801
+ });
802
+ }
803
+ for (const event of turnEvents) if (event.output.trim() === "") anomalies.push({
804
+ code: "empty-contribution",
805
+ severity: "error",
806
+ value: 0,
807
+ threshold: 0,
808
+ agentId: event.agentId
809
+ });
810
+ return {
811
+ anomalies,
812
+ stats: {
813
+ totalTurns,
814
+ agentCount,
815
+ budgetUtilizationPct
816
+ }
817
+ };
818
+ }
819
+ function assertFiniteNonNegativeThreshold(value, name) {
820
+ if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new RangeError(`${name} must be a finite non-negative number`);
821
+ }
822
+ function assertBudgetNearMissThreshold(value) {
823
+ assertFiniteNonNegativeThreshold(value, "budgetNearMissPct");
824
+ if (value !== void 0 && value > 100) throw new RangeError("budgetNearMissPct must be between 0 and 100");
825
+ }
826
+ //#endregion
487
827
  //#region src/runtime/cancellation.ts
828
+ /**
829
+ * Classify an abort signal's reason into the BUDGET-01 / BUDGET-02
830
+ * `detail.reason` discriminator.
831
+ *
832
+ * - `"timeout"` when the reason is a {@link DogpileError} with `code === "timeout"`
833
+ * (matches the parent-deadline abort path in `engine.ts:createTimeoutAbortLifecycle`).
834
+ * - `"parent-aborted"` for every other reason — explicit caller abort, plain
835
+ * `Error`, `undefined`, or arbitrary primitive.
836
+ */
837
+ function classifyAbortReason(signalReasonOrError) {
838
+ if (DogpileError.isInstance(signalReasonOrError) && signalReasonOrError.code === "timeout") return "timeout";
839
+ return "parent-aborted";
840
+ }
841
+ function classifyChildTimeoutSource(_error, context) {
842
+ if (context.isProviderError) return "provider";
843
+ if (context.decisionTimeoutMs !== void 0 || context.engineDefaultTimeoutMs !== void 0) return "engine";
844
+ return "provider";
845
+ }
488
846
  function throwIfAborted(signal, providerId) {
489
847
  if (!signal?.aborted) return;
490
848
  throw createAbortErrorFromSignal(signal, providerId);
@@ -501,7 +859,7 @@ function createAbortError(providerId, detail, cause) {
501
859
  }
502
860
  function createAbortErrorFromSignal(signal, providerId) {
503
861
  if (DogpileError.isInstance(signal.reason)) return signal.reason;
504
- return createAbortError(providerId, void 0, signal.reason);
862
+ return createAbortError(providerId, { reason: classifyAbortReason(signal.reason) }, signal.reason);
505
863
  }
506
864
  function createTimeoutError(providerId, timeoutMs) {
507
865
  return new DogpileError({
@@ -512,23 +870,223 @@ function createTimeoutError(providerId, timeoutMs) {
512
870
  detail: { timeoutMs }
513
871
  });
514
872
  }
873
+ function createEngineDeadlineTimeoutError(providerId, timeoutMs) {
874
+ return new DogpileError({
875
+ code: "provider-timeout",
876
+ message: `The child engine deadline expired after ${timeoutMs}ms.`,
877
+ retryable: true,
878
+ providerId,
879
+ detail: {
880
+ timeoutMs,
881
+ source: "engine"
882
+ }
883
+ });
884
+ }
515
885
  //#endregion
516
886
  //#region src/runtime/decisions.ts
517
- function parseAgentDecision(output) {
887
+ var PROTOCOL_NAMES = [
888
+ "coordinator",
889
+ "sequential",
890
+ "broadcast",
891
+ "shared"
892
+ ];
893
+ function parseAgentDecision(output, context = {}) {
894
+ const delegateBlock = matchDelegateBlock(output);
895
+ if (delegateBlock !== void 0) return parseDelegateDecision(delegateBlock, context);
896
+ return parseParticipateDecision(output);
897
+ }
898
+ function isParticipatingDecision(decision) {
899
+ if (decision === void 0 || isDelegateDecisionArray(decision) || decision.type !== "participate") return false;
900
+ return decision.participation !== "abstain";
901
+ }
902
+ function isDelegateDecisionArray(decision) {
903
+ return Array.isArray(decision);
904
+ }
905
+ function parseParticipateDecision(output) {
518
906
  const selectedRole = matchLine(output, /^role_selected:\s*(.+)$/imu);
519
907
  const participation = matchLine(output, /^participation:\s*(contribute|abstain)$/imu);
520
908
  const rationale = matchLine(output, /^rationale:\s*(.+)$/imu);
521
909
  const contribution = matchContribution(output);
522
910
  if (!selectedRole || !participation || !isAgentParticipation(participation) || !rationale || !contribution) return;
523
911
  return {
912
+ type: "participate",
524
913
  selectedRole,
525
914
  participation,
526
915
  rationale,
527
916
  contribution
528
917
  };
529
918
  }
530
- function isParticipatingDecision(decision) {
531
- return decision?.participation !== "abstain";
919
+ /**
920
+ * Locate a `delegate:` line followed by a fenced JSON block in the agent's
921
+ * output. Returns the raw JSON text inside the fence, or `undefined` when no
922
+ * delegate block is present. Tolerates ```` ```json ```` and bare ```` ``` ````.
923
+ */
924
+ function matchDelegateBlock(output) {
925
+ return output.match(/^delegate:\s*\r?\n\s*```(?:json)?\s*\r?\n([\s\S]*?)\r?\n\s*```/imu)?.[1];
926
+ }
927
+ function parseDelegateDecision(jsonText, context) {
928
+ let parsed;
929
+ try {
930
+ parsed = JSON.parse(jsonText);
931
+ } catch (error) {
932
+ throwInvalidDelegate({
933
+ path: "decision",
934
+ message: `delegate JSON did not parse: ${error instanceof Error ? error.message : String(error)}`,
935
+ expected: "valid JSON object",
936
+ received: truncate(jsonText)
937
+ });
938
+ }
939
+ if (Array.isArray(parsed)) {
940
+ if (parsed.length === 0) throwInvalidDelegate({
941
+ path: "decision",
942
+ message: "delegate array must not be empty.",
943
+ expected: "array with 1..8 delegate objects",
944
+ received: "empty array"
945
+ });
946
+ return parsed.map((item) => parseSingleDelegateObject(item, context));
947
+ }
948
+ return parseSingleDelegateObject(parsed, context);
949
+ }
950
+ function parseSingleDelegateObject(parsed, context) {
951
+ if (parsed === null || typeof parsed !== "object") throwInvalidDelegate({
952
+ path: "decision",
953
+ message: "delegate decision must be a JSON object.",
954
+ expected: "object",
955
+ received: describe(parsed)
956
+ });
957
+ const record = parsed;
958
+ const protocol = record["protocol"];
959
+ if (typeof protocol !== "string" || !PROTOCOL_NAMES.includes(protocol)) throwInvalidDelegate({
960
+ path: "decision.protocol",
961
+ message: `protocol "${describe(protocol)}" is not a known coordination protocol.`,
962
+ expected: PROTOCOL_NAMES.join(" | "),
963
+ received: describe(protocol)
964
+ });
965
+ const intentRaw = record["intent"];
966
+ const intent = typeof intentRaw === "string" ? intentRaw.trim() : "";
967
+ if (intent.length === 0) throwInvalidDelegate({
968
+ path: "decision.intent",
969
+ message: "delegate decision must include a non-empty intent string.",
970
+ expected: "non-empty string",
971
+ received: describe(intentRaw)
972
+ });
973
+ const result = {
974
+ type: "delegate",
975
+ protocol,
976
+ intent
977
+ };
978
+ if (record["model"] !== void 0) {
979
+ const model = record["model"];
980
+ if (typeof model !== "string" || model.length === 0) throwInvalidDelegate({
981
+ path: "decision.model",
982
+ message: "delegate decision model must be a non-empty string when present.",
983
+ expected: "non-empty string",
984
+ received: describe(model)
985
+ });
986
+ if (context.parentProviderId !== void 0 && model !== context.parentProviderId) throwInvalidDelegate({
987
+ path: "decision.model",
988
+ message: `delegate decision model "${model}" does not match parent provider id "${context.parentProviderId}".`,
989
+ expected: context.parentProviderId,
990
+ received: model
991
+ });
992
+ result.model = model;
993
+ }
994
+ if (record["budget"] !== void 0) result.budget = parseDelegateBudget(record["budget"]);
995
+ if (record["maxConcurrentChildren"] !== void 0) {
996
+ const value = record["maxConcurrentChildren"];
997
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) throwInvalidDelegate({
998
+ path: "decision.maxConcurrentChildren",
999
+ message: "delegate decision maxConcurrentChildren must be a positive integer when present.",
1000
+ expected: "integer >= 1",
1001
+ received: describe(value)
1002
+ });
1003
+ result.maxConcurrentChildren = value;
1004
+ }
1005
+ if (context.currentDepth !== void 0 && context.maxDepth !== void 0) {
1006
+ if (context.currentDepth + 1 > context.maxDepth) throw depthOverflowError(context.currentDepth, context.maxDepth);
1007
+ }
1008
+ return result;
1009
+ }
1010
+ /**
1011
+ * Build the canonical depth-overflow `DogpileError`. Used by the parser (this
1012
+ * file) and the coordinator dispatcher; kept here so both call sites produce
1013
+ * the exact same error shape (D-14, D-15).
1014
+ */
1015
+ function depthOverflowError(currentDepth, maxDepth) {
1016
+ return new DogpileError({
1017
+ code: "invalid-configuration",
1018
+ message: `Depth overflow: cannot dispatch sub-run at depth ${currentDepth + 1} (maxDepth = ${maxDepth}).`,
1019
+ retryable: false,
1020
+ detail: {
1021
+ kind: "delegate-validation",
1022
+ path: "decision.protocol",
1023
+ reason: "depth-overflow",
1024
+ currentDepth,
1025
+ maxDepth
1026
+ }
1027
+ });
1028
+ }
1029
+ /**
1030
+ * Dispatcher-time depth gate. Throws the same error shape the parser uses; the
1031
+ * dual gate (parser + dispatcher) defends against any TOCTOU window between
1032
+ * decision parsing and child-run spin-up (D-14).
1033
+ */
1034
+ function assertDepthWithinLimit(currentDepth, maxDepth) {
1035
+ if (currentDepth + 1 > maxDepth) throw depthOverflowError(currentDepth, maxDepth);
1036
+ }
1037
+ function parseDelegateBudget(raw) {
1038
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throwInvalidDelegate({
1039
+ path: "decision.budget",
1040
+ message: "delegate decision budget must be an object.",
1041
+ expected: "object",
1042
+ received: describe(raw)
1043
+ });
1044
+ const record = raw;
1045
+ const budget = {};
1046
+ if (record["timeoutMs"] !== void 0) {
1047
+ const value = record["timeoutMs"];
1048
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throwInvalidDelegate({
1049
+ path: "decision.budget.timeoutMs",
1050
+ message: "delegate decision budget.timeoutMs must be a non-negative integer.",
1051
+ expected: "integer >= 0",
1052
+ received: describe(value)
1053
+ });
1054
+ budget.timeoutMs = value;
1055
+ }
1056
+ if (record["maxTokens"] !== void 0) {
1057
+ const value = record["maxTokens"];
1058
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throwInvalidDelegate({
1059
+ path: "decision.budget.maxTokens",
1060
+ message: "delegate decision budget.maxTokens must be a non-negative integer.",
1061
+ expected: "integer >= 0",
1062
+ received: describe(value)
1063
+ });
1064
+ budget.maxTokens = value;
1065
+ }
1066
+ if (record["maxIterations"] !== void 0) {
1067
+ const value = record["maxIterations"];
1068
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throwInvalidDelegate({
1069
+ path: "decision.budget.maxIterations",
1070
+ message: "delegate decision budget.maxIterations must be a non-negative integer.",
1071
+ expected: "integer >= 0",
1072
+ received: describe(value)
1073
+ });
1074
+ budget.maxIterations = value;
1075
+ }
1076
+ return budget;
1077
+ }
1078
+ function throwInvalidDelegate(failure) {
1079
+ throw new DogpileError({
1080
+ code: "invalid-configuration",
1081
+ message: `Invalid Dogpile configuration at ${failure.path}: ${failure.message}`,
1082
+ retryable: false,
1083
+ detail: {
1084
+ kind: "delegate-validation",
1085
+ path: failure.path,
1086
+ expected: failure.expected,
1087
+ received: failure.received
1088
+ }
1089
+ });
532
1090
  }
533
1091
  function matchLine(output, pattern) {
534
1092
  return output.match(pattern)?.[1]?.trim();
@@ -540,16 +1098,38 @@ function matchContribution(output) {
540
1098
  function isAgentParticipation(value) {
541
1099
  return value === "contribute" || value === "abstain";
542
1100
  }
1101
+ function describe(value) {
1102
+ if (value === null) return "null";
1103
+ if (Array.isArray(value)) return "array";
1104
+ if (typeof value === "string") return JSON.stringify(value).slice(0, 200);
1105
+ return typeof value;
1106
+ }
1107
+ function truncate(value) {
1108
+ return value.length > 200 ? `${value.slice(0, 200)}…` : value;
1109
+ }
543
1110
  //#endregion
544
1111
  //#region src/runtime/model.ts
545
1112
  async function generateModelTurn(options) {
546
1113
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1114
+ const modelId = options.model.modelId ?? options.model.id;
1115
+ const traceRequest = requestForTrace(options.request);
547
1116
  let response;
548
1117
  throwIfAborted(options.request.signal, options.model.id);
1118
+ options.emit({
1119
+ type: "model-request",
1120
+ runId: options.runId,
1121
+ callId: options.callId,
1122
+ providerId: options.model.id,
1123
+ modelId,
1124
+ startedAt,
1125
+ agentId: options.agent.id,
1126
+ role: options.agent.role,
1127
+ request: traceRequest
1128
+ });
549
1129
  if (!options.model.stream) {
550
1130
  response = await options.model.generate(options.request);
551
1131
  throwIfAborted(options.request.signal, options.model.id);
552
- recordProviderCall(response, startedAt, options);
1132
+ recordProviderCall(response, startedAt, modelId, traceRequest, options);
553
1133
  return response;
554
1134
  }
555
1135
  let text = "";
@@ -589,27 +1169,41 @@ async function generateModelTurn(options) {
589
1169
  ...metadata !== void 0 ? { metadata } : {}
590
1170
  };
591
1171
  throwIfAborted(options.request.signal, options.model.id);
592
- recordProviderCall(response, startedAt, options);
1172
+ recordProviderCall(response, startedAt, modelId, traceRequest, options);
593
1173
  return response;
594
1174
  }
595
- function recordProviderCall(response, startedAt, options) {
1175
+ function recordProviderCall(response, startedAt, modelId, request, options) {
1176
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1177
+ options.emit({
1178
+ type: "model-response",
1179
+ runId: options.runId,
1180
+ callId: options.callId,
1181
+ providerId: options.model.id,
1182
+ modelId,
1183
+ startedAt,
1184
+ completedAt,
1185
+ agentId: options.agent.id,
1186
+ role: options.agent.role,
1187
+ response
1188
+ });
596
1189
  options.onProviderCall?.({
597
1190
  kind: "replay-trace-provider-call",
598
1191
  callId: options.callId,
599
1192
  providerId: options.model.id,
1193
+ modelId,
600
1194
  startedAt,
601
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1195
+ completedAt,
602
1196
  agentId: options.agent.id,
603
1197
  role: options.agent.role,
604
- request: requestForTrace(options.request),
1198
+ request,
605
1199
  response
606
1200
  });
607
1201
  }
608
1202
  function requestForTrace(request) {
609
1203
  return {
610
- messages: request.messages,
1204
+ messages: request.messages.map((message) => ({ ...message })),
611
1205
  temperature: request.temperature,
612
- metadata: request.metadata
1206
+ metadata: JSON.parse(JSON.stringify(request.metadata))
613
1207
  };
614
1208
  }
615
1209
  //#endregion
@@ -997,6 +1591,7 @@ var budgetTiers = [
997
1591
  "balanced",
998
1592
  "quality"
999
1593
  ];
1594
+ var onChildFailureModes = ["continue", "abort"];
1000
1595
  /**
1001
1596
  * Validate high-level caller options before any protocol execution starts.
1002
1597
  */
@@ -1015,11 +1610,25 @@ function validateDogpileOptions(options) {
1015
1610
  validateOptionalFunction(options.evaluate, "evaluate");
1016
1611
  validateOptionalSeed(options.seed, "seed");
1017
1612
  validateOptionalAbortSignal(options.signal, "signal");
1613
+ validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
1614
+ validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
1615
+ validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
1616
+ validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
1018
1617
  }
1019
1618
  function validateMissionIntent(intent, path = "intent") {
1020
1619
  validateNonEmptyString(intent, path, "intent is required.");
1021
1620
  }
1022
1621
  /**
1622
+ * Validate per-call run/stream options (`Engine.run(intent, options)` / `Engine.stream(...)`).
1623
+ */
1624
+ function validateRunCallOptions(options, path = "options") {
1625
+ if (options === void 0) return;
1626
+ const record = requireRecord(options, path);
1627
+ validateOptionalNonNegativeInteger(record.maxDepth, `${path}.maxDepth`);
1628
+ validateOptionalPositiveInteger(record.maxConcurrentChildren, `${path}.maxConcurrentChildren`);
1629
+ validateOptionalOnChildFailure(record.onChildFailure, `${path}.onChildFailure`);
1630
+ }
1631
+ /**
1023
1632
  * Validate low-level engine configuration before normalizing reusable controls.
1024
1633
  */
1025
1634
  function validateEngineOptions(options) {
@@ -1036,6 +1645,10 @@ function validateEngineOptions(options) {
1036
1645
  validateOptionalFunction(options.evaluate, "evaluate");
1037
1646
  validateOptionalSeed(options.seed, "seed");
1038
1647
  validateOptionalAbortSignal(options.signal, "signal");
1648
+ validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
1649
+ validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
1650
+ validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
1651
+ validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
1039
1652
  }
1040
1653
  function validateProtocolSelection(value, path) {
1041
1654
  if (typeof value === "string") {
@@ -1083,6 +1696,23 @@ function validateBudgetTier(value, path) {
1083
1696
  actual: value
1084
1697
  });
1085
1698
  }
1699
+ function validateOptionalOnChildFailure(value, path) {
1700
+ if (value === void 0) return;
1701
+ if (value === "continue" || value === "abort") return;
1702
+ throw new DogpileError({
1703
+ code: "invalid-configuration",
1704
+ message: `Invalid onChildFailure: expected "continue" or "abort", got ${JSON.stringify(value)}`,
1705
+ retryable: false,
1706
+ detail: {
1707
+ kind: "configuration-validation",
1708
+ path,
1709
+ rule: "enum",
1710
+ expected: onChildFailureModes.join(" | "),
1711
+ received: describeValue(value),
1712
+ reason: "invalid-on-child-failure"
1713
+ }
1714
+ });
1715
+ }
1086
1716
  /**
1087
1717
  * Validate configured model provider definitions at registration boundaries.
1088
1718
  */
@@ -1092,6 +1722,21 @@ function validateModelProviderRegistration(value, path = "model") {
1092
1722
  validateFunction(record.generate, `${path}.generate`);
1093
1723
  validateOptionalFunction(record.stream, `${path}.stream`);
1094
1724
  }
1725
+ /**
1726
+ * Engine-time defense-in-depth check that a provider's optional
1727
+ * `metadata.locality` is one of the two valid values when present (Phase 3 D-03).
1728
+ * Catches user-implemented providers that bypass TypeScript checks.
1729
+ */
1730
+ function validateProviderLocality(provider, pathPrefix = "model") {
1731
+ const loc = provider.metadata?.locality;
1732
+ if (loc !== void 0 && loc !== "local" && loc !== "remote") invalidConfiguration({
1733
+ path: `${pathPrefix}.metadata.locality`,
1734
+ rule: "enum",
1735
+ message: `${pathPrefix}.metadata.locality must be "local" or "remote" when provided.`,
1736
+ expected: "\"local\" | \"remote\"",
1737
+ actual: loc
1738
+ });
1739
+ }
1095
1740
  function validateOptionalAgents(value, path) {
1096
1741
  if (value === void 0) return;
1097
1742
  if (!Array.isArray(value)) invalidConfiguration({
@@ -1371,6 +2016,16 @@ function validateOptionalNonNegativeInteger(value, path) {
1371
2016
  actual: value
1372
2017
  });
1373
2018
  }
2019
+ function validateOptionalPositiveFiniteNumber(value, path) {
2020
+ if (value === void 0) return;
2021
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) invalidConfiguration({
2022
+ path,
2023
+ rule: "positive-finite-number",
2024
+ message: "value must be a positive finite number.",
2025
+ expected: "finite number > 0",
2026
+ actual: value
2027
+ });
2028
+ }
1374
2029
  function validateOptionalNonNegativeNumber(value, path) {
1375
2030
  if (value === void 0) return;
1376
2031
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) invalidConfiguration({
@@ -2491,44 +3146,45 @@ async function runBroadcast(options) {
2491
3146
  emit(final);
2492
3147
  recordProtocolDecision(final, { transcriptEntryCount: transcript.length });
2493
3148
  const finalEvent = events.at(-1);
3149
+ const trace = {
3150
+ schemaVersion: "1.0",
3151
+ runId,
3152
+ protocol: "broadcast",
3153
+ tier: options.tier,
3154
+ modelProviderId: options.model.id,
3155
+ agentsUsed: options.agents,
3156
+ inputs: createReplayTraceRunInputs({
3157
+ intent: options.intent,
3158
+ protocol: options.protocol,
3159
+ tier: options.tier,
3160
+ modelProviderId: options.model.id,
3161
+ agents: options.agents,
3162
+ temperature: options.temperature
3163
+ }),
3164
+ budget: createReplayTraceBudget({
3165
+ tier: options.tier,
3166
+ ...options.budget ? { caps: options.budget } : {},
3167
+ ...options.terminate ? { termination: options.terminate } : {}
3168
+ }),
3169
+ budgetStateChanges: createReplayTraceBudgetStateChanges(events),
3170
+ seed: createReplayTraceSeed(options.seed),
3171
+ protocolDecisions,
3172
+ providerCalls,
3173
+ finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
3174
+ type: "final",
3175
+ runId,
3176
+ at: "",
3177
+ output,
3178
+ cost: totalCost,
3179
+ transcript: createTranscriptLink(transcript)
3180
+ }),
3181
+ events,
3182
+ transcript
3183
+ };
2494
3184
  return {
2495
3185
  output,
2496
3186
  eventLog: createRunEventLog(runId, "broadcast", events),
2497
- trace: {
2498
- schemaVersion: "1.0",
2499
- runId,
2500
- protocol: "broadcast",
2501
- tier: options.tier,
2502
- modelProviderId: options.model.id,
2503
- agentsUsed: options.agents,
2504
- inputs: createReplayTraceRunInputs({
2505
- intent: options.intent,
2506
- protocol: options.protocol,
2507
- tier: options.tier,
2508
- modelProviderId: options.model.id,
2509
- agents: options.agents,
2510
- temperature: options.temperature
2511
- }),
2512
- budget: createReplayTraceBudget({
2513
- tier: options.tier,
2514
- ...options.budget ? { caps: options.budget } : {},
2515
- ...options.terminate ? { termination: options.terminate } : {}
2516
- }),
2517
- budgetStateChanges: createReplayTraceBudgetStateChanges(events),
2518
- seed: createReplayTraceSeed(options.seed),
2519
- protocolDecisions,
2520
- providerCalls,
2521
- finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
2522
- type: "final",
2523
- runId,
2524
- at: "",
2525
- output,
2526
- cost: totalCost,
2527
- transcript: createTranscriptLink(transcript)
2528
- }),
2529
- events,
2530
- transcript
2531
- },
3187
+ trace,
2532
3188
  transcript,
2533
3189
  usage: createRunUsage(totalCost),
2534
3190
  metadata: createRunMetadata({
@@ -2546,7 +3202,8 @@ async function runBroadcast(options) {
2546
3202
  cost: totalCost,
2547
3203
  events
2548
3204
  }),
2549
- cost: totalCost
3205
+ cost: totalCost,
3206
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
2550
3207
  };
2551
3208
  function stopIfNeeded() {
2552
3209
  throwIfAborted(options.signal, options.model.id);
@@ -2608,22 +3265,77 @@ function responseCost$3(response) {
2608
3265
  }
2609
3266
  //#endregion
2610
3267
  //#region src/runtime/coordinator.ts
2611
- async function runCoordinator(options) {
2612
- const runId = createRunId();
2613
- const events = [];
2614
- const transcript = [];
2615
- const protocolDecisions = [];
2616
- const providerCalls = [];
2617
- let totalCost = emptyCost();
2618
- const maxTurns = options.protocol.maxTurns ?? options.agents.length;
2619
- const activeAgents = options.agents.slice(0, maxTurns);
2620
- const coordinator = activeAgents[0];
2621
- const startedAtMs = nowMs();
2622
- let stopped = false;
2623
- let termination;
2624
- const wrapUpHint = createWrapUpHintController({
2625
- protocol: options.protocol,
2626
- tier: options.tier,
3268
+ /**
3269
+ * Hard-coded loop guard for the delegate dispatch in the coordinator plan
3270
+ * turn. After this many consecutive delegate decisions the coordinator throws
3271
+ * `invalid-configuration` (T-03-01). Not a public option.
3272
+ */
3273
+ var MAX_DISPATCH_PER_TURN = 8;
3274
+ var DEFAULT_MAX_CONCURRENT_CHILDREN$1 = 4;
3275
+ function createSemaphore(maxConcurrent) {
3276
+ let inFlight = 0;
3277
+ const waiters = [];
3278
+ return {
3279
+ acquire() {
3280
+ if (inFlight < maxConcurrent) {
3281
+ inFlight += 1;
3282
+ return Promise.resolve();
3283
+ }
3284
+ return new Promise((resolve) => {
3285
+ waiters.push(() => {
3286
+ inFlight += 1;
3287
+ resolve();
3288
+ });
3289
+ });
3290
+ },
3291
+ release() {
3292
+ inFlight -= 1;
3293
+ const next = waiters.shift();
3294
+ if (next !== void 0) next();
3295
+ },
3296
+ get inFlight() {
3297
+ return inFlight;
3298
+ },
3299
+ get queued() {
3300
+ return waiters.length;
3301
+ }
3302
+ };
3303
+ }
3304
+ /**
3305
+ * Walk the coordinator's active provider set and return the FIRST provider
3306
+ * whose metadata.locality === "local", or undefined if none found.
3307
+ *
3308
+ * Walk order (forward-compat): options.model first, then options.agents in
3309
+ * declaration order. AgentSpec has no `model` field today (Phase 3 D-11
3310
+ * forward-compat scaffolding); the agent walk uses optional chaining and
3311
+ * effectively no-ops until a future phase adds AgentSpec.model.
3312
+ */
3313
+ function findFirstLocalProvider(options) {
3314
+ if (options.model.metadata?.locality === "local") return options.model;
3315
+ for (const agent of options.agents) {
3316
+ const agentModel = agent.model;
3317
+ if (agentModel?.metadata?.locality === "local") return agentModel;
3318
+ }
3319
+ }
3320
+ async function runCoordinator(options) {
3321
+ const runId = createRunId();
3322
+ const events = [];
3323
+ const transcript = [];
3324
+ const protocolDecisions = [];
3325
+ const providerCalls = [];
3326
+ const dispatchedChildren = /* @__PURE__ */ new Map();
3327
+ let totalCost = emptyCost();
3328
+ let concurrencyClampEmitted = false;
3329
+ const maxTurns = options.protocol.maxTurns ?? options.agents.length;
3330
+ const activeAgents = options.agents.slice(0, maxTurns);
3331
+ const coordinator = activeAgents[0];
3332
+ const startedAtMs = nowMs();
3333
+ let stopped = false;
3334
+ let termination;
3335
+ let triggeringFailureForAbortMode;
3336
+ const wrapUpHint = createWrapUpHintController({
3337
+ protocol: options.protocol,
3338
+ tier: options.tier,
2627
3339
  ...options.budget ? { budget: options.budget } : {},
2628
3340
  ...options.terminate ? { terminate: options.terminate } : {},
2629
3341
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}
@@ -2636,6 +3348,51 @@ async function runCoordinator(options) {
2636
3348
  const recordProtocolDecision = (event, decisionOptions) => {
2637
3349
  protocolDecisions.push(createReplayTraceProtocolDecision("coordinator", event, events.length - 1, decisionOptions));
2638
3350
  };
3351
+ const drainOnParentAbort = (reasonSource) => {
3352
+ const reason = classifyAbortReason(reasonSource);
3353
+ for (const child of dispatchedChildren.values()) {
3354
+ if (child.closed) continue;
3355
+ const partialCost = child.started ? lastCostBearingEventCost(child.childEvents) ?? emptyCost() : emptyCost();
3356
+ const partialTrace = buildPartialTrace({
3357
+ childRunId: child.childRunId,
3358
+ events: [...child.childEvents],
3359
+ startedAtMs: child.startedAtMs,
3360
+ protocol: child.decision.protocol,
3361
+ tier: options.tier,
3362
+ modelProviderId: options.model.id,
3363
+ agents: options.agents,
3364
+ intent: child.decision.intent,
3365
+ temperature: options.temperature,
3366
+ ...child.childTimeoutMs !== void 0 ? { childTimeoutMs: child.childTimeoutMs } : {},
3367
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
3368
+ });
3369
+ const failedEvent = {
3370
+ type: "sub-run-failed",
3371
+ runId,
3372
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3373
+ childRunId: child.childRunId,
3374
+ parentRunId: runId,
3375
+ parentDecisionId: child.parentDecisionId,
3376
+ parentDecisionArrayIndex: child.parentDecisionArrayIndex,
3377
+ error: child.started ? {
3378
+ code: "aborted",
3379
+ message: "Parent run aborted.",
3380
+ detail: { reason }
3381
+ } : {
3382
+ code: "aborted",
3383
+ message: "Sibling delegate failed; queued delegate never started.",
3384
+ detail: { reason: "sibling-failed" }
3385
+ },
3386
+ partialTrace,
3387
+ partialCost
3388
+ };
3389
+ child.closed = true;
3390
+ totalCost = addCost(totalCost, partialCost);
3391
+ emit(failedEvent);
3392
+ recordProtocolDecision(failedEvent);
3393
+ }
3394
+ };
3395
+ options.registerAbortDrain?.(drainOnParentAbort);
2639
3396
  const toolExecutor = createRuntimeToolExecutor({
2640
3397
  runId,
2641
3398
  protocol: "coordinator",
@@ -2666,24 +3423,231 @@ async function runCoordinator(options) {
2666
3423
  }
2667
3424
  if (coordinator) {
2668
3425
  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
- });
3426
+ let dispatchInput = buildCoordinatorPlanInput(options.intent, coordinator);
3427
+ let dispatchCount = 0;
3428
+ while (true) {
3429
+ const turnOutcome = await runCoordinatorTurn({
3430
+ agent: coordinator,
3431
+ coordinator,
3432
+ input: dispatchInput,
3433
+ phase: "plan",
3434
+ options,
3435
+ runId,
3436
+ transcript,
3437
+ totalCost,
3438
+ providerCalls,
3439
+ toolExecutor,
3440
+ toolAvailability,
3441
+ events,
3442
+ startedAtMs,
3443
+ wrapUpHint,
3444
+ emit,
3445
+ recordProtocolDecision
3446
+ });
3447
+ totalCost = turnOutcome.totalCost;
3448
+ if (turnOutcome.decision === void 0) break;
3449
+ const delegates = Array.isArray(turnOutcome.decision) ? turnOutcome.decision : turnOutcome.decision.type === "delegate" ? [turnOutcome.decision] : [];
3450
+ if (delegates.length === 0) break;
3451
+ if (dispatchCount + delegates.length > MAX_DISPATCH_PER_TURN) throw new DogpileError({
3452
+ code: "invalid-configuration",
3453
+ message: `Coordinator plan turn delegated ${delegates.length} more children after ${dispatchCount}; max is ${MAX_DISPATCH_PER_TURN}.`,
3454
+ retryable: false,
3455
+ detail: {
3456
+ kind: "delegate-validation",
3457
+ path: "decision",
3458
+ reason: "loop-guard-exceeded",
3459
+ maxDispatchPerTurn: MAX_DISPATCH_PER_TURN
3460
+ }
3461
+ });
3462
+ const parentDecisionId = String(events.length - 1);
3463
+ const parentDepth = options.currentDepth ?? 0;
3464
+ const decisionMax = delegates.reduce((max, delegate) => Math.min(max, delegate.maxConcurrentChildren ?? Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
3465
+ let effectiveForTurn = Math.min(options.effectiveMaxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN$1, decisionMax);
3466
+ const requestedMax = effectiveForTurn;
3467
+ const localProvider = findFirstLocalProvider(options);
3468
+ if (localProvider !== void 0) {
3469
+ effectiveForTurn = 1;
3470
+ if (!concurrencyClampEmitted) {
3471
+ const clampEvent = {
3472
+ type: "sub-run-concurrency-clamped",
3473
+ runId,
3474
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3475
+ requestedMax,
3476
+ effectiveMax: 1,
3477
+ reason: "local-provider-detected",
3478
+ providerId: localProvider.id
3479
+ };
3480
+ emit(clampEvent);
3481
+ recordProtocolDecision(clampEvent);
3482
+ concurrencyClampEmitted = true;
3483
+ }
3484
+ }
3485
+ const semaphore = createSemaphore(effectiveForTurn);
3486
+ const childRunIds = delegates.map(() => createRunId());
3487
+ const dispatchedForTurn = delegates.map((delegate, index) => {
3488
+ const childRunId = childRunIds[index];
3489
+ if (childRunId === void 0) throw new Error("missing child run id");
3490
+ const dispatchedChild = {
3491
+ childRunId,
3492
+ decision: delegate,
3493
+ parentDecisionId,
3494
+ parentDecisionArrayIndex: index,
3495
+ parentDepth,
3496
+ controller: new AbortController(),
3497
+ removeParentListener: void 0,
3498
+ childEvents: [],
3499
+ started: false,
3500
+ closed: false,
3501
+ startedAtMs: Date.now(),
3502
+ childTimeoutMs: void 0,
3503
+ failure: void 0
3504
+ };
3505
+ dispatchedChildren.set(childRunId, dispatchedChild);
3506
+ return dispatchedChild;
3507
+ });
3508
+ const dispatchResults = [];
3509
+ let firstFailureIndex;
3510
+ const tasks = delegates.map(async (delegate, index) => {
3511
+ const childRunId = childRunIds[index];
3512
+ if (childRunId === void 0) throw new Error("missing child run id");
3513
+ if (semaphore.inFlight >= effectiveForTurn) {
3514
+ const queuedEvent = {
3515
+ type: "sub-run-queued",
3516
+ runId,
3517
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3518
+ childRunId,
3519
+ parentRunId: runId,
3520
+ parentDecisionId,
3521
+ parentDecisionArrayIndex: index,
3522
+ protocol: delegate.protocol,
3523
+ intent: delegate.intent,
3524
+ depth: parentDepth + 1,
3525
+ queuePosition: semaphore.queued
3526
+ };
3527
+ emit(queuedEvent);
3528
+ recordProtocolDecision(queuedEvent);
3529
+ }
3530
+ await semaphore.acquire();
3531
+ try {
3532
+ const dispatchedChild = dispatchedForTurn[index];
3533
+ if (!dispatchedChild) throw new Error("missing dispatched child");
3534
+ if (firstFailureIndex !== void 0) {
3535
+ if (dispatchedChild.closed) {
3536
+ dispatchResults.push({
3537
+ index,
3538
+ result: {
3539
+ nextInput: "",
3540
+ taggedText: `[sub-run ${childRunId}]: skipped because the parent run aborted`,
3541
+ completedAtMs: Date.now()
3542
+ }
3543
+ });
3544
+ return;
3545
+ }
3546
+ const partialCost = emptyCost();
3547
+ const partialTrace = buildPartialTrace({
3548
+ childRunId,
3549
+ events: [],
3550
+ startedAtMs: Date.now(),
3551
+ protocol: delegate.protocol,
3552
+ tier: options.tier,
3553
+ modelProviderId: options.model.id,
3554
+ agents: options.agents,
3555
+ intent: delegate.intent,
3556
+ temperature: options.temperature,
3557
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
3558
+ });
3559
+ const failedEvent = {
3560
+ type: "sub-run-failed",
3561
+ runId,
3562
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3563
+ childRunId,
3564
+ parentRunId: runId,
3565
+ parentDecisionId,
3566
+ parentDecisionArrayIndex: index,
3567
+ error: {
3568
+ code: "aborted",
3569
+ message: "Sibling delegate failed; queued delegate never started.",
3570
+ detail: { reason: "sibling-failed" }
3571
+ },
3572
+ partialTrace,
3573
+ partialCost
3574
+ };
3575
+ emit(failedEvent);
3576
+ recordProtocolDecision(failedEvent);
3577
+ dispatchedChild.closed = true;
3578
+ dispatchResults.push({
3579
+ index,
3580
+ result: {
3581
+ nextInput: "",
3582
+ taggedText: `[sub-run ${childRunId}]: skipped because a sibling delegate failed`,
3583
+ completedAtMs: Date.now()
3584
+ }
3585
+ });
3586
+ return;
3587
+ }
3588
+ const result = await dispatchDelegate({
3589
+ decision: delegate,
3590
+ childRunId,
3591
+ parentDecisionId,
3592
+ parentDecisionArrayIndex: index,
3593
+ parentDepth,
3594
+ parentRunId: runId,
3595
+ options,
3596
+ transcript,
3597
+ emit,
3598
+ recordProtocolDecision,
3599
+ recordSubRunCost: (cost) => {
3600
+ totalCost = addCost(totalCost, cost);
3601
+ },
3602
+ dispatchedChild
3603
+ });
3604
+ dispatchResults.push({
3605
+ index,
3606
+ result
3607
+ });
3608
+ } catch (error) {
3609
+ firstFailureIndex ??= index;
3610
+ const failure = dispatchedForTurn[index]?.failure;
3611
+ if (delegates.length === 1 && (options.onChildFailure === "abort" || failure === void 0 || isDelegateValidationError(error))) throw error;
3612
+ let taggedText = `[sub-run ${childRunId} failed]: ${error instanceof Error ? error.message : String(error)}`;
3613
+ if (failure) {
3614
+ const error = failure.error;
3615
+ taggedText = `[sub-run ${childRunId} failed | code=${error.code} | spent=$${failure.partialCost.usd.toFixed(3)}]: ${error.message}`;
3616
+ }
3617
+ dispatchResults.push({
3618
+ index,
3619
+ result: {
3620
+ nextInput: "",
3621
+ taggedText,
3622
+ completedAtMs: Date.now()
3623
+ }
3624
+ });
3625
+ } finally {
3626
+ semaphore.release();
3627
+ }
3628
+ });
3629
+ const firstRejected = (await Promise.allSettled(tasks)).find((result) => result.status === "rejected");
3630
+ if (firstRejected?.status === "rejected" && delegates.length === 1 && (options.onChildFailure === "abort" || dispatchResults.length === 0)) throw firstRejected.reason;
3631
+ dispatchResults.sort((a, b) => a.result.completedAtMs - b.result.completedAtMs);
3632
+ const taggedResults = dispatchResults.map((entry) => entry.result.taggedText).join("\n\n");
3633
+ const currentWaveFailures = dispatchedForTurn.map((child) => child.failure).filter((failure) => failure !== void 0);
3634
+ if (options.onChildFailure === "abort" && currentWaveFailures.length > 0) {
3635
+ triggeringFailureForAbortMode ??= currentWaveFailures[0];
3636
+ break;
3637
+ }
3638
+ const failuresSection = buildFailuresSection(currentWaveFailures);
3639
+ const coordinatorAgent = options.agents[0] ?? {
3640
+ id: "coordinator",
3641
+ role: "coordinator"
3642
+ };
3643
+ dispatchInput = [
3644
+ buildCoordinatorPlanInput(options.intent, coordinatorAgent),
3645
+ taggedResults,
3646
+ failuresSection,
3647
+ "Using the sub-run results above, decide the next step (participate or delegate)."
3648
+ ].filter((section) => Boolean(section)).join("\n\n");
3649
+ dispatchCount += delegates.length;
3650
+ }
2687
3651
  stopIfNeeded();
2688
3652
  }
2689
3653
  if (!stopIfNeeded()) {
@@ -2741,7 +3705,7 @@ async function runCoordinator(options) {
2741
3705
  stopIfNeeded();
2742
3706
  }
2743
3707
  if (!stopIfNeeded()) {
2744
- totalCost = await runCoordinatorTurn({
3708
+ const synthesisOutcome = await runCoordinatorTurn({
2745
3709
  agent: coordinator,
2746
3710
  coordinator,
2747
3711
  input: buildFinalSynthesisInput(options.intent, transcript, coordinator),
@@ -2759,6 +3723,17 @@ async function runCoordinator(options) {
2759
3723
  emit,
2760
3724
  recordProtocolDecision
2761
3725
  });
3726
+ totalCost = synthesisOutcome.totalCost;
3727
+ if (Array.isArray(synthesisOutcome.decision) || synthesisOutcome.decision?.type === "delegate") throw new DogpileError({
3728
+ code: "invalid-configuration",
3729
+ message: "Coordinator final-synthesis turn cannot emit a delegate decision in Phase 1",
3730
+ retryable: false,
3731
+ detail: {
3732
+ kind: "delegate-validation",
3733
+ path: "decision",
3734
+ phase: "final-synthesis"
3735
+ }
3736
+ });
2762
3737
  stopIfNeeded();
2763
3738
  }
2764
3739
  }
@@ -2776,44 +3751,46 @@ async function runCoordinator(options) {
2776
3751
  emit(final);
2777
3752
  recordProtocolDecision(final, { transcriptEntryCount: transcript.length });
2778
3753
  const finalEvent = events.at(-1);
3754
+ const trace = {
3755
+ schemaVersion: "1.0",
3756
+ runId,
3757
+ protocol: "coordinator",
3758
+ tier: options.tier,
3759
+ modelProviderId: options.model.id,
3760
+ agentsUsed: activeAgents,
3761
+ inputs: createReplayTraceRunInputs({
3762
+ intent: options.intent,
3763
+ protocol: options.protocol,
3764
+ tier: options.tier,
3765
+ modelProviderId: options.model.id,
3766
+ agents: activeAgents,
3767
+ temperature: options.temperature
3768
+ }),
3769
+ budget: createReplayTraceBudget({
3770
+ tier: options.tier,
3771
+ ...options.budget ? { caps: options.budget } : {},
3772
+ ...options.terminate ? { termination: options.terminate } : {}
3773
+ }),
3774
+ budgetStateChanges: createReplayTraceBudgetStateChanges(events),
3775
+ seed: createReplayTraceSeed(options.seed),
3776
+ protocolDecisions,
3777
+ providerCalls,
3778
+ finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
3779
+ type: "final",
3780
+ runId,
3781
+ at: "",
3782
+ output,
3783
+ cost: totalCost,
3784
+ transcript: createTranscriptLink(transcript)
3785
+ }),
3786
+ ...triggeringFailureForAbortMode !== void 0 ? { triggeringFailureForAbortMode } : {},
3787
+ events,
3788
+ transcript
3789
+ };
2779
3790
  return {
2780
3791
  output,
2781
3792
  eventLog: createRunEventLog(runId, "coordinator", events),
2782
- trace: {
2783
- schemaVersion: "1.0",
2784
- runId,
2785
- protocol: "coordinator",
2786
- tier: options.tier,
2787
- modelProviderId: options.model.id,
2788
- agentsUsed: activeAgents,
2789
- inputs: createReplayTraceRunInputs({
2790
- intent: options.intent,
2791
- protocol: options.protocol,
2792
- tier: options.tier,
2793
- modelProviderId: options.model.id,
2794
- agents: activeAgents,
2795
- temperature: options.temperature
2796
- }),
2797
- budget: createReplayTraceBudget({
2798
- tier: options.tier,
2799
- ...options.budget ? { caps: options.budget } : {},
2800
- ...options.terminate ? { termination: options.terminate } : {}
2801
- }),
2802
- budgetStateChanges: createReplayTraceBudgetStateChanges(events),
2803
- seed: createReplayTraceSeed(options.seed),
2804
- protocolDecisions,
2805
- providerCalls,
2806
- finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
2807
- type: "final",
2808
- runId,
2809
- at: "",
2810
- output,
2811
- cost: totalCost,
2812
- transcript: createTranscriptLink(transcript)
2813
- }),
2814
- events,
2815
- transcript
2816
- },
3793
+ trace,
2817
3794
  transcript,
2818
3795
  usage: createRunUsage(totalCost),
2819
3796
  metadata: createRunMetadata({
@@ -2831,7 +3808,8 @@ async function runCoordinator(options) {
2831
3808
  cost: totalCost,
2832
3809
  events
2833
3810
  }),
2834
- cost: totalCost
3811
+ cost: totalCost,
3812
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
2835
3813
  };
2836
3814
  function stopIfNeeded() {
2837
3815
  throwIfAborted(options.signal, options.model.id);
@@ -2868,6 +3846,9 @@ async function runCoordinator(options) {
2868
3846
  recordProtocolDecision(event, { transcriptEntryCount: transcript.length });
2869
3847
  }
2870
3848
  }
3849
+ function isDelegateValidationError(error) {
3850
+ return DogpileError.isInstance(error) && error.code === "invalid-configuration" && error.detail?.["kind"] === "delegate-validation";
3851
+ }
2871
3852
  async function runCoordinatorTurn(turn) {
2872
3853
  throwIfAborted(turn.options.signal, turn.options.model.id);
2873
3854
  const request = {
@@ -2911,7 +3892,11 @@ async function runCoordinatorTurn(turn) {
2911
3892
  turn.providerCalls.push(call);
2912
3893
  }
2913
3894
  });
2914
- const decision = parseAgentDecision(response.text);
3895
+ const decision = parseAgentDecision(response.text, {
3896
+ parentProviderId: turn.options.model.id,
3897
+ currentDepth: turn.options.currentDepth ?? 0,
3898
+ maxDepth: turn.options.effectiveMaxDepth ?? Number.POSITIVE_INFINITY
3899
+ });
2915
3900
  const totalCost = addCost(turn.totalCost, responseCost$2(response));
2916
3901
  const toolCalls = await executeModelResponseToolRequests({
2917
3902
  response,
@@ -2947,7 +3932,10 @@ async function runCoordinatorTurn(turn) {
2947
3932
  phase: turn.phase,
2948
3933
  transcriptEntryCount: turn.transcript.length
2949
3934
  });
2950
- return totalCost;
3935
+ return {
3936
+ totalCost,
3937
+ decision
3938
+ };
2951
3939
  }
2952
3940
  async function runCoordinatorWorkerTurn(turn) {
2953
3941
  throwIfAborted(turn.options.signal, turn.options.model.id);
@@ -2992,7 +3980,21 @@ async function runCoordinatorWorkerTurn(turn) {
2992
3980
  turn.providerCallSlots[turn.providerCallIndex] = call;
2993
3981
  }
2994
3982
  });
2995
- const decision = parseAgentDecision(response.text);
3983
+ const decision = parseAgentDecision(response.text, {
3984
+ parentProviderId: turn.options.model.id,
3985
+ currentDepth: turn.options.currentDepth ?? 0,
3986
+ maxDepth: turn.options.effectiveMaxDepth ?? Number.POSITIVE_INFINITY
3987
+ });
3988
+ if (Array.isArray(decision) || decision?.type === "delegate") throw new DogpileError({
3989
+ code: "invalid-configuration",
3990
+ message: "Workers cannot emit delegate decisions in Phase 1",
3991
+ retryable: false,
3992
+ detail: {
3993
+ kind: "delegate-validation",
3994
+ path: "decision",
3995
+ phase: "worker"
3996
+ }
3997
+ });
2996
3998
  const toolCalls = await executeModelResponseToolRequests({
2997
3999
  response,
2998
4000
  executor: turn.toolExecutor,
@@ -3019,6 +4021,30 @@ function buildSystemPrompt$2(agent, coordinator) {
3019
4021
  function buildCoordinatorPlanInput(intent, coordinator) {
3020
4022
  return `Mission: ${intent}\nCoordinator ${coordinator.id}: assign the work, name the plan, and provide the first contribution.`;
3021
4023
  }
4024
+ function buildFailuresSection(failures) {
4025
+ if (failures.length === 0) return null;
4026
+ return [
4027
+ "## Sub-run failures since last decision",
4028
+ "",
4029
+ "```json",
4030
+ JSON.stringify(failures, null, 2),
4031
+ "```"
4032
+ ].join("\n");
4033
+ }
4034
+ function dispatchWaveFailureFromEvent(intent, event) {
4035
+ const reason = typeof event.error.detail?.["reason"] === "string" ? event.error.detail["reason"] : void 0;
4036
+ if (reason === "sibling-failed" || reason === "parent-aborted") return;
4037
+ return {
4038
+ childRunId: event.childRunId,
4039
+ intent,
4040
+ error: {
4041
+ code: event.error.code,
4042
+ message: event.error.message,
4043
+ ...reason !== void 0 ? { detail: { reason } } : {}
4044
+ },
4045
+ partialCost: { usd: event.partialCost.usd }
4046
+ };
4047
+ }
3022
4048
  function buildWorkerInput(intent, transcript, coordinator) {
3023
4049
  const prior = transcript.map((entry) => `${entry.role} (${entry.agentId}): ${entry.output}`).join("\n\n");
3024
4050
  return `Mission: ${intent}\n\nCoordinator: ${coordinator.id}\nPrior contributions:\n${prior}\n\nFollow the coordinator-managed plan and provide your assigned contribution.`;
@@ -3035,6 +4061,388 @@ function responseCost$2(response) {
3035
4061
  totalTokens: response.usage?.totalTokens ?? 0
3036
4062
  };
3037
4063
  }
4064
+ /**
4065
+ * Dispatch a single delegate decision as a recursive sub-run.
4066
+ *
4067
+ * D-11: child reuses the parent provider object verbatim.
4068
+ * D-16: `recursive: true` flag set when both parent and child protocol are
4069
+ * `coordinator`.
4070
+ * D-17: tagged result text appended to the next coordinator prompt.
4071
+ * D-18: synthetic transcript entry pushed for replay/provenance.
4072
+ *
4073
+ * On thrown error from the child engine, builds `partialTrace` from a locally
4074
+ * tee'd `childEvents` buffer — `runProtocol`'s error contract is unchanged.
4075
+ */
4076
+ async function dispatchDelegate(input) {
4077
+ const { decision, options } = input;
4078
+ if (options.effectiveMaxDepth !== void 0) assertDepthWithinLimit(input.parentDepth, options.effectiveMaxDepth);
4079
+ const childRunId = input.childRunId ?? createRunId();
4080
+ const recursive = decision.protocol === "coordinator";
4081
+ const decisionTimeoutMs = decision.budget?.timeoutMs;
4082
+ const parentDeadlineMs = options.parentDeadlineMs;
4083
+ const remainingMs = parentDeadlineMs !== void 0 ? Math.max(0, parentDeadlineMs - Date.now()) : void 0;
4084
+ if (parentDeadlineMs !== void 0 && remainingMs === 0) throw new DogpileError({
4085
+ code: "aborted",
4086
+ message: "Parent deadline elapsed before sub-run dispatch.",
4087
+ retryable: false,
4088
+ providerId: options.model.id,
4089
+ detail: { reason: "timeout" }
4090
+ });
4091
+ let childTimeoutMs;
4092
+ let clampedFrom;
4093
+ if (remainingMs !== void 0) if (decisionTimeoutMs !== void 0) if (decisionTimeoutMs > remainingMs) {
4094
+ clampedFrom = decisionTimeoutMs;
4095
+ childTimeoutMs = remainingMs;
4096
+ } else childTimeoutMs = decisionTimeoutMs;
4097
+ else childTimeoutMs = remainingMs;
4098
+ else if (decisionTimeoutMs !== void 0) childTimeoutMs = decisionTimeoutMs;
4099
+ else if (options.defaultSubRunTimeoutMs !== void 0) childTimeoutMs = options.defaultSubRunTimeoutMs;
4100
+ if (!options.runProtocol) throw new DogpileError({
4101
+ code: "invalid-configuration",
4102
+ message: "Coordinator delegate dispatch requires the engine `runProtocol` callback. Use `Dogpile.run` / `createEngine` rather than calling `runCoordinator` directly when delegate is in play.",
4103
+ retryable: false,
4104
+ detail: {
4105
+ kind: "delegate-validation",
4106
+ path: "runProtocol"
4107
+ }
4108
+ });
4109
+ const childEvents = input.dispatchedChild.childEvents;
4110
+ const parentEmit = input.emit;
4111
+ const teedEmit = (event) => {
4112
+ childEvents.push(event);
4113
+ if (input.dispatchedChild.closed) return;
4114
+ if (options.streamEvents && options.emit) {
4115
+ const inbound = event.parentRunIds;
4116
+ options.emit({
4117
+ ...event,
4118
+ parentRunIds: [input.parentRunId, ...inbound ?? []]
4119
+ });
4120
+ }
4121
+ };
4122
+ const childStartedAt = Date.now();
4123
+ input.dispatchedChild.startedAtMs = childStartedAt;
4124
+ if (clampedFrom !== void 0 && childTimeoutMs !== void 0) {
4125
+ const clampEvent = {
4126
+ type: "sub-run-budget-clamped",
4127
+ runId: input.parentRunId,
4128
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4129
+ childRunId,
4130
+ parentRunId: input.parentRunId,
4131
+ parentDecisionId: input.parentDecisionId,
4132
+ requestedTimeoutMs: clampedFrom,
4133
+ clampedTimeoutMs: childTimeoutMs,
4134
+ reason: "exceeded-parent-remaining"
4135
+ };
4136
+ input.emit(clampEvent);
4137
+ input.recordProtocolDecision(clampEvent);
4138
+ }
4139
+ const startEvent = {
4140
+ type: "sub-run-started",
4141
+ runId: input.parentRunId,
4142
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4143
+ childRunId,
4144
+ parentRunId: input.parentRunId,
4145
+ parentDecisionId: input.parentDecisionId,
4146
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
4147
+ protocol: decision.protocol,
4148
+ intent: decision.intent,
4149
+ depth: input.parentDepth + 1,
4150
+ ...recursive ? { recursive: true } : {}
4151
+ };
4152
+ parentEmit(startEvent);
4153
+ input.recordProtocolDecision(startEvent);
4154
+ const parentSignal = options.signal;
4155
+ let removeParentAbortListener;
4156
+ if (parentSignal !== void 0) if (parentSignal.aborted) input.dispatchedChild.controller.abort(parentSignal.reason);
4157
+ else {
4158
+ const handler = () => {
4159
+ input.dispatchedChild.controller.abort(parentSignal.reason);
4160
+ };
4161
+ parentSignal.addEventListener("abort", handler, { once: true });
4162
+ removeParentAbortListener = () => {
4163
+ parentSignal.removeEventListener("abort", handler);
4164
+ };
4165
+ }
4166
+ input.dispatchedChild.removeParentListener = removeParentAbortListener;
4167
+ input.dispatchedChild.started = true;
4168
+ input.dispatchedChild.childTimeoutMs = childTimeoutMs;
4169
+ const childDeadlineReason = childTimeoutMs !== void 0 && parentDeadlineMs === void 0 ? createEngineDeadlineTimeoutError(options.model.id, childTimeoutMs) : void 0;
4170
+ const childDeadlineTimer = childDeadlineReason !== void 0 ? setTimeout(() => {
4171
+ input.dispatchedChild.controller.abort(childDeadlineReason);
4172
+ }, childTimeoutMs) : void 0;
4173
+ const childOptions = {
4174
+ runId: childRunId,
4175
+ intent: decision.intent,
4176
+ protocol: decision.protocol,
4177
+ tier: options.tier,
4178
+ model: options.model,
4179
+ agents: options.agents,
4180
+ tools: options.tools,
4181
+ temperature: options.temperature,
4182
+ ...childTimeoutMs !== void 0 ? { budget: { timeoutMs: childTimeoutMs } } : {},
4183
+ signal: input.dispatchedChild.controller.signal,
4184
+ emit: teedEmit,
4185
+ ...options.streamEvents !== void 0 ? { streamEvents: options.streamEvents } : {},
4186
+ currentDepth: input.parentDepth + 1,
4187
+ ...options.effectiveMaxDepth !== void 0 ? { effectiveMaxDepth: options.effectiveMaxDepth } : {},
4188
+ ...options.effectiveMaxConcurrentChildren !== void 0 ? { effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren } : {},
4189
+ ...options.onChildFailure !== void 0 ? { onChildFailure: options.onChildFailure } : {},
4190
+ ...parentDeadlineMs !== void 0 ? { parentDeadlineMs } : {},
4191
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {}
4192
+ };
4193
+ let subResult;
4194
+ try {
4195
+ subResult = await options.runProtocol(childOptions);
4196
+ } catch (error) {
4197
+ if (childDeadlineTimer !== void 0) clearTimeout(childDeadlineTimer);
4198
+ removeParentAbortListener?.();
4199
+ if (input.dispatchedChild.closed) {
4200
+ const enrichedError = enrichAbortErrorWithParentReason(error, parentSignal);
4201
+ if (DogpileError.isInstance(enrichedError)) throw enrichedError;
4202
+ throw error;
4203
+ }
4204
+ const failedDecision = {
4205
+ type: "delegate",
4206
+ protocol: decision.protocol,
4207
+ intent: decision.intent,
4208
+ ...decision.model !== void 0 ? { model: decision.model } : {},
4209
+ ...decision.budget !== void 0 ? { budget: decision.budget } : {}
4210
+ };
4211
+ const partialTrace = buildPartialTrace({
4212
+ childRunId,
4213
+ events: childEvents,
4214
+ startedAtMs: childStartedAt,
4215
+ protocol: decision.protocol,
4216
+ tier: options.tier,
4217
+ modelProviderId: options.model.id,
4218
+ agents: options.agents,
4219
+ intent: decision.intent,
4220
+ temperature: options.temperature,
4221
+ ...childTimeoutMs !== void 0 ? { childTimeoutMs } : {},
4222
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
4223
+ });
4224
+ const enrichedError = enrichProviderTimeoutSource(enrichAbortErrorWithParentReason(error, parentSignal), {
4225
+ ...decisionTimeoutMs !== void 0 ? { decisionTimeoutMs } : {},
4226
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { engineDefaultTimeoutMs: options.defaultSubRunTimeoutMs } : {}
4227
+ });
4228
+ if (DogpileError.isInstance(enrichedError)) options.failureInstancesByChildRunId?.set(childRunId, enrichedError);
4229
+ const errorPayload = errorPayloadFromUnknown(enrichedError, failedDecision);
4230
+ const partialCost = lastCostBearingEventCost(childEvents) ?? emptyCost();
4231
+ input.recordSubRunCost(partialCost);
4232
+ const failEvent = {
4233
+ type: "sub-run-failed",
4234
+ runId: input.parentRunId,
4235
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4236
+ childRunId,
4237
+ parentRunId: input.parentRunId,
4238
+ parentDecisionId: input.parentDecisionId,
4239
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
4240
+ error: errorPayload,
4241
+ partialTrace,
4242
+ partialCost
4243
+ };
4244
+ parentEmit(failEvent);
4245
+ input.recordProtocolDecision(failEvent);
4246
+ input.dispatchedChild.closed = true;
4247
+ input.dispatchedChild.failure = dispatchWaveFailureFromEvent(decision.intent, failEvent);
4248
+ if (DogpileError.isInstance(enrichedError)) throw enrichedError;
4249
+ throw new DogpileError({
4250
+ code: "invalid-configuration",
4251
+ message: error instanceof Error ? error.message : String(error),
4252
+ retryable: false,
4253
+ detail: {
4254
+ kind: "delegate-validation",
4255
+ path: "decision",
4256
+ reason: "child-run-failed"
4257
+ }
4258
+ });
4259
+ }
4260
+ if (childDeadlineTimer !== void 0) clearTimeout(childDeadlineTimer);
4261
+ removeParentAbortListener?.();
4262
+ input.recordSubRunCost(subResult.cost);
4263
+ const completedEvent = {
4264
+ type: "sub-run-completed",
4265
+ runId: input.parentRunId,
4266
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4267
+ childRunId,
4268
+ parentRunId: input.parentRunId,
4269
+ parentDecisionId: input.parentDecisionId,
4270
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
4271
+ subResult
4272
+ };
4273
+ parentEmit(completedEvent);
4274
+ input.recordProtocolDecision(completedEvent);
4275
+ input.dispatchedChild.closed = true;
4276
+ if (parentSignal?.aborted) {
4277
+ const abortMarker = {
4278
+ type: "sub-run-parent-aborted",
4279
+ runId: input.parentRunId,
4280
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4281
+ childRunId,
4282
+ parentRunId: input.parentRunId,
4283
+ reason: "parent-aborted"
4284
+ };
4285
+ parentEmit(abortMarker);
4286
+ input.recordProtocolDecision(abortMarker);
4287
+ throw enrichAbortErrorWithParentReason(createAbortErrorFromSignal(parentSignal, options.model.id), parentSignal);
4288
+ }
4289
+ const decisionAsJson = {
4290
+ type: "delegate",
4291
+ protocol: decision.protocol,
4292
+ intent: decision.intent,
4293
+ ...decision.model !== void 0 ? { model: decision.model } : {},
4294
+ ...decision.budget !== void 0 ? { budget: decision.budget } : {}
4295
+ };
4296
+ const taggedText = renderSubRunResult(childRunId, subResult);
4297
+ input.transcript.push({
4298
+ agentId: `sub-run:${childRunId}`,
4299
+ role: "delegate-result",
4300
+ input: JSON.stringify(decisionAsJson),
4301
+ output: taggedText
4302
+ });
4303
+ const coordinatorAgent = options.agents[0];
4304
+ return {
4305
+ nextInput: `${buildCoordinatorPlanInput(input.options.intent, coordinatorAgent ?? {
4306
+ id: "coordinator",
4307
+ role: "coordinator"
4308
+ })}\n\n${taggedText}\n\nUsing the sub-run result above, decide the next step (participate or delegate).`,
4309
+ taggedText,
4310
+ completedAtMs: Date.now()
4311
+ };
4312
+ }
4313
+ /**
4314
+ * D-17 prompt-injection helper. Renders a child `RunResult` as the canonical
4315
+ * tagged-result block injected into the parent coordinator's next prompt.
4316
+ *
4317
+ * Format:
4318
+ * `[sub-run <childRunId>]: <output>`
4319
+ * `[sub-run <childRunId> stats]: turns=<N> costUsd=<X> durationMs=<Y>`
4320
+ *
4321
+ * The stats line is a soft contract — field names stable, ordering stable.
4322
+ */
4323
+ function renderSubRunResult(childRunId, subResult) {
4324
+ const turns = subResult.transcript.length;
4325
+ const costUsd = subResult.cost.usd ?? 0;
4326
+ const startedAt = eventTimestamp(subResult.trace.events[0]);
4327
+ const endedAt = eventTimestamp(subResult.trace.events.at(-1));
4328
+ const durationMs = startedAt && endedAt ? Math.max(0, Date.parse(endedAt) - Date.parse(startedAt)) : 0;
4329
+ return [`[sub-run ${childRunId}]: ${subResult.output}`, `[sub-run ${childRunId} stats]: turns=${turns} costUsd=${costUsd} durationMs=${durationMs}`].join("\n");
4330
+ }
4331
+ function eventTimestamp(event) {
4332
+ if (event === void 0) return void 0;
4333
+ if ("at" in event) return event.at;
4334
+ return event.type === "model-response" ? event.completedAt : event.startedAt;
4335
+ }
4336
+ /**
4337
+ * Build a JSON-serializable {@link Trace} for `sub-run-failed.partialTrace`
4338
+ * from a buffered tee of child emits. Keeps `runProtocol`'s error contract
4339
+ * unchanged — Plan 03 step 8.
4340
+ */
4341
+ function buildPartialTrace(input) {
4342
+ const protocolName = typeof input.protocol === "string" ? input.protocol : input.protocol.kind;
4343
+ const protocolConfig = typeof input.protocol === "string" ? { kind: input.protocol } : input.protocol;
4344
+ return {
4345
+ schemaVersion: "1.0",
4346
+ runId: input.childRunId,
4347
+ protocol: protocolName,
4348
+ tier: input.tier,
4349
+ modelProviderId: input.modelProviderId,
4350
+ agentsUsed: input.agents,
4351
+ inputs: createReplayTraceRunInputs({
4352
+ intent: input.intent,
4353
+ protocol: protocolConfig,
4354
+ tier: input.tier,
4355
+ modelProviderId: input.modelProviderId,
4356
+ agents: input.agents,
4357
+ temperature: input.temperature
4358
+ }),
4359
+ budget: createReplayTraceBudget({
4360
+ tier: input.tier,
4361
+ ...input.childTimeoutMs !== void 0 ? { caps: { timeoutMs: input.childTimeoutMs } } : {}
4362
+ }),
4363
+ budgetStateChanges: createReplayTraceBudgetStateChanges(input.events),
4364
+ seed: createReplayTraceSeed(input.seed),
4365
+ protocolDecisions: [],
4366
+ providerCalls: [],
4367
+ finalOutput: {
4368
+ kind: "replay-trace-final-output",
4369
+ output: "",
4370
+ cost: emptyCost(),
4371
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4372
+ transcript: createTranscriptLink([])
4373
+ },
4374
+ events: input.events,
4375
+ transcript: []
4376
+ };
4377
+ }
4378
+ /**
4379
+ * BUDGET-01 / D-08: when a child sub-run threw because the parent's signal
4380
+ * aborted, lock the `detail.reason` discriminator on the resulting
4381
+ * `code: "aborted"` error. Preserves any pre-existing detail keys (e.g.,
4382
+ * `detail.status: "cancelled"` attached by `createStreamCancellationError`).
4383
+ *
4384
+ * No-op when:
4385
+ * - parent.signal is undefined or not aborted (child failure was unrelated)
4386
+ * - error is not a DogpileError with `code: "aborted"`
4387
+ * - error already has a `detail.reason` set (preserve upstream classification)
4388
+ */
4389
+ function enrichAbortErrorWithParentReason(error, parentSignal) {
4390
+ if (parentSignal === void 0 || !parentSignal.aborted) return error;
4391
+ if (!DogpileError.isInstance(error) || error.code !== "aborted") return error;
4392
+ const existingDetail = error.detail ?? {};
4393
+ if (existingDetail["reason"] !== void 0) return error;
4394
+ const reason = classifyAbortReason(parentSignal.reason);
4395
+ return new DogpileError({
4396
+ code: "aborted",
4397
+ message: error.message,
4398
+ retryable: error.retryable ?? false,
4399
+ ...error.providerId !== void 0 ? { providerId: error.providerId } : {},
4400
+ detail: {
4401
+ ...existingDetail,
4402
+ reason
4403
+ },
4404
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
4405
+ });
4406
+ }
4407
+ function enrichProviderTimeoutSource(error, context) {
4408
+ if (!DogpileError.isInstance(error) || error.code !== "provider-timeout") return error;
4409
+ const existingDetail = error.detail ?? {};
4410
+ if (existingDetail["source"] !== void 0) return error;
4411
+ const source = classifyChildTimeoutSource(error, {
4412
+ ...context,
4413
+ isProviderError: true
4414
+ });
4415
+ return new DogpileError({
4416
+ code: "provider-timeout",
4417
+ message: error.message,
4418
+ retryable: error.retryable ?? true,
4419
+ ...error.providerId !== void 0 ? { providerId: error.providerId } : {},
4420
+ detail: {
4421
+ ...existingDetail,
4422
+ source
4423
+ },
4424
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
4425
+ });
4426
+ }
4427
+ function errorPayloadFromUnknown(error, failedDecision) {
4428
+ if (DogpileError.isInstance(error)) {
4429
+ const detail = {
4430
+ ...error.detail ?? {},
4431
+ failedDecision
4432
+ };
4433
+ return {
4434
+ code: error.code,
4435
+ message: error.message,
4436
+ ...error.providerId !== void 0 ? { providerId: error.providerId } : {},
4437
+ detail
4438
+ };
4439
+ }
4440
+ return {
4441
+ code: "invalid-configuration",
4442
+ message: error instanceof Error ? error.message : String(error),
4443
+ detail: { failedDecision }
4444
+ };
4445
+ }
3038
4446
  //#endregion
3039
4447
  //#region src/runtime/sequential.ts
3040
4448
  async function runSequential(options) {
@@ -3173,7 +4581,8 @@ async function runSequential(options) {
3173
4581
  });
3174
4582
  if (stopIfNeeded()) break;
3175
4583
  }
3176
- const output = [...transcript].reverse().find((entry) => isParticipatingDecision(entry.decision))?.output ?? "";
4584
+ const reversed = [...transcript].reverse();
4585
+ const output = reversed.find((entry) => isParticipatingDecision(entry.decision))?.output ?? reversed.find((entry) => entry.decision === void 0)?.output ?? "";
3177
4586
  throwIfAborted(options.signal, options.model.id);
3178
4587
  const final = {
3179
4588
  type: "final",
@@ -3187,44 +4596,45 @@ async function runSequential(options) {
3187
4596
  emit(final);
3188
4597
  recordProtocolDecision(final, { transcriptEntryCount: transcript.length });
3189
4598
  const finalEvent = events.at(-1);
4599
+ const trace = {
4600
+ schemaVersion: "1.0",
4601
+ runId,
4602
+ protocol: "sequential",
4603
+ tier: options.tier,
4604
+ modelProviderId: options.model.id,
4605
+ agentsUsed: activeAgents,
4606
+ inputs: createReplayTraceRunInputs({
4607
+ intent: options.intent,
4608
+ protocol: options.protocol,
4609
+ tier: options.tier,
4610
+ modelProviderId: options.model.id,
4611
+ agents: activeAgents,
4612
+ temperature: options.temperature
4613
+ }),
4614
+ budget: createReplayTraceBudget({
4615
+ tier: options.tier,
4616
+ ...options.budget ? { caps: options.budget } : {},
4617
+ ...options.terminate ? { termination: options.terminate } : {}
4618
+ }),
4619
+ budgetStateChanges: createReplayTraceBudgetStateChanges(events),
4620
+ seed: createReplayTraceSeed(options.seed),
4621
+ protocolDecisions,
4622
+ providerCalls,
4623
+ finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? events[0] ?? {
4624
+ type: "final",
4625
+ runId,
4626
+ at: "",
4627
+ output,
4628
+ cost: totalCost,
4629
+ transcript: createTranscriptLink(transcript)
4630
+ }),
4631
+ events,
4632
+ transcript
4633
+ };
3190
4634
  return {
3191
4635
  output,
3192
4636
  eventLog: createRunEventLog(runId, "sequential", events),
3193
- trace: {
3194
- schemaVersion: "1.0",
3195
- runId,
3196
- protocol: "sequential",
3197
- tier: options.tier,
3198
- modelProviderId: options.model.id,
3199
- agentsUsed: activeAgents,
3200
- inputs: createReplayTraceRunInputs({
3201
- intent: options.intent,
3202
- protocol: options.protocol,
3203
- tier: options.tier,
3204
- modelProviderId: options.model.id,
3205
- agents: activeAgents,
3206
- temperature: options.temperature
3207
- }),
3208
- budget: createReplayTraceBudget({
3209
- tier: options.tier,
3210
- ...options.budget ? { caps: options.budget } : {},
3211
- ...options.terminate ? { termination: options.terminate } : {}
3212
- }),
3213
- budgetStateChanges: createReplayTraceBudgetStateChanges(events),
3214
- seed: createReplayTraceSeed(options.seed),
3215
- protocolDecisions,
3216
- providerCalls,
3217
- finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? events[0] ?? {
3218
- type: "final",
3219
- runId,
3220
- at: "",
3221
- output,
3222
- cost: totalCost,
3223
- transcript: createTranscriptLink(transcript)
3224
- }),
3225
- events,
3226
- transcript
3227
- },
4637
+ trace,
3228
4638
  transcript,
3229
4639
  usage: createRunUsage(totalCost),
3230
4640
  metadata: createRunMetadata({
@@ -3242,7 +4652,8 @@ async function runSequential(options) {
3242
4652
  cost: totalCost,
3243
4653
  events
3244
4654
  }),
3245
- cost: totalCost
4655
+ cost: totalCost,
4656
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
3246
4657
  };
3247
4658
  function stopIfNeeded() {
3248
4659
  throwIfAborted(options.signal, options.model.id);
@@ -3461,44 +4872,45 @@ async function runShared(options) {
3461
4872
  emit(final);
3462
4873
  recordProtocolDecision(final, { transcriptEntryCount: transcript.length });
3463
4874
  const finalEvent = events.at(-1);
4875
+ const trace = {
4876
+ schemaVersion: "1.0",
4877
+ runId,
4878
+ protocol: "shared",
4879
+ tier: options.tier,
4880
+ modelProviderId: options.model.id,
4881
+ agentsUsed: activeAgents,
4882
+ inputs: createReplayTraceRunInputs({
4883
+ intent: options.intent,
4884
+ protocol: options.protocol,
4885
+ tier: options.tier,
4886
+ modelProviderId: options.model.id,
4887
+ agents: activeAgents,
4888
+ temperature: options.temperature
4889
+ }),
4890
+ budget: createReplayTraceBudget({
4891
+ tier: options.tier,
4892
+ ...options.budget ? { caps: options.budget } : {},
4893
+ ...options.terminate ? { termination: options.terminate } : {}
4894
+ }),
4895
+ budgetStateChanges: createReplayTraceBudgetStateChanges(events),
4896
+ seed: createReplayTraceSeed(options.seed),
4897
+ protocolDecisions,
4898
+ providerCalls,
4899
+ finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
4900
+ type: "final",
4901
+ runId,
4902
+ at: "",
4903
+ output,
4904
+ cost: totalCost,
4905
+ transcript: createTranscriptLink(transcript)
4906
+ }),
4907
+ events,
4908
+ transcript
4909
+ };
3464
4910
  return {
3465
4911
  output,
3466
4912
  eventLog: createRunEventLog(runId, "shared", events),
3467
- trace: {
3468
- schemaVersion: "1.0",
3469
- runId,
3470
- protocol: "shared",
3471
- tier: options.tier,
3472
- modelProviderId: options.model.id,
3473
- agentsUsed: activeAgents,
3474
- inputs: createReplayTraceRunInputs({
3475
- intent: options.intent,
3476
- protocol: options.protocol,
3477
- tier: options.tier,
3478
- modelProviderId: options.model.id,
3479
- agents: activeAgents,
3480
- temperature: options.temperature
3481
- }),
3482
- budget: createReplayTraceBudget({
3483
- tier: options.tier,
3484
- ...options.budget ? { caps: options.budget } : {},
3485
- ...options.terminate ? { termination: options.terminate } : {}
3486
- }),
3487
- budgetStateChanges: createReplayTraceBudgetStateChanges(events),
3488
- seed: createReplayTraceSeed(options.seed),
3489
- protocolDecisions,
3490
- providerCalls,
3491
- finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
3492
- type: "final",
3493
- runId,
3494
- at: "",
3495
- output,
3496
- cost: totalCost,
3497
- transcript: createTranscriptLink(transcript)
3498
- }),
3499
- events,
3500
- transcript
3501
- },
4913
+ trace,
3502
4914
  transcript,
3503
4915
  usage: createRunUsage(totalCost),
3504
4916
  metadata: createRunMetadata({
@@ -3516,7 +4928,8 @@ async function runShared(options) {
3516
4928
  cost: totalCost,
3517
4929
  events
3518
4930
  }),
3519
- cost: totalCost
4931
+ cost: totalCost,
4932
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
3520
4933
  };
3521
4934
  function stopIfNeeded() {
3522
4935
  throwIfAborted(options.signal, options.model.id);
@@ -3572,7 +4985,17 @@ function responseCost(response) {
3572
4985
  };
3573
4986
  }
3574
4987
  //#endregion
4988
+ //#region src/runtime/tracing.ts
4989
+ var DOGPILE_SPAN_NAMES = {
4990
+ RUN: "dogpile.run",
4991
+ SUB_RUN: "dogpile.sub-run",
4992
+ AGENT_TURN: "dogpile.agent-turn",
4993
+ MODEL_CALL: "dogpile.model-call"
4994
+ };
4995
+ //#endregion
3575
4996
  //#region src/runtime/engine.ts
4997
+ var DEFAULT_MAX_DEPTH = 4;
4998
+ var DEFAULT_MAX_CONCURRENT_CHILDREN = 4;
3576
4999
  var defaultHighLevelProtocol = "sequential";
3577
5000
  var defaultHighLevelTier = "balanced";
3578
5001
  /**
@@ -3593,9 +5016,20 @@ function createEngine(options) {
3593
5016
  const temperature = options.temperature ?? tierTemperature(options.tier);
3594
5017
  const agents = orderAgentsForTemperature(options.agents ?? defaultAgents(), temperature, options.seed);
3595
5018
  const terminate = options.terminate ?? (options.budget ? conditionFromBudget(options.budget) : void 0);
5019
+ const engineMaxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
5020
+ const engineMaxConcurrentChildren = options.maxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN;
5021
+ const engineOnChildFailure = options.onChildFailure;
3596
5022
  return {
3597
- run(intent) {
5023
+ run(intent, runOptions) {
3598
5024
  validateMissionIntent(intent);
5025
+ validateRunCallOptions(runOptions);
5026
+ validateProviderLocality(options.model, "model");
5027
+ const effectiveMaxDepth = Math.min(engineMaxDepth, runOptions?.maxDepth ?? Number.POSITIVE_INFINITY);
5028
+ assertRunDoesNotRaiseEngineMax("maxConcurrentChildren", runOptions?.maxConcurrentChildren, engineMaxConcurrentChildren);
5029
+ const effectiveMaxConcurrentChildren = Math.min(engineMaxConcurrentChildren, runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY);
5030
+ const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
5031
+ const startedAtMs = Date.now();
5032
+ const parentDeadlineMs = options.budget?.timeoutMs !== void 0 ? startedAtMs + options.budget.timeoutMs : void 0;
3599
5033
  return runNonStreamingProtocol({
3600
5034
  intent,
3601
5035
  protocol,
@@ -3609,11 +5043,26 @@ function createEngine(options) {
3609
5043
  ...options.signal !== void 0 ? { signal: options.signal } : {},
3610
5044
  ...terminate ? { terminate } : {},
3611
5045
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {},
3612
- ...options.evaluate ? { evaluate: options.evaluate } : {}
5046
+ ...options.evaluate ? { evaluate: options.evaluate } : {},
5047
+ ...options.tracer ? { tracer: options.tracer } : {},
5048
+ ...options.metricsHook ? { metricsHook: options.metricsHook } : {},
5049
+ ...options.logger ? { logger: options.logger } : {},
5050
+ currentDepth: 0,
5051
+ effectiveMaxDepth,
5052
+ effectiveMaxConcurrentChildren,
5053
+ onChildFailure,
5054
+ ...parentDeadlineMs !== void 0 ? { parentDeadlineMs } : {},
5055
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {}
3613
5056
  });
3614
5057
  },
3615
- stream(intent) {
5058
+ stream(intent, runOptions) {
3616
5059
  validateMissionIntent(intent);
5060
+ validateRunCallOptions(runOptions);
5061
+ validateProviderLocality(options.model, "model");
5062
+ const effectiveMaxDepth = Math.min(engineMaxDepth, runOptions?.maxDepth ?? Number.POSITIVE_INFINITY);
5063
+ assertRunDoesNotRaiseEngineMax("maxConcurrentChildren", runOptions?.maxConcurrentChildren, engineMaxConcurrentChildren);
5064
+ const effectiveMaxConcurrentChildren = Math.min(engineMaxConcurrentChildren, runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY);
5065
+ const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
3617
5066
  const pendingEvents = [];
3618
5067
  const pendingResolvers = [];
3619
5068
  const emittedEvents = [];
@@ -3630,7 +5079,10 @@ function createEngine(options) {
3630
5079
  const abortRace = createAbortRace(abortController.signal, options.model.id);
3631
5080
  let complete = false;
3632
5081
  let lastRunId = "";
5082
+ let rootRunId;
3633
5083
  let pendingFinalEvent;
5084
+ let activeAbortDrain;
5085
+ const failureInstancesByChildRunId = /* @__PURE__ */ new Map();
3634
5086
  let status = "running";
3635
5087
  let resolveResult;
3636
5088
  let rejectResult;
@@ -3676,6 +5128,8 @@ function createEngine(options) {
3676
5128
  async function execute() {
3677
5129
  if (status !== "running") return;
3678
5130
  try {
5131
+ const streamStartedAtMs = Date.now();
5132
+ const streamParentDeadlineMs = options.budget?.timeoutMs !== void 0 ? streamStartedAtMs + options.budget.timeoutMs : void 0;
3679
5133
  const baseResult = await abortRace.run(runProtocol({
3680
5134
  intent,
3681
5135
  protocol,
@@ -3688,17 +5142,35 @@ function createEngine(options) {
3688
5142
  ...options.seed !== void 0 ? { seed: options.seed } : {},
3689
5143
  signal: abortController.signal,
3690
5144
  ...terminate ? { terminate } : {},
5145
+ currentDepth: 0,
5146
+ effectiveMaxDepth,
5147
+ effectiveMaxConcurrentChildren,
5148
+ onChildFailure,
5149
+ ...streamParentDeadlineMs !== void 0 ? { parentDeadlineMs: streamParentDeadlineMs } : {},
5150
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {},
5151
+ ...options.tracer ? { tracer: options.tracer } : {},
5152
+ ...options.metricsHook ? { metricsHook: options.metricsHook } : {},
5153
+ ...options.logger ? { logger: options.logger } : {},
5154
+ streamEvents: true,
3691
5155
  emit(event) {
3692
5156
  if (status !== "running") return;
5157
+ const parentRunIds = event.parentRunIds;
5158
+ if (rootRunId === void 0 && parentRunIds === void 0) rootRunId = event.runId;
3693
5159
  lastRunId = event.runId;
3694
- if (event.type === "final") {
5160
+ if (event.type === "final" && event.runId === rootRunId) {
3695
5161
  pendingFinalEvent = event;
3696
5162
  return;
3697
5163
  }
3698
5164
  publish(event);
3699
- }
5165
+ },
5166
+ registerAbortDrain(drain) {
5167
+ activeAbortDrain = drain;
5168
+ },
5169
+ failureInstancesByChildRunId
3700
5170
  }));
3701
5171
  if (status !== "running") return;
5172
+ const terminalThrow = resolveRuntimeTerminalThrow(baseResult.trace, failureInstancesByChildRunId);
5173
+ if (terminalThrow) throw terminalThrow;
3702
5174
  const finalizedResult = await abortRace.run(applyRunEvaluation(baseResult, options.evaluate));
3703
5175
  if (status !== "running") return;
3704
5176
  const finalEvent = finalizedResult.trace.events.at(-1);
@@ -3711,6 +5183,10 @@ function createEngine(options) {
3711
5183
  if (isStreamHandleStatus(status, "cancelled")) return;
3712
5184
  const runtimeError = timeoutLifecycle.translateError(error);
3713
5185
  status = isCancellationError(runtimeError) ? "cancelled" : "failed";
5186
+ if (shouldPublishAborted(runtimeError)) {
5187
+ activeAbortDrain?.(runtimeError);
5188
+ publish(createStreamAbortedEvent(runtimeError, lastRunId));
5189
+ }
3714
5190
  publish(createStreamErrorEvent(runtimeError, lastRunId));
3715
5191
  closeStream();
3716
5192
  rejectResult(runtimeError);
@@ -3719,15 +5195,18 @@ function createEngine(options) {
3719
5195
  function cancelRun(cause) {
3720
5196
  if (status !== "running") return;
3721
5197
  const error = createStreamCancellationError(options.model.id, cause);
3722
- status = "cancelled";
3723
5198
  abortController.abort(error);
5199
+ activeAbortDrain?.(error);
5200
+ publish(createStreamAbortedEvent(error, lastRunId));
3724
5201
  publish(createStreamErrorEvent(error, lastRunId));
5202
+ status = "cancelled";
3725
5203
  closeStream();
3726
5204
  rejectResult(error);
3727
5205
  }
3728
5206
  function closeStream() {
3729
5207
  if (complete) return;
3730
5208
  complete = true;
5209
+ failureInstancesByChildRunId.clear();
3731
5210
  removeCallerAbortListener();
3732
5211
  timeoutLifecycle.cleanup();
3733
5212
  abortRace.cleanup();
@@ -3783,7 +5262,8 @@ function createNonStreamingAbortLifecycle(options) {
3783
5262
  const timeoutLifecycle = createTimeoutAbortLifecycle({
3784
5263
  abortController,
3785
5264
  timeoutMs: options.timeoutMs,
3786
- providerId: options.providerId
5265
+ providerId: options.providerId,
5266
+ timeoutErrorSource: options.timeoutErrorSource ?? "runtime"
3787
5267
  });
3788
5268
  const abortRace = createAbortRace(abortController.signal, options.providerId);
3789
5269
  const removeCallerAbortListener = wireCallerAbortSignal(options.callerSignal, abortController, () => {
@@ -3811,7 +5291,11 @@ function createTimeoutAbortLifecycle(options) {
3811
5291
  },
3812
5292
  cleanup() {}
3813
5293
  };
3814
- const timeoutError = createTimeoutError(options.providerId, options.timeoutMs);
5294
+ const timeoutSource = classifyChildTimeoutSource(void 0, {
5295
+ ...options.timeoutErrorSource === "engine" ? { engineDefaultTimeoutMs: options.timeoutMs } : {},
5296
+ isProviderError: false
5297
+ });
5298
+ const timeoutError = options.timeoutErrorSource === "engine" && timeoutSource === "engine" ? createEngineDeadlineTimeoutError(options.providerId, options.timeoutMs) : createTimeoutError(options.providerId, options.timeoutMs);
3815
5299
  const timeoutId = setTimeout(() => {
3816
5300
  options.abortController.abort(timeoutError);
3817
5301
  }, options.timeoutMs);
@@ -3879,6 +5363,23 @@ function timeoutMsFromTermination(condition) {
3879
5363
  function readAbortSignalReason(signal) {
3880
5364
  return signal?.aborted ? signal.reason : void 0;
3881
5365
  }
5366
+ function createStreamAbortedEvent(error, runId) {
5367
+ return {
5368
+ type: "aborted",
5369
+ runId,
5370
+ at: (/* @__PURE__ */ new Date()).toISOString(),
5371
+ reason: streamAbortedReason(error)
5372
+ };
5373
+ }
5374
+ function shouldPublishAborted(error) {
5375
+ return DogpileError.isInstance(error) && (error.code === "aborted" || error.code === "timeout");
5376
+ }
5377
+ function streamAbortedReason(error) {
5378
+ if (DogpileError.isInstance(error)) {
5379
+ if (error.code === "timeout" || error.detail?.["reason"] === "timeout") return "timeout";
5380
+ }
5381
+ return "parent-aborted";
5382
+ }
3882
5383
  function createStreamErrorEvent(error, runId) {
3883
5384
  if (DogpileError.isInstance(error)) return {
3884
5385
  type: "error",
@@ -3910,11 +5411,336 @@ function dogpileErrorStreamDetail(error) {
3910
5411
  if (error.detail !== void 0) for (const [key, value] of Object.entries(error.detail)) detail[key] = value;
3911
5412
  return detail;
3912
5413
  }
5414
+ function openRunTracing(options) {
5415
+ if (!options.tracer) return;
5416
+ const runSpan = options.tracer.startSpan(DOGPILE_SPAN_NAMES.RUN, {
5417
+ ...options.parentSpan ? { parent: options.parentSpan } : {},
5418
+ attributes: {
5419
+ "dogpile.run.protocol": options.protocolKind,
5420
+ "dogpile.run.tier": String(options.tier),
5421
+ "dogpile.run.intent": options.intent.slice(0, 200)
5422
+ }
5423
+ });
5424
+ return {
5425
+ tracer: options.tracer,
5426
+ runSpan,
5427
+ subRunSpans: /* @__PURE__ */ new Map(),
5428
+ agentTurnSpans: /* @__PURE__ */ new Map(),
5429
+ modelCallSpans: /* @__PURE__ */ new Map(),
5430
+ pendingModelRequests: /* @__PURE__ */ new Map(),
5431
+ agentTurnCounters: /* @__PURE__ */ new Map(),
5432
+ turnAccumByAgent: /* @__PURE__ */ new Map(),
5433
+ agentIds: /* @__PURE__ */ new Set(),
5434
+ turnCount: 0,
5435
+ lastCost: emptyCost()
5436
+ };
5437
+ }
5438
+ function openRunMetrics(options) {
5439
+ if (!options.metricsHook) return;
5440
+ return {
5441
+ metricsHook: options.metricsHook,
5442
+ logger: options.logger,
5443
+ startedAtMs: Date.now(),
5444
+ subRunStartTimes: /* @__PURE__ */ new Map(),
5445
+ totalCost: emptyCost(),
5446
+ nestedCost: emptyCost(),
5447
+ turns: 0
5448
+ };
5449
+ }
5450
+ function routeMetricsError(err, logger) {
5451
+ const msg = err instanceof Error ? err.message : String(err);
5452
+ try {
5453
+ if (logger !== void 0) logger.error("dogpile:metricsHook threw", { error: msg });
5454
+ else console.error("dogpile:metricsHook threw", { error: msg });
5455
+ } catch {}
5456
+ }
5457
+ function fireHook(callback, snapshot, logger) {
5458
+ if (!callback) return;
5459
+ try {
5460
+ const result = callback(snapshot);
5461
+ if (result && typeof result.catch === "function") result.catch((err) => {
5462
+ routeMetricsError(err, logger);
5463
+ });
5464
+ } catch (err) {
5465
+ routeMetricsError(err, logger);
5466
+ }
5467
+ }
5468
+ function buildRunSnapshot(result, startedAtMs) {
5469
+ const nestedCosts = nestedSubRunCosts(result);
5470
+ const outcome = result.trace.events.find((event) => event.type === "budget-stop") !== void 0 ? "budget-stopped" : "completed";
5471
+ const totalInputTokens = result.cost.inputTokens;
5472
+ const totalOutputTokens = result.cost.outputTokens;
5473
+ const totalCostUsd = result.cost.usd;
5474
+ return {
5475
+ outcome,
5476
+ inputTokens: totalInputTokens - nestedCosts.reduce((sum, cost) => sum + cost.inputTokens, 0),
5477
+ outputTokens: totalOutputTokens - nestedCosts.reduce((sum, cost) => sum + cost.outputTokens, 0),
5478
+ costUsd: totalCostUsd - nestedCosts.reduce((sum, cost) => sum + cost.usd, 0),
5479
+ totalInputTokens,
5480
+ totalOutputTokens,
5481
+ totalCostUsd,
5482
+ turns: result.trace.events.filter((event) => event.type === "agent-turn").length,
5483
+ durationMs: Date.now() - startedAtMs
5484
+ };
5485
+ }
5486
+ function buildSubRunSnapshot(subResult, durationMs) {
5487
+ const nestedCosts = nestedSubRunCosts(subResult);
5488
+ const outcome = subResult.trace.events.find((event) => event.type === "budget-stop") !== void 0 ? "budget-stopped" : "completed";
5489
+ const totalInputTokens = subResult.cost.inputTokens;
5490
+ const totalOutputTokens = subResult.cost.outputTokens;
5491
+ const totalCostUsd = subResult.cost.usd;
5492
+ return {
5493
+ outcome,
5494
+ inputTokens: totalInputTokens - nestedCosts.reduce((sum, cost) => sum + cost.inputTokens, 0),
5495
+ outputTokens: totalOutputTokens - nestedCosts.reduce((sum, cost) => sum + cost.outputTokens, 0),
5496
+ costUsd: totalCostUsd - nestedCosts.reduce((sum, cost) => sum + cost.usd, 0),
5497
+ totalInputTokens,
5498
+ totalOutputTokens,
5499
+ totalCostUsd,
5500
+ turns: subResult.trace.events.filter((event) => event.type === "agent-turn").length,
5501
+ durationMs
5502
+ };
5503
+ }
5504
+ function nestedSubRunCosts(result) {
5505
+ return result.trace.events.flatMap((event) => {
5506
+ if (event.type === "sub-run-completed") return [event.subResult.cost];
5507
+ if (event.type === "sub-run-failed") return [event.partialCost];
5508
+ return [];
5509
+ });
5510
+ }
5511
+ function subtractCost(total, nested) {
5512
+ return {
5513
+ usd: total.usd - nested.usd,
5514
+ inputTokens: total.inputTokens - nested.inputTokens,
5515
+ outputTokens: total.outputTokens - nested.outputTokens,
5516
+ totalTokens: total.totalTokens - nested.totalTokens
5517
+ };
5518
+ }
5519
+ function handleMetricsEvent(state, event) {
5520
+ if (event.parentRunIds !== void 0) return;
5521
+ switch (event.type) {
5522
+ case "agent-turn":
5523
+ state.totalCost = event.cost;
5524
+ state.turns += 1;
5525
+ break;
5526
+ case "broadcast":
5527
+ case "budget-stop":
5528
+ case "final":
5529
+ state.totalCost = event.cost;
5530
+ break;
5531
+ case "sub-run-started":
5532
+ state.subRunStartTimes.set(event.childRunId, Date.now());
5533
+ break;
5534
+ case "sub-run-completed": {
5535
+ state.totalCost = addCost(state.totalCost, event.subResult.cost);
5536
+ state.nestedCost = addCost(state.nestedCost, event.subResult.cost);
5537
+ const startMs = state.subRunStartTimes.get(event.childRunId);
5538
+ const durationMs = startMs !== void 0 ? Date.now() - startMs : 0;
5539
+ state.subRunStartTimes.delete(event.childRunId);
5540
+ const snapshot = buildSubRunSnapshot(event.subResult, durationMs);
5541
+ fireHook(state.metricsHook.onSubRunComplete, snapshot, state.logger);
5542
+ break;
5543
+ }
5544
+ case "sub-run-failed":
5545
+ state.totalCost = addCost(state.totalCost, event.partialCost);
5546
+ state.nestedCost = addCost(state.nestedCost, event.partialCost);
5547
+ state.subRunStartTimes.delete(event.childRunId);
5548
+ break;
5549
+ default: break;
5550
+ }
5551
+ }
5552
+ function closeRunMetrics(state, result) {
5553
+ if (result !== void 0) {
5554
+ const snapshot = buildRunSnapshot(result, state.startedAtMs);
5555
+ fireHook(state.metricsHook.onRunComplete, snapshot, state.logger);
5556
+ return;
5557
+ }
5558
+ const ownCost = subtractCost(state.totalCost, state.nestedCost);
5559
+ const snapshot = {
5560
+ outcome: "aborted",
5561
+ inputTokens: ownCost.inputTokens,
5562
+ outputTokens: ownCost.outputTokens,
5563
+ costUsd: ownCost.usd,
5564
+ totalInputTokens: state.totalCost.inputTokens,
5565
+ totalOutputTokens: state.totalCost.outputTokens,
5566
+ totalCostUsd: state.totalCost.usd,
5567
+ turns: state.turns,
5568
+ durationMs: Date.now() - state.startedAtMs
5569
+ };
5570
+ fireHook(state.metricsHook.onRunComplete, snapshot, state.logger);
5571
+ }
5572
+ function handleTracingEvent(state, event) {
5573
+ if (event.parentRunIds !== void 0) return;
5574
+ if (state.runId === void 0) {
5575
+ state.runId = event.runId;
5576
+ state.runSpan.setAttribute("dogpile.run.id", event.runId);
5577
+ }
5578
+ switch (event.type) {
5579
+ case "model-request": {
5580
+ state.pendingModelRequests.set(event.callId, event);
5581
+ state.agentIds.add(event.agentId);
5582
+ if (!state.agentTurnSpans.has(event.agentId)) {
5583
+ const turnNumber = (state.agentTurnCounters.get(event.agentId) ?? 0) + 1;
5584
+ state.agentTurnCounters.set(event.agentId, turnNumber);
5585
+ const turnParent = state.subRunSpans.get(event.runId) ?? state.runSpan;
5586
+ const turnSpan = state.tracer.startSpan(DOGPILE_SPAN_NAMES.AGENT_TURN, {
5587
+ parent: turnParent,
5588
+ attributes: {
5589
+ "dogpile.agent.id": event.agentId,
5590
+ "dogpile.agent.role": event.role,
5591
+ "dogpile.turn.number": turnNumber,
5592
+ "dogpile.model.id": event.modelId
5593
+ }
5594
+ });
5595
+ state.agentTurnSpans.set(event.agentId, turnSpan);
5596
+ }
5597
+ const callParent = state.agentTurnSpans.get(event.agentId) ?? state.subRunSpans.get(event.runId) ?? state.runSpan;
5598
+ const callSpan = state.tracer.startSpan(DOGPILE_SPAN_NAMES.MODEL_CALL, {
5599
+ parent: callParent,
5600
+ attributes: {
5601
+ "dogpile.model.id": event.modelId,
5602
+ "dogpile.call.id": event.callId,
5603
+ "dogpile.provider.id": event.providerId
5604
+ }
5605
+ });
5606
+ state.modelCallSpans.set(event.callId, callSpan);
5607
+ break;
5608
+ }
5609
+ case "model-response": {
5610
+ const span = state.modelCallSpans.get(event.callId);
5611
+ if (span) {
5612
+ const inputTokens = event.response.usage?.inputTokens ?? 0;
5613
+ const outputTokens = event.response.usage?.outputTokens ?? 0;
5614
+ const responseCost = {
5615
+ usd: event.response.costUsd ?? 0,
5616
+ inputTokens,
5617
+ outputTokens,
5618
+ totalTokens: event.response.usage?.totalTokens ?? inputTokens + outputTokens
5619
+ };
5620
+ span.setAttribute("dogpile.model.input_tokens", inputTokens);
5621
+ span.setAttribute("dogpile.model.output_tokens", outputTokens);
5622
+ if (event.response.costUsd !== void 0) span.setAttribute("dogpile.model.cost_usd", event.response.costUsd);
5623
+ span.setStatus("ok");
5624
+ span.end();
5625
+ state.modelCallSpans.delete(event.callId);
5626
+ const accum = state.turnAccumByAgent.get(event.agentId) ?? {
5627
+ inputTokens: 0,
5628
+ outputTokens: 0,
5629
+ costUsd: 0
5630
+ };
5631
+ accum.inputTokens += inputTokens;
5632
+ accum.outputTokens += outputTokens;
5633
+ accum.costUsd += responseCost.usd;
5634
+ state.turnAccumByAgent.set(event.agentId, accum);
5635
+ state.lastCost = addCost(state.lastCost, responseCost);
5636
+ }
5637
+ state.pendingModelRequests.delete(event.callId);
5638
+ break;
5639
+ }
5640
+ case "agent-turn": {
5641
+ state.agentIds.add(event.agentId);
5642
+ state.turnCount += 1;
5643
+ state.lastCost = event.cost;
5644
+ const turnSpan = state.agentTurnSpans.get(event.agentId);
5645
+ if (turnSpan) {
5646
+ turnSpan.setAttribute("dogpile.agent.role", event.role);
5647
+ const accum = state.turnAccumByAgent.get(event.agentId);
5648
+ turnSpan.setAttribute("dogpile.turn.cost_usd", accum?.costUsd ?? 0);
5649
+ turnSpan.setAttribute("dogpile.turn.input_tokens", accum?.inputTokens ?? 0);
5650
+ turnSpan.setAttribute("dogpile.turn.output_tokens", accum?.outputTokens ?? 0);
5651
+ turnSpan.setStatus("ok");
5652
+ turnSpan.end();
5653
+ state.agentTurnSpans.delete(event.agentId);
5654
+ }
5655
+ state.turnAccumByAgent.delete(event.agentId);
5656
+ break;
5657
+ }
5658
+ case "broadcast":
5659
+ case "budget-stop":
5660
+ case "final":
5661
+ state.lastCost = event.cost;
5662
+ break;
5663
+ case "sub-run-started": {
5664
+ const span = state.tracer.startSpan(DOGPILE_SPAN_NAMES.SUB_RUN, {
5665
+ parent: state.runSpan,
5666
+ attributes: {
5667
+ "dogpile.sub_run.child_run_id": event.childRunId,
5668
+ "dogpile.sub_run.parent_run_id": event.parentRunId,
5669
+ "dogpile.sub_run.depth": event.depth
5670
+ }
5671
+ });
5672
+ state.subRunSpans.set(event.childRunId, span);
5673
+ break;
5674
+ }
5675
+ case "sub-run-completed": {
5676
+ const span = state.subRunSpans.get(event.childRunId);
5677
+ if (span) {
5678
+ span.setStatus("ok");
5679
+ span.end();
5680
+ state.subRunSpans.delete(event.childRunId);
5681
+ }
5682
+ break;
5683
+ }
5684
+ case "sub-run-failed": {
5685
+ const span = state.subRunSpans.get(event.childRunId);
5686
+ if (span) {
5687
+ span.setStatus("error", event.error.message);
5688
+ span.end();
5689
+ state.subRunSpans.delete(event.childRunId);
5690
+ }
5691
+ break;
5692
+ }
5693
+ default: break;
5694
+ }
5695
+ }
5696
+ function closeRunTracing(state, result, error) {
5697
+ if (error !== void 0) {
5698
+ if (state.runId !== void 0) state.runSpan.setAttribute("dogpile.run.id", state.runId);
5699
+ state.runSpan.setAttribute("dogpile.run.agent_count", state.agentIds.size);
5700
+ state.runSpan.setAttribute("dogpile.run.turn_count", state.turnCount);
5701
+ state.runSpan.setAttribute("dogpile.run.cost_usd", state.lastCost.usd);
5702
+ state.runSpan.setAttribute("dogpile.run.input_tokens", state.lastCost.inputTokens);
5703
+ state.runSpan.setAttribute("dogpile.run.output_tokens", state.lastCost.outputTokens);
5704
+ state.runSpan.setAttribute("dogpile.run.outcome", "aborted");
5705
+ state.runSpan.setStatus("error", error instanceof Error ? error.message : String(error));
5706
+ closeOpenTracingSpans(state);
5707
+ state.runSpan.end();
5708
+ return;
5709
+ }
5710
+ if (result === void 0) {
5711
+ closeOpenTracingSpans(state);
5712
+ state.runSpan.end();
5713
+ return;
5714
+ }
5715
+ const terminationReason = result.trace.events.find((event) => event.type === "budget-stop")?.reason;
5716
+ const outcome = terminationReason !== void 0 ? "budget-stopped" : "completed";
5717
+ state.runSpan.setAttribute("dogpile.run.id", result.trace.runId);
5718
+ state.runSpan.setAttribute("dogpile.run.agent_count", result.trace.agentsUsed.length);
5719
+ state.runSpan.setAttribute("dogpile.run.turn_count", result.trace.events.filter((event) => event.type === "agent-turn").length);
5720
+ state.runSpan.setAttribute("dogpile.run.cost_usd", result.cost.usd);
5721
+ state.runSpan.setAttribute("dogpile.run.input_tokens", result.cost.inputTokens);
5722
+ state.runSpan.setAttribute("dogpile.run.output_tokens", result.cost.outputTokens);
5723
+ state.runSpan.setAttribute("dogpile.run.outcome", outcome);
5724
+ if (terminationReason !== void 0) state.runSpan.setAttribute("dogpile.run.termination_reason", terminationReason);
5725
+ state.runSpan.setStatus("ok");
5726
+ closeOpenTracingSpans(state);
5727
+ state.runSpan.end();
5728
+ }
5729
+ function closeOpenTracingSpans(state) {
5730
+ for (const span of state.modelCallSpans.values()) span.end();
5731
+ state.modelCallSpans.clear();
5732
+ for (const span of state.agentTurnSpans.values()) span.end();
5733
+ state.agentTurnSpans.clear();
5734
+ for (const span of state.subRunSpans.values()) span.end();
5735
+ state.subRunSpans.clear();
5736
+ }
3913
5737
  async function runNonStreamingProtocol(options) {
5738
+ const failureInstancesByChildRunId = /* @__PURE__ */ new Map();
3914
5739
  const abortLifecycle = createNonStreamingAbortLifecycle({
3915
5740
  callerSignal: options.signal,
3916
5741
  timeoutMs: runtimeTimeoutMs(options),
3917
- providerId: options.model.id
5742
+ providerId: options.model.id,
5743
+ timeoutErrorSource: options.currentDepth !== void 0 && options.currentDepth > 0 && options.parentDeadlineMs === void 0 ? "engine" : "runtime"
3918
5744
  });
3919
5745
  try {
3920
5746
  const emittedEvents = [];
@@ -3923,7 +5749,8 @@ async function runNonStreamingProtocol(options) {
3923
5749
  ...abortLifecycle.signal !== void 0 ? { signal: abortLifecycle.signal } : {},
3924
5750
  emit(event) {
3925
5751
  emittedEvents.push(event);
3926
- }
5752
+ },
5753
+ failureInstancesByChildRunId
3927
5754
  }));
3928
5755
  const events = emittedEvents.length > 0 ? emittedEvents : result.trace.events;
3929
5756
  const trace = {
@@ -3942,12 +5769,16 @@ async function runNonStreamingProtocol(options) {
3942
5769
  events
3943
5770
  }),
3944
5771
  eventLog: createRunEventLog(trace.runId, trace.protocol, events),
3945
- trace
5772
+ trace,
5773
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
3946
5774
  };
5775
+ const terminalThrow = resolveRuntimeTerminalThrow(runResult.trace, failureInstancesByChildRunId);
5776
+ if (terminalThrow) throw terminalThrow;
3947
5777
  return canonicalizeRunResult(await abortLifecycle.run(applyRunEvaluation(runResult, options.evaluate)));
3948
5778
  } catch (error) {
3949
5779
  throw abortLifecycle.translateError(error);
3950
5780
  } finally {
5781
+ failureInstancesByChildRunId.clear();
3951
5782
  abortLifecycle.cleanup();
3952
5783
  }
3953
5784
  }
@@ -3977,7 +5808,39 @@ function finalEventWithEvaluation(event, evaluation) {
3977
5808
  evaluation
3978
5809
  };
3979
5810
  }
3980
- function runProtocol(options) {
5811
+ async function runProtocol(options) {
5812
+ const tracing = openRunTracing({
5813
+ ...options.tracer ? { tracer: options.tracer } : {},
5814
+ ...options.parentSpan ? { parentSpan: options.parentSpan } : {},
5815
+ intent: options.intent,
5816
+ protocolKind: options.protocol.kind,
5817
+ tier: options.tier
5818
+ });
5819
+ const metrics = openRunMetrics({
5820
+ ...options.metricsHook ? { metricsHook: options.metricsHook } : {},
5821
+ ...options.logger ? { logger: options.logger } : {}
5822
+ });
5823
+ const emitForProtocol = tracing || metrics || options.emit ? (event) => {
5824
+ if (tracing) handleTracingEvent(tracing, event);
5825
+ if (metrics) handleMetricsEvent(metrics, event);
5826
+ options.emit?.(event);
5827
+ } : void 0;
5828
+ const protocolOptions = tracing ? {
5829
+ ...options,
5830
+ subRunSpansByChildId: tracing.subRunSpans
5831
+ } : options;
5832
+ try {
5833
+ const result = await runProtocolInner(protocolOptions, emitForProtocol);
5834
+ if (tracing) closeRunTracing(tracing, result);
5835
+ if (metrics && (options.currentDepth === 0 || options.currentDepth === void 0)) closeRunMetrics(metrics, result);
5836
+ return result;
5837
+ } catch (error) {
5838
+ if (tracing) closeRunTracing(tracing, void 0, error);
5839
+ if (metrics && (options.currentDepth === 0 || options.currentDepth === void 0)) closeRunMetrics(metrics, void 0);
5840
+ throw error;
5841
+ }
5842
+ }
5843
+ function runProtocolInner(options, emitForProtocol) {
3981
5844
  switch (options.protocol.kind) {
3982
5845
  case "sequential": return runSequential({
3983
5846
  intent: options.intent,
@@ -3992,7 +5855,7 @@ function runProtocol(options) {
3992
5855
  ...options.signal !== void 0 ? { signal: options.signal } : {},
3993
5856
  ...options.terminate ? { terminate: options.terminate } : {},
3994
5857
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {},
3995
- ...options.emit ? { emit: options.emit } : {}
5858
+ ...emitForProtocol ? { emit: emitForProtocol } : {}
3996
5859
  });
3997
5860
  case "broadcast": return runBroadcast({
3998
5861
  intent: options.intent,
@@ -4007,7 +5870,7 @@ function runProtocol(options) {
4007
5870
  ...options.signal !== void 0 ? { signal: options.signal } : {},
4008
5871
  ...options.terminate ? { terminate: options.terminate } : {},
4009
5872
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {},
4010
- ...options.emit ? { emit: options.emit } : {}
5873
+ ...emitForProtocol ? { emit: emitForProtocol } : {}
4011
5874
  });
4012
5875
  case "coordinator": return runCoordinator({
4013
5876
  intent: options.intent,
@@ -4022,7 +5885,27 @@ function runProtocol(options) {
4022
5885
  ...options.signal !== void 0 ? { signal: options.signal } : {},
4023
5886
  ...options.terminate ? { terminate: options.terminate } : {},
4024
5887
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {},
4025
- ...options.emit ? { emit: options.emit } : {}
5888
+ ...emitForProtocol ? { emit: emitForProtocol } : {},
5889
+ ...options.streamEvents !== void 0 ? { streamEvents: options.streamEvents } : {},
5890
+ currentDepth: options.currentDepth ?? 0,
5891
+ effectiveMaxDepth: options.effectiveMaxDepth ?? Infinity,
5892
+ effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN,
5893
+ onChildFailure: options.onChildFailure ?? "continue",
5894
+ ...options.parentDeadlineMs !== void 0 ? { parentDeadlineMs: options.parentDeadlineMs } : {},
5895
+ ...options.defaultSubRunTimeoutMs !== void 0 ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs } : {},
5896
+ ...options.registerAbortDrain !== void 0 ? { registerAbortDrain: options.registerAbortDrain } : {},
5897
+ ...options.failureInstancesByChildRunId !== void 0 ? { failureInstancesByChildRunId: options.failureInstancesByChildRunId } : {},
5898
+ runProtocol: (childInput) => {
5899
+ const { runId: childRunId, ...childProtocolInput } = childInput;
5900
+ const childParent = options.subRunSpansByChildId?.get(childRunId) ?? options.parentSpan;
5901
+ return runProtocol({
5902
+ ...childProtocolInput,
5903
+ protocol: normalizeProtocol(childProtocolInput.protocol),
5904
+ ...options.tracer ? { tracer: options.tracer } : {},
5905
+ ...childParent ? { parentSpan: childParent } : {},
5906
+ ...options.logger ? { logger: options.logger } : {}
5907
+ });
5908
+ }
4026
5909
  });
4027
5910
  case "shared": return runShared({
4028
5911
  intent: options.intent,
@@ -4037,7 +5920,7 @@ function runProtocol(options) {
4037
5920
  ...options.signal !== void 0 ? { signal: options.signal } : {},
4038
5921
  ...options.terminate ? { terminate: options.terminate } : {},
4039
5922
  ...options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {},
4040
- ...options.emit ? { emit: options.emit } : {}
5923
+ ...emitForProtocol ? { emit: emitForProtocol } : {}
4041
5924
  });
4042
5925
  }
4043
5926
  }
@@ -4085,13 +5968,22 @@ function stream(options) {
4085
5968
  * the ergonomic {@link RunResult} wrapper from the JSON-serializable
4086
5969
  * {@link Trace} returned by a previous `run()`, `stream()`, or
4087
5970
  * `Dogpile.pile()` call.
5971
+ *
5972
+ * Tracing and metrics: replay is intentionally tracing-free and metrics-free.
5973
+ * Even when an engine instance has been configured with a `tracer` or
5974
+ * `metricsHook` on its `EngineOptions`, calling this function emits no spans
5975
+ * or callbacks — replaying historical events with current timestamps would
5976
+ * confuse observability backends. See `docs/developer-usage.md`.
4088
5977
  */
4089
5978
  function replay(trace) {
4090
5979
  const cost = trace.finalOutput.cost;
4091
5980
  const lastEvent = trace.events.at(-1);
5981
+ const accounting = recomputeAccountingFromTrace(trace);
5982
+ const replayThrow = resolveReplayTerminalThrow(trace);
5983
+ if (replayThrow) throw replayThrow;
4092
5984
  const baseResult = {
4093
5985
  output: trace.finalOutput.output,
4094
- eventLog: createRunEventLog(trace.runId, trace.protocol, trace.events),
5986
+ eventLog: createRunEventLog(trace.runId, trace.protocol, synthesizeProviderEvents(trace, trace.providerCalls)),
4095
5987
  trace,
4096
5988
  transcript: trace.transcript,
4097
5989
  usage: createRunUsage(cost),
@@ -4103,14 +5995,9 @@ function replay(trace) {
4103
5995
  agentsUsed: trace.agentsUsed,
4104
5996
  events: trace.events
4105
5997
  }),
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
- }),
4113
- cost
5998
+ accounting,
5999
+ cost,
6000
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
4114
6001
  };
4115
6002
  if (lastEvent?.type !== "final") return baseResult;
4116
6003
  return {
@@ -4119,30 +6006,137 @@ function replay(trace) {
4119
6006
  ...lastEvent.evaluation !== void 0 ? { evaluation: lastEvent.evaluation } : {}
4120
6007
  };
4121
6008
  }
6009
+ function synthesizeProviderEvents(trace, providerCalls) {
6010
+ if (trace.events.some((event) => event.type === "model-request" || event.type === "model-response")) return trace.events;
6011
+ const baseEvents = trace.events.filter((event) => event.type !== "model-request" && event.type !== "model-response");
6012
+ const result = [];
6013
+ let turnCount = 0;
6014
+ for (const event of baseEvents) {
6015
+ if (event.type === "agent-turn") {
6016
+ const call = providerCalls[turnCount];
6017
+ if (call !== void 0) {
6018
+ const modelId = typeof call.modelId === "string" && call.modelId.length > 0 ? call.modelId : call.providerId;
6019
+ result.push({
6020
+ type: "model-request",
6021
+ runId: trace.runId,
6022
+ callId: call.callId,
6023
+ providerId: call.providerId,
6024
+ modelId,
6025
+ startedAt: call.startedAt,
6026
+ agentId: call.agentId,
6027
+ role: call.role,
6028
+ request: call.request
6029
+ });
6030
+ result.push({
6031
+ type: "model-response",
6032
+ runId: trace.runId,
6033
+ callId: call.callId,
6034
+ providerId: call.providerId,
6035
+ modelId,
6036
+ startedAt: call.startedAt,
6037
+ completedAt: call.completedAt,
6038
+ agentId: call.agentId,
6039
+ role: call.role,
6040
+ response: call.response
6041
+ });
6042
+ }
6043
+ turnCount += 1;
6044
+ }
6045
+ result.push(event);
6046
+ }
6047
+ return result;
6048
+ }
6049
+ function resolveRuntimeTerminalThrow(trace, failureInstancesByChildRunId) {
6050
+ if (trace.triggeringFailureForAbortMode !== void 0) return failureInstancesByChildRunId.get(trace.triggeringFailureForAbortMode.childRunId) ?? null;
6051
+ const finalEvent = trace.events.at(-1);
6052
+ if (finalEvent?.type !== "final" || finalEvent.termination === void 0) return null;
6053
+ const lastFailure = findLastRealFailure(trace.events, failureInstancesByChildRunId);
6054
+ if (lastFailure === null) return null;
6055
+ if (hasFinalSynthesisAfterEvent(trace, lastFailure.eventIndex)) return null;
6056
+ return lastFailure.error;
6057
+ }
6058
+ function findLastRealFailure(events, failureInstancesByChildRunId) {
6059
+ for (let index = events.length - 1; index >= 0; index -= 1) {
6060
+ const event = events[index];
6061
+ if (event?.type !== "sub-run-failed") continue;
6062
+ const instance = failureInstancesByChildRunId.get(event.childRunId);
6063
+ if (instance) return {
6064
+ error: instance,
6065
+ eventIndex: index
6066
+ };
6067
+ }
6068
+ return null;
6069
+ }
6070
+ function resolveReplayTerminalThrow(trace) {
6071
+ if (trace.triggeringFailureForAbortMode !== void 0) return dogpileErrorFromSerializedPayload(trace.triggeringFailureForAbortMode.error);
6072
+ const finalEvent = trace.events.at(-1);
6073
+ if (finalEvent?.type !== "final" || finalEvent.termination === void 0) return null;
6074
+ const lastFailure = reconstructLastRealFailure(trace.events);
6075
+ if (lastFailure === null) return null;
6076
+ if (hasFinalSynthesisAfterEvent(trace, lastFailure.eventIndex)) return null;
6077
+ return lastFailure.error;
6078
+ }
6079
+ function reconstructLastRealFailure(events) {
6080
+ for (let index = events.length - 1; index >= 0; index -= 1) {
6081
+ const event = events[index];
6082
+ if (event?.type !== "sub-run-failed" || isSyntheticSubRunFailure(event)) continue;
6083
+ return {
6084
+ error: dogpileErrorFromSerializedPayload(event.error),
6085
+ eventIndex: index
6086
+ };
6087
+ }
6088
+ return null;
6089
+ }
6090
+ function hasFinalSynthesisAfterEvent(trace, eventIndex) {
6091
+ return trace.protocolDecisions.some((decision) => {
6092
+ return decision.phase === "final-synthesis" && decision.eventIndex > eventIndex;
6093
+ });
6094
+ }
6095
+ function isSyntheticSubRunFailure(event) {
6096
+ const reason = event.error.detail?.["reason"];
6097
+ return reason === "sibling-failed" || reason === "parent-aborted";
6098
+ }
6099
+ function dogpileErrorFromSerializedPayload(input) {
6100
+ return new DogpileError({
6101
+ code: input.code,
6102
+ message: input.message,
6103
+ ...input.providerId !== void 0 ? { providerId: input.providerId } : {},
6104
+ ...input.detail !== void 0 ? { detail: input.detail } : {}
6105
+ });
6106
+ }
4122
6107
  /**
4123
6108
  * Replay a saved completed trace as a stream without invoking a model provider.
4124
6109
  *
4125
6110
  * @remarks
4126
- * This is the streaming counterpart to {@link replay}. It yields the exact
4127
- * saved {@link Trace.events} in order and resolves {@link StreamHandle.result}
4128
- * to the rehydrated {@link RunResult}. Since all data comes from the trace,
4129
- * replay remains storage-free and provider-free.
6111
+ * This is the streaming counterpart to {@link replay}. It yields the same
6112
+ * event sequence exposed by the replayed result event log, including legacy
6113
+ * provenance synthesis when a saved trace predates model request/response
6114
+ * events. Since all data comes from the trace, replay remains storage-free and
6115
+ * provider-free.
6116
+ *
6117
+ * Tracing and metrics: replayStream is intentionally tracing-free and
6118
+ * metrics-free. Even when an engine instance has been configured with a
6119
+ * `tracer` or `metricsHook` on its `EngineOptions`, calling this function
6120
+ * emits no spans or callbacks — replaying historical events with current
6121
+ * timestamps would confuse observability backends. See `docs/developer-usage.md`.
4130
6122
  */
4131
6123
  function replayStream(trace) {
6124
+ const result = Promise.resolve(replay(trace));
6125
+ const replayEvents = replayStreamEvents(trace);
4132
6126
  return {
4133
6127
  get status() {
4134
6128
  return "completed";
4135
6129
  },
4136
- result: Promise.resolve(replay(trace)),
6130
+ result,
4137
6131
  cancel() {},
4138
6132
  subscribe(subscriber) {
4139
- for (const event of trace.events) subscriber(event);
6133
+ for (const event of replayEvents) subscriber(event);
4140
6134
  return { unsubscribe() {} };
4141
6135
  },
4142
6136
  [Symbol.asyncIterator]() {
4143
6137
  let index = 0;
4144
6138
  return { next() {
4145
- const event = trace.events[index];
6139
+ const event = replayEvents[index];
4146
6140
  if (event) {
4147
6141
  index += 1;
4148
6142
  return Promise.resolve({
@@ -4158,6 +6152,22 @@ function replayStream(trace) {
4158
6152
  }
4159
6153
  };
4160
6154
  }
6155
+ function replayStreamEvents(trace, parentRunIds = []) {
6156
+ const events = [];
6157
+ for (const event of synthesizeProviderEvents(trace, trace.providerCalls)) {
6158
+ if (event.type === "sub-run-completed") events.push(...replayStreamEvents(event.subResult.trace, [...parentRunIds, trace.runId]));
6159
+ events.push(wrapReplayStreamEvent(event, parentRunIds));
6160
+ }
6161
+ return events;
6162
+ }
6163
+ function wrapReplayStreamEvent(event, parentRunIds) {
6164
+ if (parentRunIds.length === 0) return event;
6165
+ const inbound = event.parentRunIds;
6166
+ return {
6167
+ ...event,
6168
+ parentRunIds: [...parentRunIds, ...inbound ?? []]
6169
+ };
6170
+ }
4161
6171
  function wireCallerAbortSignal(callerSignal, abortController, cancelRun) {
4162
6172
  if (!callerSignal) return () => {};
4163
6173
  const cancelFromCaller = () => {
@@ -4181,7 +6191,10 @@ function createStreamCancellationError(providerId, cause) {
4181
6191
  retryable: false,
4182
6192
  providerId,
4183
6193
  ...cause !== void 0 ? { cause } : {},
4184
- detail: { status: "cancelled" }
6194
+ detail: {
6195
+ status: "cancelled",
6196
+ reason: "parent-aborted"
6197
+ }
4185
6198
  });
4186
6199
  }
4187
6200
  function isCancellationError(error) {
@@ -4195,6 +6208,20 @@ function withHighLevelDefaults(options) {
4195
6208
  tier: options.tier ?? defaultHighLevelTier
4196
6209
  };
4197
6210
  }
6211
+ function assertRunDoesNotRaiseEngineMax(path, runValue, engineValue) {
6212
+ if (runValue === void 0 || runValue <= engineValue) return;
6213
+ throw new DogpileError({
6214
+ code: "invalid-configuration",
6215
+ message: `${path} cannot raise the engine ceiling (${engineValue}).`,
6216
+ retryable: false,
6217
+ detail: {
6218
+ kind: "configuration-validation",
6219
+ path,
6220
+ expected: `integer <= ${engineValue}`,
6221
+ actual: runValue
6222
+ }
6223
+ });
6224
+ }
4198
6225
  /**
4199
6226
  * Branded high-level SDK namespace.
4200
6227
  *
@@ -4221,6 +6248,8 @@ function createOpenAICompatibleProvider(options) {
4221
6248
  validateOptions(options);
4222
6249
  const providerId = options.id ?? `openai-compatible:${options.model}`;
4223
6250
  const fetchImplementation = options.fetch ?? globalThis.fetch?.bind(globalThis);
6251
+ const detectedLocality = classifyHostLocality(new URL(String(options.baseURL ?? defaultBaseURL)).hostname);
6252
+ const resolvedLocality = options.locality === "local" ? "local" : options.locality === "remote" ? "remote" : detectedLocality;
4224
6253
  if (!fetchImplementation) throw new DogpileError({
4225
6254
  code: "invalid-configuration",
4226
6255
  message: "createOpenAICompatibleProvider() requires a fetch implementation in this runtime.",
@@ -4234,6 +6263,8 @@ function createOpenAICompatibleProvider(options) {
4234
6263
  });
4235
6264
  return {
4236
6265
  id: providerId,
6266
+ modelId: options.model,
6267
+ metadata: { locality: resolvedLocality },
4237
6268
  async generate(request) {
4238
6269
  let response;
4239
6270
  try {
@@ -4246,9 +6277,11 @@ function createOpenAICompatibleProvider(options) {
4246
6277
  } catch (error) {
4247
6278
  throw normalizeFetchError(error, providerId);
4248
6279
  }
4249
- const payload = await readJson(response, providerId);
4250
- if (!response.ok) throw createProviderError(response, payload, providerId);
4251
- const completion = asChatCompletionResponse(payload, providerId);
6280
+ if (!response.ok) {
6281
+ const payload = await readJsonLenient(response);
6282
+ throw createProviderError(response, payload, providerId);
6283
+ }
6284
+ const completion = asChatCompletionResponse(await readJson(response, providerId), providerId);
4252
6285
  const text = readAssistantText(completion, providerId);
4253
6286
  const usage = normalizeUsage(completion.usage);
4254
6287
  const finishReason = normalizeFinishReason(completion.choices?.[0]?.finish_reason);
@@ -4277,6 +6310,22 @@ function validateOptions(options) {
4277
6310
  if (options.fetch !== void 0 && typeof options.fetch !== "function") throwInvalid("fetch", "a fetch-compatible function when provided");
4278
6311
  if (options.maxOutputTokens !== void 0 && (!Number.isInteger(options.maxOutputTokens) || options.maxOutputTokens <= 0)) throwInvalid("maxOutputTokens", "a positive integer when provided");
4279
6312
  if (options.costEstimator !== void 0 && typeof options.costEstimator !== "function") throwInvalid("costEstimator", "a function when provided");
6313
+ if (options.locality !== void 0 && options.locality !== "local" && options.locality !== "remote") throwInvalid("locality", "\"local\" | \"remote\" when provided");
6314
+ if (options.locality === "remote") {
6315
+ const baseURL = new URL(String(options.baseURL ?? defaultBaseURL));
6316
+ if (classifyHostLocality(baseURL.hostname) === "local") throw new DogpileError({
6317
+ code: "invalid-configuration",
6318
+ message: `locality "remote" cannot be set when baseURL resolves to a local host (${baseURL.hostname}).`,
6319
+ retryable: false,
6320
+ detail: {
6321
+ kind: "configuration-validation",
6322
+ path: "locality",
6323
+ expected: "\"local\" (or omit to auto-detect)",
6324
+ reason: "remote-override-on-local-host",
6325
+ host: baseURL.hostname
6326
+ }
6327
+ });
6328
+ }
4280
6329
  }
4281
6330
  function throwInvalid(path, expected) {
4282
6331
  throw new DogpileError({
@@ -4290,6 +6339,38 @@ function throwInvalid(path, expected) {
4290
6339
  }
4291
6340
  });
4292
6341
  }
6342
+ /**
6343
+ * Classify a URL hostname as "local" or "remote" per Phase 3 D-02.
6344
+ * Local: localhost, *.local mDNS, IPv4 loopback (127.0.0.0/8), RFC1918
6345
+ * (10/8, 172.16/12, 192.168/16), link-local (169.254/16), IPv6 loopback (::1),
6346
+ * IPv6 ULA (fc00::/7), IPv6 link-local (fe80::/10).
6347
+ *
6348
+ * Pure function: no I/O, no side effects. Exported for tests and future reuse.
6349
+ */
6350
+ function classifyHostLocality(host) {
6351
+ const lower = host.toLowerCase().replace(/^\[|\]$/g, "");
6352
+ const mappedIpv4 = ipv4MappedToDottedQuad(lower);
6353
+ if (mappedIpv4 !== void 0) return classifyHostLocality(mappedIpv4);
6354
+ if (lower === "localhost") return "local";
6355
+ if (lower.endsWith(".local")) return "local";
6356
+ if (/^127(?:\.\d{1,3}){3}$/.test(lower)) return "local";
6357
+ if (/^10(?:\.\d{1,3}){3}$/.test(lower)) return "local";
6358
+ if (/^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/.test(lower)) return "local";
6359
+ if (/^192\.168(?:\.\d{1,3}){2}$/.test(lower)) return "local";
6360
+ if (/^169\.254(?:\.\d{1,3}){2}$/.test(lower)) return "local";
6361
+ if (lower === "::1") return "local";
6362
+ if (/^f[cd][0-9a-f]{2}:/.test(lower)) return "local";
6363
+ if (/^fe[89ab][0-9a-f]?:/.test(lower)) return "local";
6364
+ return "remote";
6365
+ }
6366
+ function ipv4MappedToDottedQuad(host) {
6367
+ const match = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
6368
+ if (match === null) return;
6369
+ const high = Number.parseInt(match[1] ?? "", 16);
6370
+ const low = Number.parseInt(match[2] ?? "", 16);
6371
+ if (!Number.isFinite(high) || !Number.isFinite(low)) return;
6372
+ return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
6373
+ }
4293
6374
  function createURL(options) {
4294
6375
  const baseURL = new URL(String(options.baseURL ?? defaultBaseURL));
4295
6376
  const path = options.path ?? defaultPath;
@@ -4339,6 +6420,13 @@ async function readJson(response, providerId) {
4339
6420
  });
4340
6421
  }
4341
6422
  }
6423
+ async function readJsonLenient(response) {
6424
+ try {
6425
+ return await response.json();
6426
+ } catch {
6427
+ return;
6428
+ }
6429
+ }
4342
6430
  function asChatCompletionResponse(payload, providerId) {
4343
6431
  if (!isRecord(payload)) throw new DogpileError({
4344
6432
  code: "provider-invalid-response",
@@ -4409,14 +6497,17 @@ function responseMetadata(response) {
4409
6497
  });
4410
6498
  }
4411
6499
  function createProviderError(response, payload, providerId) {
6500
+ const code = codeForStatus(response.status);
6501
+ const timeoutSource = code === "provider-timeout" ? { source: "provider" } : {};
4412
6502
  return new DogpileError({
4413
- code: codeForStatus(response.status),
6503
+ code,
4414
6504
  message: providerResponseErrorMessage(response, payload),
4415
6505
  retryable: response.status === 408 || response.status === 429 || response.status >= 500,
4416
6506
  providerId,
4417
6507
  detail: removeUndefined({
4418
6508
  statusCode: response.status,
4419
6509
  statusText: response.statusText,
6510
+ ...timeoutSource,
4420
6511
  response: isJsonValue(payload) ? payload : void 0
4421
6512
  })
4422
6513
  });
@@ -4737,6 +6828,6 @@ function defaultSleep(ms, signal) {
4737
6828
  });
4738
6829
  }
4739
6830
  //#endregion
4740
- export { DEFAULT_RETRYABLE_DOGPILE_CODES, Dogpile, DogpileError, budget, builtInDogpileToolIdentity, builtInDogpileToolInputSchema, builtInDogpileToolPermissions, combineTerminationDecisions, consoleLogger, convergence, createCodeExecToolAdapter, createEngine, createOpenAICompatibleProvider, createRuntimeToolExecutor, createWebSearchToolAdapter, evaluateBudget, evaluateConvergence, evaluateFirstOf, evaluateJudge, evaluateTermination, evaluateTerminationStop, firstOf, judge, loggerFromEvents, noopLogger, normalizeBuiltInDogpileTool, normalizeBuiltInDogpileTools, normalizeRuntimeToolAdapterError, replay, replayStream, run, runtimeToolAvailability, runtimeToolManifest, stream, validateBuiltInDogpileToolInput, withRetry };
6831
+ export { DEFAULT_RETRYABLE_DOGPILE_CODES, DOGPILE_SPAN_NAMES, Dogpile, DogpileError, budget, builtInDogpileToolIdentity, builtInDogpileToolInputSchema, builtInDogpileToolPermissions, combineTerminationDecisions, consoleLogger, convergence, createCodeExecToolAdapter, createEngine, createOpenAICompatibleProvider, createRuntimeToolExecutor, createWebSearchToolAdapter, evaluateBudget, evaluateConvergence, evaluateFirstOf, evaluateJudge, evaluateTermination, evaluateTerminationStop, firstOf, judge, loggerFromEvents, noopLogger, normalizeBuiltInDogpileTool, normalizeBuiltInDogpileTools, normalizeRuntimeToolAdapterError, replay, replayStream, run, runtimeToolAvailability, runtimeToolManifest, stream, validateBuiltInDogpileToolInput, withRetry };
4741
6832
 
4742
6833
  //# sourceMappingURL=index.js.map