@dogpile/sdk 0.4.0 → 0.6.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 (108) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/browser/index.js +4156 -4611
  3. package/dist/browser/index.js.map +1 -1
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/providers/openai-compatible.d.ts.map +1 -1
  9. package/dist/providers/openai-compatible.js +6 -1
  10. package/dist/providers/openai-compatible.js.map +1 -1
  11. package/dist/runtime/audit.d.ts +42 -0
  12. package/dist/runtime/audit.d.ts.map +1 -0
  13. package/dist/runtime/audit.js +73 -0
  14. package/dist/runtime/audit.js.map +1 -0
  15. package/dist/runtime/broadcast.d.ts +1 -0
  16. package/dist/runtime/broadcast.d.ts.map +1 -1
  17. package/dist/runtime/broadcast.js +171 -105
  18. package/dist/runtime/broadcast.js.map +1 -1
  19. package/dist/runtime/coordinator.d.ts +9 -2
  20. package/dist/runtime/coordinator.d.ts.map +1 -1
  21. package/dist/runtime/coordinator.js +164 -78
  22. package/dist/runtime/coordinator.js.map +1 -1
  23. package/dist/runtime/defaults.d.ts.map +1 -1
  24. package/dist/runtime/defaults.js +14 -5
  25. package/dist/runtime/defaults.js.map +1 -1
  26. package/dist/runtime/engine.d.ts +17 -4
  27. package/dist/runtime/engine.d.ts.map +1 -1
  28. package/dist/runtime/engine.js +577 -52
  29. package/dist/runtime/engine.js.map +1 -1
  30. package/dist/runtime/health.d.ts +51 -0
  31. package/dist/runtime/health.d.ts.map +1 -0
  32. package/dist/runtime/health.js +85 -0
  33. package/dist/runtime/health.js.map +1 -0
  34. package/dist/runtime/introspection.d.ts +96 -0
  35. package/dist/runtime/introspection.d.ts.map +1 -0
  36. package/dist/runtime/introspection.js +31 -0
  37. package/dist/runtime/introspection.js.map +1 -0
  38. package/dist/runtime/metrics.d.ts +44 -0
  39. package/dist/runtime/metrics.d.ts.map +1 -0
  40. package/dist/runtime/metrics.js +12 -0
  41. package/dist/runtime/metrics.js.map +1 -0
  42. package/dist/runtime/model.d.ts.map +1 -1
  43. package/dist/runtime/model.js +40 -10
  44. package/dist/runtime/model.js.map +1 -1
  45. package/dist/runtime/provenance.d.ts +25 -0
  46. package/dist/runtime/provenance.d.ts.map +1 -0
  47. package/dist/runtime/provenance.js +13 -0
  48. package/dist/runtime/provenance.js.map +1 -0
  49. package/dist/runtime/redaction.d.ts +13 -0
  50. package/dist/runtime/redaction.d.ts.map +1 -0
  51. package/dist/runtime/redaction.js +278 -0
  52. package/dist/runtime/redaction.js.map +1 -0
  53. package/dist/runtime/sanitization.d.ts +4 -0
  54. package/dist/runtime/sanitization.d.ts.map +1 -0
  55. package/dist/runtime/sanitization.js +63 -0
  56. package/dist/runtime/sanitization.js.map +1 -0
  57. package/dist/runtime/sequential.d.ts.map +1 -1
  58. package/dist/runtime/sequential.js +39 -36
  59. package/dist/runtime/sequential.js.map +1 -1
  60. package/dist/runtime/shared.d.ts +1 -0
  61. package/dist/runtime/shared.d.ts.map +1 -1
  62. package/dist/runtime/shared.js +167 -101
  63. package/dist/runtime/shared.js.map +1 -1
  64. package/dist/runtime/tools/built-in.d.ts +2 -0
  65. package/dist/runtime/tools/built-in.d.ts.map +1 -1
  66. package/dist/runtime/tools/built-in.js +153 -15
  67. package/dist/runtime/tools/built-in.js.map +1 -1
  68. package/dist/runtime/tools.d.ts.map +1 -1
  69. package/dist/runtime/tools.js +29 -7
  70. package/dist/runtime/tools.js.map +1 -1
  71. package/dist/runtime/tracing.d.ts +31 -0
  72. package/dist/runtime/tracing.d.ts.map +1 -0
  73. package/dist/runtime/tracing.js +18 -0
  74. package/dist/runtime/tracing.js.map +1 -0
  75. package/dist/runtime/validation.d.ts.map +1 -1
  76. package/dist/runtime/validation.js +3 -0
  77. package/dist/runtime/validation.js.map +1 -1
  78. package/dist/types/events.d.ts +13 -7
  79. package/dist/types/events.d.ts.map +1 -1
  80. package/dist/types/replay.d.ts +5 -1
  81. package/dist/types/replay.d.ts.map +1 -1
  82. package/dist/types.d.ts +144 -1
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/package.json +46 -1
  86. package/src/index.ts +5 -0
  87. package/src/providers/openai-compatible.ts +6 -1
  88. package/src/runtime/audit.ts +121 -0
  89. package/src/runtime/broadcast.ts +195 -108
  90. package/src/runtime/coordinator.ts +197 -86
  91. package/src/runtime/defaults.ts +15 -5
  92. package/src/runtime/engine.ts +725 -58
  93. package/src/runtime/health.ts +136 -0
  94. package/src/runtime/introspection.ts +122 -0
  95. package/src/runtime/metrics.ts +45 -0
  96. package/src/runtime/model.ts +44 -9
  97. package/src/runtime/provenance.ts +43 -0
  98. package/src/runtime/redaction.ts +355 -0
  99. package/src/runtime/sanitization.ts +81 -0
  100. package/src/runtime/sequential.ts +40 -37
  101. package/src/runtime/shared.ts +191 -104
  102. package/src/runtime/tools/built-in.ts +168 -15
  103. package/src/runtime/tools.ts +39 -8
  104. package/src/runtime/tracing.ts +35 -0
  105. package/src/runtime/validation.ts +3 -0
  106. package/src/types/events.ts +13 -7
  107. package/src/types/replay.ts +5 -1
  108. package/src/types.ts +152 -1
@@ -46,6 +46,7 @@ import {
46
46
  lastCostBearingEventCost,
47
47
  nextProviderCallId
48
48
  } from "./defaults.js";
49
+ import { computeHealth, DEFAULT_HEALTH_THRESHOLDS } from "./health.js";
49
50
  import {
50
51
  classifyAbortReason,
51
52
  classifyChildTimeoutSource,
@@ -64,6 +65,11 @@ import { createWrapUpHintController } from "./wrap-up.js";
64
65
  * in by `engine.ts` so coordinator avoids a circular import.
65
66
  */
66
67
  export type RunProtocolFn = (input: {
68
+ /**
69
+ * Planned child run id emitted on sub-run lifecycle events before dispatch.
70
+ * The engine callback uses this to look up the matching sub-run span.
71
+ */
72
+ readonly runId: string;
67
73
  readonly intent: string;
68
74
  readonly protocol: ProtocolSelection;
69
75
  readonly tier: Tier;
@@ -81,6 +87,7 @@ export type RunProtocolFn = (input: {
81
87
  readonly currentDepth?: number;
82
88
  readonly effectiveMaxDepth?: number;
83
89
  readonly effectiveMaxConcurrentChildren?: number;
90
+ readonly effectiveMaxConcurrentAgentTurns?: number;
84
91
  readonly onChildFailure?: DogpileOptions["onChildFailure"];
85
92
  /**
86
93
  * Root-run deadline (epoch ms). Children inherit `parentDeadlineMs - now()`
@@ -120,11 +127,12 @@ interface CoordinatorRunOptions {
120
127
  */
121
128
  readonly currentDepth?: number;
122
129
  /**
123
- * Effective max recursion depth resolved at run start. Plan 04 enforces;
124
- * Plan 03 only plumbs the value.
130
+ * Effective max recursion depth resolved at run start and enforced before
131
+ * delegate dispatch.
125
132
  */
126
133
  readonly effectiveMaxDepth?: number;
127
134
  readonly effectiveMaxConcurrentChildren?: number;
135
+ readonly effectiveMaxConcurrentAgentTurns?: number;
128
136
  readonly onChildFailure?: DogpileOptions["onChildFailure"];
129
137
  /**
130
138
  * Engine `runProtocol` callback used by the delegate dispatch loop to
@@ -156,6 +164,7 @@ interface CoordinatorRunOptions {
156
164
  */
157
165
  const MAX_DISPATCH_PER_TURN = 8;
158
166
  const DEFAULT_MAX_CONCURRENT_CHILDREN = 4;
167
+ const DEFAULT_MAX_CONCURRENT_AGENT_TURNS = 4;
159
168
 
160
169
  type DispatchWaveFailure = {
161
170
  readonly childRunId: string;
@@ -177,7 +186,8 @@ interface Semaphore {
177
186
 
178
187
  function createSemaphore(maxConcurrent: number): Semaphore {
179
188
  let inFlight = 0;
180
- const waiters: Array<() => void> = [];
189
+ const waiters: Array<(() => void) | undefined> = [];
190
+ let waiterHead = 0;
181
191
  return {
182
192
  acquire(): Promise<void> {
183
193
  if (inFlight < maxConcurrent) {
@@ -193,8 +203,14 @@ function createSemaphore(maxConcurrent: number): Semaphore {
193
203
  },
194
204
  release(): void {
195
205
  inFlight -= 1;
196
- const next = waiters.shift();
206
+ const next = waiters[waiterHead];
197
207
  if (next !== undefined) {
208
+ waiters[waiterHead] = undefined;
209
+ waiterHead += 1;
210
+ if (waiterHead > 32 && waiterHead * 2 > waiters.length) {
211
+ waiters.splice(0, waiterHead);
212
+ waiterHead = 0;
213
+ }
198
214
  next();
199
215
  }
200
216
  },
@@ -202,7 +218,7 @@ function createSemaphore(maxConcurrent: number): Semaphore {
202
218
  return inFlight;
203
219
  },
204
220
  get queued() {
205
- return waiters.length;
221
+ return waiters.length - waiterHead;
206
222
  }
207
223
  };
208
224
  }
@@ -212,15 +228,16 @@ function createSemaphore(maxConcurrent: number): Semaphore {
212
228
  * whose metadata.locality === "local", or undefined if none found.
213
229
  *
214
230
  * Walk order (forward-compat): options.model first, then options.agents in
215
- * declaration order. AgentSpec has no `model` field today (Phase 3 D-11
216
- * forward-compat scaffolding); the agent walk uses optional chaining and
217
- * effectively no-ops until a future phase adds AgentSpec.model.
231
+ * declaration order. AgentSpec has no `model` field today; the agent walk
232
+ * uses optional chaining and effectively no-ops until a future caller-defined
233
+ * tree surface adds AgentSpec.model.
218
234
  */
219
235
  function findFirstLocalProvider(options: CoordinatorRunOptions): ConfiguredModelProvider | undefined {
220
236
  if (options.model.metadata?.locality === "local") {
221
237
  return options.model;
222
238
  }
223
- // Forward-compat: AgentSpec.model not yet declared (Phase 3 D-11). Walk no-ops today; ready for caller-defined trees in a future milestone.
239
+ // Forward-compat: AgentSpec.model is not yet declared; this no-ops today and
240
+ // is ready for a future caller-defined tree surface.
224
241
  for (const agent of options.agents) {
225
242
  const agentModel = (agent as { readonly model?: ConfiguredModelProvider }).model;
226
243
  if (agentModel?.metadata?.locality === "local") {
@@ -357,13 +374,12 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
357
374
 
358
375
  if (coordinator) {
359
376
  if (!stopIfNeeded()) {
360
- // Delegate dispatch loop (D-11/D-16/D-17/D-18). Phase 1 limits delegation
361
- // to the coordinator's plan turn; workers cannot delegate. The loop
362
- // re-issues the coordinator plan turn after each successful sub-run with
363
- // the projected D-17 result tagged into the next prompt and a synthetic
364
- // D-18 transcript entry already appended. `partialTrace` for failed
365
- // sub-runs is captured via a tee'd emit buffer locally — `runProtocol`'s
366
- // error contract is unchanged.
377
+ // Delegate dispatch is restricted to the coordinator's plan turn.
378
+ // Workers and final synthesis cannot delegate. The loop re-issues the
379
+ // coordinator plan turn after each successful sub-run with the tagged
380
+ // child result in the next prompt and a synthetic transcript entry
381
+ // already appended. Failed children use the local tee buffer for
382
+ // partialTrace capture; runProtocol's error contract stays unchanged.
367
383
  let dispatchInput = buildCoordinatorPlanInput(options.intent, coordinator);
368
384
  let dispatchCount = 0;
369
385
  while (true) {
@@ -638,29 +654,39 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
638
654
  const workers = activeAgents.slice(1);
639
655
  const providerCallSlots: ReplayTraceProviderCall[] = [];
640
656
  const planTranscript = [...transcript];
641
- const workerResults = await Promise.all(
642
- workers.map((agent, index) =>
643
- runCoordinatorWorkerTurn({
644
- agent,
645
- coordinator,
646
- input: buildWorkerInput(options.intent, planTranscript, coordinator),
647
- options,
648
- runId,
649
- turn: transcript.length + index + 1,
650
- providerCallId: providerCallIdFor(runId, providerCalls.length + index + 1),
651
- providerCallIndex: index,
652
- providerCallSlots,
653
- toolExecutor,
654
- toolAvailability,
655
- totalCost,
656
- events,
657
- transcript: planTranscript,
658
- startedAtMs,
659
- wrapUpHint,
660
- emit
661
- })
662
- )
663
- );
657
+ const fanout = createFanoutAbortController(options.signal);
658
+ const workerResults = await (async () => {
659
+ try {
660
+ return await mapWithConcurrency(
661
+ workers,
662
+ options.effectiveMaxConcurrentAgentTurns ?? DEFAULT_MAX_CONCURRENT_AGENT_TURNS,
663
+ fanout,
664
+ (agent, index) =>
665
+ runCoordinatorWorkerTurn({
666
+ agent,
667
+ coordinator,
668
+ input: buildWorkerInput(options.intent, planTranscript, coordinator),
669
+ options,
670
+ runId,
671
+ turn: transcript.length + index + 1,
672
+ providerCallId: providerCallIdFor(runId, providerCalls.length + index + 1),
673
+ providerCallIndex: index,
674
+ providerCallSlots,
675
+ toolExecutor,
676
+ toolAvailability,
677
+ totalCost,
678
+ events,
679
+ transcript: planTranscript,
680
+ startedAtMs,
681
+ wrapUpHint,
682
+ emit,
683
+ fanoutSignal: fanout.signal
684
+ })
685
+ );
686
+ } finally {
687
+ fanout.cleanup();
688
+ }
689
+ })();
664
690
  providerCalls.push(...providerCallSlots.filter((call): call is ReplayTraceProviderCall => call !== undefined));
665
691
 
666
692
  for (const result of workerResults) {
@@ -715,11 +741,11 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
715
741
  recordProtocolDecision
716
742
  });
717
743
  totalCost = synthesisOutcome.totalCost;
718
- // Phase 1: final-synthesis turn cannot delegate.
744
+ // Final synthesis is terminal and cannot dispatch children.
719
745
  if (Array.isArray(synthesisOutcome.decision) || synthesisOutcome.decision?.type === "delegate") {
720
746
  throw new DogpileError({
721
747
  code: "invalid-configuration",
722
- message: "Coordinator final-synthesis turn cannot emit a delegate decision in Phase 1",
748
+ message: "Coordinator final-synthesis turn cannot emit a delegate decision.",
723
749
  retryable: false,
724
750
  detail: {
725
751
  kind: "delegate-validation",
@@ -748,46 +774,47 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
748
774
  transcriptEntryCount: transcript.length
749
775
  });
750
776
  const finalEvent = events.at(-1);
777
+ const trace: Trace = {
778
+ schemaVersion: "1.0",
779
+ runId,
780
+ protocol: "coordinator",
781
+ tier: options.tier,
782
+ modelProviderId: options.model.id,
783
+ agentsUsed: activeAgents,
784
+ inputs: createReplayTraceRunInputs({
785
+ intent: options.intent,
786
+ protocol: options.protocol,
787
+ tier: options.tier,
788
+ modelProviderId: options.model.id,
789
+ agents: activeAgents,
790
+ temperature: options.temperature
791
+ }),
792
+ budget: createReplayTraceBudget({
793
+ tier: options.tier,
794
+ ...(options.budget ? { caps: options.budget } : {}),
795
+ ...(options.terminate ? { termination: options.terminate } : {})
796
+ }),
797
+ budgetStateChanges: createReplayTraceBudgetStateChanges(events),
798
+ seed: createReplayTraceSeed(options.seed),
799
+ protocolDecisions,
800
+ providerCalls,
801
+ finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
802
+ type: "final",
803
+ runId,
804
+ at: "",
805
+ output,
806
+ cost: totalCost,
807
+ transcript: createTranscriptLink(transcript)
808
+ }),
809
+ ...(triggeringFailureForAbortMode !== undefined ? { triggeringFailureForAbortMode } : {}),
810
+ events,
811
+ transcript
812
+ };
751
813
 
752
814
  return {
753
815
  output,
754
816
  eventLog: createRunEventLog(runId, "coordinator", events),
755
- trace: {
756
- schemaVersion: "1.0",
757
- runId,
758
- protocol: "coordinator",
759
- tier: options.tier,
760
- modelProviderId: options.model.id,
761
- agentsUsed: activeAgents,
762
- inputs: createReplayTraceRunInputs({
763
- intent: options.intent,
764
- protocol: options.protocol,
765
- tier: options.tier,
766
- modelProviderId: options.model.id,
767
- agents: activeAgents,
768
- temperature: options.temperature
769
- }),
770
- budget: createReplayTraceBudget({
771
- tier: options.tier,
772
- ...(options.budget ? { caps: options.budget } : {}),
773
- ...(options.terminate ? { termination: options.terminate } : {})
774
- }),
775
- budgetStateChanges: createReplayTraceBudgetStateChanges(events),
776
- seed: createReplayTraceSeed(options.seed),
777
- protocolDecisions,
778
- providerCalls,
779
- finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
780
- type: "final",
781
- runId,
782
- at: "",
783
- output,
784
- cost: totalCost,
785
- transcript: createTranscriptLink(transcript)
786
- }),
787
- ...(triggeringFailureForAbortMode !== undefined ? { triggeringFailureForAbortMode } : {}),
788
- events,
789
- transcript
790
- },
817
+ trace,
791
818
  transcript,
792
819
  usage: createRunUsage(totalCost),
793
820
  metadata: createRunMetadata({
@@ -805,7 +832,8 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
805
832
  cost: totalCost,
806
833
  events
807
834
  }),
808
- cost: totalCost
835
+ cost: totalCost,
836
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
809
837
  };
810
838
 
811
839
  function stopIfNeeded(): boolean {
@@ -1008,6 +1036,7 @@ interface CoordinatorWorkerTurnOptions {
1008
1036
  readonly startedAtMs: number;
1009
1037
  readonly wrapUpHint: ReturnType<typeof createWrapUpHintController>;
1010
1038
  readonly emit: (event: RunEvent) => void;
1039
+ readonly fanoutSignal?: AbortSignal;
1011
1040
  }
1012
1041
 
1013
1042
  interface CoordinatorWorkerTurnResult {
@@ -1021,10 +1050,11 @@ interface CoordinatorWorkerTurnResult {
1021
1050
 
1022
1051
  async function runCoordinatorWorkerTurn(turn: CoordinatorWorkerTurnOptions): Promise<CoordinatorWorkerTurnResult> {
1023
1052
  throwIfAborted(turn.options.signal, turn.options.model.id);
1053
+ throwIfAborted(turn.fanoutSignal, turn.options.model.id);
1024
1054
 
1025
1055
  const request: ModelRequest = {
1026
1056
  temperature: turn.options.temperature,
1027
- ...(turn.options.signal !== undefined ? { signal: turn.options.signal } : {}),
1057
+ ...(turn.fanoutSignal !== undefined ? { signal: turn.fanoutSignal } : turn.options.signal !== undefined ? { signal: turn.options.signal } : {}),
1028
1058
  metadata: {
1029
1059
  runId: turn.runId,
1030
1060
  protocol: "coordinator",
@@ -1077,7 +1107,7 @@ async function runCoordinatorWorkerTurn(turn: CoordinatorWorkerTurnOptions): Pro
1077
1107
  if (Array.isArray(decision) || decision?.type === "delegate") {
1078
1108
  throw new DogpileError({
1079
1109
  code: "invalid-configuration",
1080
- message: "Workers cannot emit delegate decisions in Phase 1",
1110
+ message: "Workers cannot emit delegate decisions.",
1081
1111
  retryable: false,
1082
1112
  detail: {
1083
1113
  kind: "delegate-validation",
@@ -1097,6 +1127,7 @@ async function runCoordinatorWorkerTurn(turn: CoordinatorWorkerTurnOptions): Pro
1097
1127
  }
1098
1128
  });
1099
1129
  throwIfAborted(turn.options.signal, turn.options.model.id);
1130
+ throwIfAborted(turn.fanoutSignal, turn.options.model.id);
1100
1131
 
1101
1132
  return {
1102
1133
  agent: turn.agent,
@@ -1185,6 +1216,76 @@ function responseCost(response: ModelResponse): CostSummary {
1185
1216
  };
1186
1217
  }
1187
1218
 
1219
+ interface FanoutAbortController {
1220
+ readonly signal: AbortSignal;
1221
+ abort(reason: unknown): void;
1222
+ cleanup(): void;
1223
+ }
1224
+
1225
+ function createFanoutAbortController(parentSignal: AbortSignal | undefined): FanoutAbortController {
1226
+ const controller = new AbortController();
1227
+ let removeParentListener = (): void => {};
1228
+
1229
+ if (parentSignal?.aborted) {
1230
+ controller.abort(parentSignal.reason);
1231
+ } else if (parentSignal !== undefined) {
1232
+ const abortFromParent = (): void => {
1233
+ controller.abort(parentSignal.reason);
1234
+ };
1235
+ parentSignal.addEventListener("abort", abortFromParent, { once: true });
1236
+ removeParentListener = (): void => {
1237
+ parentSignal.removeEventListener("abort", abortFromParent);
1238
+ };
1239
+ }
1240
+
1241
+ return {
1242
+ signal: controller.signal,
1243
+ abort(reason: unknown): void {
1244
+ if (!controller.signal.aborted) {
1245
+ controller.abort(reason);
1246
+ }
1247
+ },
1248
+ cleanup(): void {
1249
+ removeParentListener();
1250
+ }
1251
+ };
1252
+ }
1253
+
1254
+ async function mapWithConcurrency<T, R>(
1255
+ items: readonly T[],
1256
+ maxConcurrent: number,
1257
+ fanout: FanoutAbortController,
1258
+ mapper: (item: T, index: number) => Promise<R>
1259
+ ): Promise<R[]> {
1260
+ if (items.length === 0) {
1261
+ return [];
1262
+ }
1263
+
1264
+ const results: R[] = new Array(items.length);
1265
+ let nextIndex = 0;
1266
+ let firstError: unknown;
1267
+ const workerCount = Math.min(maxConcurrent, items.length);
1268
+
1269
+ await Promise.all(Array.from({ length: workerCount }, async () => {
1270
+ while (nextIndex < items.length && firstError === undefined) {
1271
+ const index = nextIndex;
1272
+ nextIndex += 1;
1273
+ try {
1274
+ results[index] = await mapper(items[index]!, index);
1275
+ } catch (error) {
1276
+ firstError ??= error;
1277
+ fanout.abort(error);
1278
+ }
1279
+ }
1280
+ }));
1281
+
1282
+ if (firstError !== undefined) {
1283
+ throw firstError;
1284
+ }
1285
+
1286
+ return results;
1287
+ }
1288
+
1188
1289
  interface DispatchDelegateOptions {
1189
1290
  readonly decision: DelegateAgentDecision;
1190
1291
  readonly childRunId?: string;
@@ -1230,7 +1331,7 @@ interface DispatchedChild {
1230
1331
  startedAtMs: number;
1231
1332
  childTimeoutMs: number | undefined;
1232
1333
  failure: DispatchWaveFailure | undefined;
1233
- /** STREAM-03 hook (Phase 4). Reserved; do not use. */
1334
+ /** Reserved child stream handle slot; do not use. */
1234
1335
  readonly streamHandle?: never;
1235
1336
  }
1236
1337
 
@@ -1316,7 +1417,7 @@ async function dispatchDelegate(input: DispatchDelegateOptions): Promise<Dispatc
1316
1417
  });
1317
1418
  }
1318
1419
 
1319
- // Buffered tee for partialTrace capture see Plan 03 step 8.
1420
+ // Buffered tee captures the child event prefix used for partialTrace.
1320
1421
  const childEvents = input.dispatchedChild.childEvents;
1321
1422
  const parentEmit = input.emit;
1322
1423
  const teedEmit = (event: RunEvent): void => {
@@ -1373,7 +1474,7 @@ async function dispatchDelegate(input: DispatchDelegateOptions): Promise<Dispatc
1373
1474
  // BUDGET-01 / D-07: derive a per-child AbortController so child engines see
1374
1475
  // their own signal. Listener forwards parent.signal.reason verbatim, so
1375
1476
  // detail.reason classification (parent-aborted vs timeout) is preserved.
1376
- // Phase 4 STREAM-03 hook: per-child cancel handle attaches here.
1477
+ // Reserved per-child stream cancel hook attaches here if that surface ships.
1377
1478
  const parentSignal = options.signal;
1378
1479
  let removeParentAbortListener: (() => void) | undefined;
1379
1480
  if (parentSignal !== undefined) {
@@ -1404,6 +1505,7 @@ async function dispatchDelegate(input: DispatchDelegateOptions): Promise<Dispatc
1404
1505
  : undefined;
1405
1506
 
1406
1507
  const childOptions = {
1508
+ runId: childRunId,
1407
1509
  intent: decision.intent,
1408
1510
  protocol: decision.protocol,
1409
1511
  tier: options.tier,
@@ -1420,6 +1522,9 @@ async function dispatchDelegate(input: DispatchDelegateOptions): Promise<Dispatc
1420
1522
  ...(options.effectiveMaxConcurrentChildren !== undefined
1421
1523
  ? { effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren }
1422
1524
  : {}),
1525
+ ...(options.effectiveMaxConcurrentAgentTurns !== undefined
1526
+ ? { effectiveMaxConcurrentAgentTurns: options.effectiveMaxConcurrentAgentTurns }
1527
+ : {}),
1423
1528
  ...(options.onChildFailure !== undefined ? { onChildFailure: options.onChildFailure } : {}),
1424
1529
  // BUDGET-02 / D-12: forward the ROOT deadline so depth-N grandchildren
1425
1530
  // see the same `parentDeadlineMs` rather than a fresh per-level snapshot.
@@ -1614,8 +1719,8 @@ async function dispatchDelegate(input: DispatchDelegateOptions): Promise<Dispatc
1614
1719
  function renderSubRunResult(childRunId: string, subResult: RunResult): string {
1615
1720
  const turns = subResult.transcript.length;
1616
1721
  const costUsd = subResult.cost.usd ?? 0;
1617
- const startedAt = subResult.trace.events[0]?.at;
1618
- const endedAt = subResult.trace.events.at(-1)?.at;
1722
+ const startedAt = eventTimestamp(subResult.trace.events[0]);
1723
+ const endedAt = eventTimestamp(subResult.trace.events.at(-1));
1619
1724
  const durationMs =
1620
1725
  startedAt && endedAt
1621
1726
  ? Math.max(0, Date.parse(endedAt) - Date.parse(startedAt))
@@ -1626,10 +1731,16 @@ function renderSubRunResult(childRunId: string, subResult: RunResult): string {
1626
1731
  ].join("\n");
1627
1732
  }
1628
1733
 
1734
+ function eventTimestamp(event: RunEvent | undefined): string | undefined {
1735
+ if (event === undefined) return undefined;
1736
+ if ("at" in event) return event.at;
1737
+ return event.type === "model-response" ? event.completedAt : event.startedAt;
1738
+ }
1739
+
1629
1740
  /**
1630
1741
  * Build a JSON-serializable {@link Trace} for `sub-run-failed.partialTrace`
1631
1742
  * from a buffered tee of child emits. Keeps `runProtocol`'s error contract
1632
- * unchanged — Plan 03 step 8.
1743
+ * unchanged.
1633
1744
  */
1634
1745
  function buildPartialTrace(input: {
1635
1746
  readonly childRunId: string;
@@ -236,8 +236,8 @@ export function createRunMetadata(options: {
236
236
  tier: options.tier,
237
237
  modelProviderId: options.modelProviderId,
238
238
  agentsUsed: options.agentsUsed,
239
- startedAt: firstEvent?.at ?? "",
240
- completedAt: lastEvent?.at ?? ""
239
+ startedAt: eventTimestamp(firstEvent) ?? "",
240
+ completedAt: eventTimestamp(lastEvent) ?? ""
241
241
  };
242
242
  }
243
243
 
@@ -365,7 +365,7 @@ export function createReplayTraceProtocolDecision(
365
365
  eventType: event.type,
366
366
  protocol,
367
367
  decision: options.decision ?? defaultProtocolDecision(event),
368
- at: event.at,
368
+ at: eventTimestamp(event),
369
369
  ...(options.turn !== undefined ? { turn: options.turn } : {}),
370
370
  ...(options.phase !== undefined ? { phase: options.phase } : {}),
371
371
  ...(options.round !== undefined ? { round: options.round } : {}),
@@ -404,7 +404,8 @@ export function createReplayTraceProtocolDecision(
404
404
  agentId: event.agentId,
405
405
  role: event.role,
406
406
  input: event.input,
407
- output: event.output
407
+ output: event.text,
408
+ outputLength: event.outputLength
408
409
  };
409
410
  case "tool-call":
410
411
  return {
@@ -550,7 +551,7 @@ export function createReplayTraceFinalOutput(output: string, event: RunEvent): R
550
551
  kind: "replay-trace-final-output",
551
552
  output,
552
553
  cost: emptyCost(),
553
- completedAt: event.at,
554
+ completedAt: eventTimestamp(event),
554
555
  transcript: {
555
556
  kind: "trace-transcript",
556
557
  entryCount: 0,
@@ -559,6 +560,14 @@ export function createReplayTraceFinalOutput(output: string, event: RunEvent): R
559
560
  };
560
561
  }
561
562
 
563
+ function eventTimestamp(event: RunEvent): string;
564
+ function eventTimestamp(event: RunEvent | undefined): string | undefined;
565
+ function eventTimestamp(event: RunEvent | undefined): string | undefined {
566
+ if (event === undefined) return undefined;
567
+ if ("at" in event) return event.at;
568
+ return event.type === "model-response" ? event.completedAt : event.startedAt;
569
+ }
570
+
562
571
  export function nextProviderCallId(
563
572
  runId: string,
564
573
  providerCalls: readonly ReplayTraceProviderCall[]
@@ -589,6 +598,7 @@ export function canonicalizeRunResult(result: RunResult): RunResult {
589
598
  cost: canonicalizeSerializable(result.cost),
590
599
  ...(result.evaluation !== undefined ? { evaluation: canonicalizeSerializable(result.evaluation) } : {}),
591
600
  eventLog,
601
+ health: canonicalizeSerializable(result.health),
592
602
  metadata: canonicalizeSerializable(result.metadata),
593
603
  output: result.output,
594
604
  ...(result.quality !== undefined ? { quality: canonicalizeSerializable(result.quality) } : {}),