@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
@@ -1,7 +1,9 @@
1
1
  import { DogpileError } from "../types.js";
2
2
  import type {
3
3
  AbortedEvent,
4
+ BudgetStopEvent,
4
5
  BudgetTier,
6
+ CostSummary,
5
7
  DogpileErrorCode,
6
8
  DogpileOptions,
7
9
  Engine,
@@ -9,11 +11,13 @@ import type {
9
11
  FinalEvent,
10
12
  JsonObject,
11
13
  JsonValue,
14
+ ModelRequestEvent,
12
15
  ProtocolSelection,
13
16
  RunCallOptions,
14
17
  RunEvaluation,
15
18
  RunEvent,
16
19
  RunResult,
20
+ ReplayTraceProviderCall,
17
21
  SubRunFailedEvent,
18
22
  StreamErrorEvent,
19
23
  StreamEvent,
@@ -25,6 +29,7 @@ import type {
25
29
  import { runBroadcast } from "./broadcast.js";
26
30
  import { runCoordinator, type AbortDrainFn } from "./coordinator.js";
27
31
  import {
32
+ addCost,
28
33
  createReplayTraceFinalOutput,
29
34
  createReplayTraceBudgetStateChanges,
30
35
  canonicalizeRunResult,
@@ -34,12 +39,14 @@ import {
34
39
  createRunMetadata,
35
40
  createRunUsage,
36
41
  defaultAgents,
42
+ emptyCost,
37
43
  normalizeProtocol,
38
44
  orderAgentsForTemperature,
39
45
  recomputeAccountingFromTrace,
40
46
  resolveOnChildFailure,
41
47
  tierTemperature
42
48
  } from "./defaults.js";
49
+ import { computeHealth, DEFAULT_HEALTH_THRESHOLDS } from "./health.js";
43
50
  import { runSequential } from "./sequential.js";
44
51
  import { runShared } from "./shared.js";
45
52
  import {
@@ -56,9 +63,13 @@ import {
56
63
  validateProviderLocality,
57
64
  validateRunCallOptions
58
65
  } from "./validation.js";
66
+ import { DOGPILE_SPAN_NAMES, type DogpileSpan, type DogpileTracer } from "./tracing.js";
67
+ import type { Logger } from "./logger.js";
68
+ import type { MetricsHook, RunMetricsSnapshot } from "./metrics.js";
59
69
 
60
70
  const DEFAULT_MAX_DEPTH = 4;
61
71
  const DEFAULT_MAX_CONCURRENT_CHILDREN = 4;
72
+ const DEFAULT_MAX_CONCURRENT_AGENT_TURNS = 4;
62
73
 
63
74
  const defaultHighLevelProtocol = "sequential";
64
75
  const defaultHighLevelTier = "balanced";
@@ -89,6 +100,7 @@ export function createEngine(options: EngineOptions): Engine {
89
100
  const terminate = options.terminate ?? (options.budget ? conditionFromBudget(options.budget) : undefined);
90
101
  const engineMaxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
91
102
  const engineMaxConcurrentChildren = options.maxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN;
103
+ const engineMaxConcurrentAgentTurns = options.maxConcurrentAgentTurns ?? DEFAULT_MAX_CONCURRENT_AGENT_TURNS;
92
104
  const engineOnChildFailure = options.onChildFailure;
93
105
 
94
106
  return {
@@ -110,6 +122,15 @@ export function createEngine(options: EngineOptions): Engine {
110
122
  engineMaxConcurrentChildren,
111
123
  runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY
112
124
  );
125
+ assertRunDoesNotRaiseEngineMax(
126
+ "maxConcurrentAgentTurns",
127
+ runOptions?.maxConcurrentAgentTurns,
128
+ engineMaxConcurrentAgentTurns
129
+ );
130
+ const effectiveMaxConcurrentAgentTurns = Math.min(
131
+ engineMaxConcurrentAgentTurns,
132
+ runOptions?.maxConcurrentAgentTurns ?? Number.POSITIVE_INFINITY
133
+ );
113
134
  const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
114
135
 
115
136
  const startedAtMs = Date.now();
@@ -130,9 +151,13 @@ export function createEngine(options: EngineOptions): Engine {
130
151
  ...(terminate ? { terminate } : {}),
131
152
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
132
153
  ...(options.evaluate ? { evaluate: options.evaluate } : {}),
154
+ ...(options.tracer ? { tracer: options.tracer } : {}),
155
+ ...(options.metricsHook ? { metricsHook: options.metricsHook } : {}),
156
+ ...(options.logger ? { logger: options.logger } : {}),
133
157
  currentDepth: 0,
134
158
  effectiveMaxDepth,
135
159
  effectiveMaxConcurrentChildren,
160
+ effectiveMaxConcurrentAgentTurns,
136
161
  onChildFailure,
137
162
  ...(parentDeadlineMs !== undefined ? { parentDeadlineMs } : {}),
138
163
  ...(options.defaultSubRunTimeoutMs !== undefined
@@ -159,10 +184,18 @@ export function createEngine(options: EngineOptions): Engine {
159
184
  engineMaxConcurrentChildren,
160
185
  runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY
161
186
  );
187
+ assertRunDoesNotRaiseEngineMax(
188
+ "maxConcurrentAgentTurns",
189
+ runOptions?.maxConcurrentAgentTurns,
190
+ engineMaxConcurrentAgentTurns
191
+ );
192
+ const effectiveMaxConcurrentAgentTurns = Math.min(
193
+ engineMaxConcurrentAgentTurns,
194
+ runOptions?.maxConcurrentAgentTurns ?? Number.POSITIVE_INFINITY
195
+ );
162
196
  const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
163
197
 
164
- const pendingEvents: StreamEvent[] = [];
165
- const pendingResolvers: Array<(value: IteratorResult<StreamEvent>) => void> = [];
198
+ const pendingIteratorResolvers: Array<() => void> = [];
166
199
  const emittedEvents: StreamEvent[] = [];
167
200
  const subscribers = new Set<StreamEventSubscriber>();
168
201
  const abortController = new AbortController();
@@ -199,12 +232,20 @@ export function createEngine(options: EngineOptions): Engine {
199
232
  cancelRun();
200
233
  },
201
234
  subscribe(subscriber: StreamEventSubscriber) {
202
- subscribers.add(subscriber);
203
-
204
235
  for (const event of emittedEvents) {
205
236
  subscriber(event);
206
237
  }
207
238
 
239
+ if (complete) {
240
+ return {
241
+ unsubscribe(): void {
242
+ // Completed streams replay synchronously and have no live source.
243
+ }
244
+ };
245
+ }
246
+
247
+ subscribers.add(subscriber);
248
+
208
249
  return {
209
250
  unsubscribe(): void {
210
251
  subscribers.delete(subscriber);
@@ -212,20 +253,29 @@ export function createEngine(options: EngineOptions): Engine {
212
253
  };
213
254
  },
214
255
  [Symbol.asyncIterator](): AsyncIterator<StreamEvent> {
256
+ let cursor = 0;
257
+
215
258
  return {
216
259
  next(): Promise<IteratorResult<StreamEvent>> {
217
- const event = pendingEvents.shift();
218
- if (event) {
219
- return Promise.resolve({ done: false, value: event });
220
- }
221
- if (complete) {
222
- return Promise.resolve({ done: true, value: undefined });
223
- }
224
- return new Promise<IteratorResult<StreamEvent>>((resolve) => {
225
- pendingResolvers.push(resolve);
226
- });
260
+ return readNext();
227
261
  }
228
262
  };
263
+
264
+ function readNext(): Promise<IteratorResult<StreamEvent>> {
265
+ const event = emittedEvents[cursor];
266
+ if (event !== undefined) {
267
+ cursor += 1;
268
+ return Promise.resolve({ done: false, value: event });
269
+ }
270
+ if (complete) {
271
+ return Promise.resolve({ done: true, value: undefined });
272
+ }
273
+ return new Promise<IteratorResult<StreamEvent>>((resolve) => {
274
+ pendingIteratorResolvers.push(() => {
275
+ void readNext().then(resolve);
276
+ });
277
+ });
278
+ }
229
279
  }
230
280
  };
231
281
 
@@ -253,11 +303,15 @@ export function createEngine(options: EngineOptions): Engine {
253
303
  currentDepth: 0,
254
304
  effectiveMaxDepth,
255
305
  effectiveMaxConcurrentChildren,
306
+ effectiveMaxConcurrentAgentTurns,
256
307
  onChildFailure,
257
308
  ...(streamParentDeadlineMs !== undefined ? { parentDeadlineMs: streamParentDeadlineMs } : {}),
258
309
  ...(options.defaultSubRunTimeoutMs !== undefined
259
310
  ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs }
260
311
  : {}),
312
+ ...(options.tracer ? { tracer: options.tracer } : {}),
313
+ ...(options.metricsHook ? { metricsHook: options.metricsHook } : {}),
314
+ ...(options.logger ? { logger: options.logger } : {}),
261
315
  streamEvents: true,
262
316
  emit(event: RunEvent): void {
263
317
  if (status !== "running") {
@@ -346,8 +400,8 @@ export function createEngine(options: EngineOptions): Engine {
346
400
  timeoutLifecycle.cleanup();
347
401
  abortRace.cleanup();
348
402
  subscribers.clear();
349
- for (const resolver of pendingResolvers.splice(0)) {
350
- resolver({ done: true, value: undefined });
403
+ for (const resolvePending of pendingIteratorResolvers.splice(0)) {
404
+ resolvePending();
351
405
  }
352
406
  }
353
407
 
@@ -367,12 +421,9 @@ export function createEngine(options: EngineOptions): Engine {
367
421
  }
368
422
  }
369
423
 
370
- const resolver = pendingResolvers.shift();
371
- if (resolver) {
372
- resolver({ done: false, value: canonicalEvent });
373
- return;
424
+ for (const resolvePending of pendingIteratorResolvers.splice(0)) {
425
+ resolvePending();
374
426
  }
375
- pendingEvents.push(canonicalEvent);
376
427
  }
377
428
  }
378
429
  };
@@ -659,15 +710,17 @@ interface RunProtocolOptions {
659
710
  /**
660
711
  * Current recursion depth. Top-level runs use 0; the coordinator dispatch
661
712
  * loop increments before invoking {@link runProtocol} for a child run.
662
- * Plan 04 will wire `effectiveMaxDepth` validation around this value.
713
+ * Depth validation is enforced before delegate dispatch.
663
714
  */
664
715
  readonly currentDepth?: number;
665
716
  /**
666
- * Effective max recursion depth. Plan 04 enforces; Plan 03 plumbs the param.
717
+ * Effective max recursion depth after engine and per-run ceilings are merged.
667
718
  */
668
719
  readonly effectiveMaxDepth?: number;
669
720
  /** Effective max delegated child concurrency resolved at run start. */
670
721
  readonly effectiveMaxConcurrentChildren?: number;
722
+ /** Effective max agent-turn fan-out resolved at run start. */
723
+ readonly effectiveMaxConcurrentAgentTurns?: number;
671
724
  readonly onChildFailure?: EngineOptions["onChildFailure"];
672
725
  /**
673
726
  * Root-run deadline (epoch ms) threaded through every recursive coordinator
@@ -682,10 +735,489 @@ interface RunProtocolOptions {
682
735
  readonly defaultSubRunTimeoutMs?: number;
683
736
  readonly registerAbortDrain?: (drain: AbortDrainFn) => void;
684
737
  readonly failureInstancesByChildRunId?: Map<string, DogpileError>;
738
+ readonly tracer?: EngineOptions["tracer"];
739
+ readonly metricsHook?: EngineOptions["metricsHook"];
740
+ readonly logger?: EngineOptions["logger"];
741
+ /**
742
+ * Optional parent span for the next runProtocol invocation. Threaded by the
743
+ * coordinator when dispatching child runs so that the child's `dogpile.run`
744
+ * span is correctly nested under its parent's `dogpile.sub-run` span.
745
+ * Internal-only; not part of the public surface.
746
+ */
747
+ readonly parentSpan?: DogpileSpan;
748
+ /**
749
+ * Per-child sub-run span lookup, keyed by childRunId. Populated by the
750
+ * parent's emit closure on `sub-run-started`. The coordinator dispatcher
751
+ * reads this to thread the correct per-child span as parent for the
752
+ * recursive runProtocol call. Internal-only.
753
+ */
754
+ readonly subRunSpansByChildId?: ReadonlyMap<string, DogpileSpan>;
685
755
  }
686
756
 
687
757
  type NonStreamingProtocolOptions = Omit<RunProtocolOptions, "emit"> & Pick<EngineOptions, "evaluate">;
688
758
 
759
+ interface TracingState {
760
+ readonly tracer: DogpileTracer;
761
+ readonly runSpan: DogpileSpan;
762
+ readonly subRunSpans: Map<string, DogpileSpan>;
763
+ readonly agentTurnSpans: Map<string, DogpileSpan>;
764
+ readonly modelCallSpans: Map<string, DogpileSpan>;
765
+ readonly pendingModelRequests: Map<string, ModelRequestEvent>;
766
+ readonly agentTurnCounters: Map<string, number>;
767
+ readonly turnAccumByAgent: Map<string, TurnAccum>;
768
+ readonly agentIds: Set<string>;
769
+ runId?: string;
770
+ turnCount: number;
771
+ lastCost: CostSummary;
772
+ }
773
+
774
+ interface TurnAccum {
775
+ inputTokens: number;
776
+ outputTokens: number;
777
+ costUsd: number;
778
+ }
779
+
780
+ function openRunTracing(options: {
781
+ readonly tracer?: DogpileTracer;
782
+ readonly parentSpan?: DogpileSpan;
783
+ readonly intent: string;
784
+ readonly protocolKind: string;
785
+ readonly tier: unknown;
786
+ }): TracingState | undefined {
787
+ if (!options.tracer) {
788
+ return undefined;
789
+ }
790
+
791
+ const runSpan = options.tracer.startSpan(DOGPILE_SPAN_NAMES.RUN, {
792
+ ...(options.parentSpan ? { parent: options.parentSpan } : {}),
793
+ attributes: {
794
+ "dogpile.run.protocol": options.protocolKind,
795
+ "dogpile.run.tier": String(options.tier),
796
+ "dogpile.run.intent": options.intent.slice(0, 200)
797
+ }
798
+ });
799
+
800
+ return {
801
+ tracer: options.tracer,
802
+ runSpan,
803
+ subRunSpans: new Map(),
804
+ agentTurnSpans: new Map(),
805
+ modelCallSpans: new Map(),
806
+ pendingModelRequests: new Map(),
807
+ agentTurnCounters: new Map(),
808
+ turnAccumByAgent: new Map(),
809
+ agentIds: new Set(),
810
+ turnCount: 0,
811
+ lastCost: emptyCost()
812
+ };
813
+ }
814
+
815
+ interface MetricsState {
816
+ readonly metricsHook: MetricsHook;
817
+ readonly logger: Logger | undefined;
818
+ readonly startedAtMs: number;
819
+ readonly subRunStartTimes: Map<string, number>;
820
+ totalCost: CostSummary;
821
+ nestedCost: CostSummary;
822
+ turns: number;
823
+ }
824
+
825
+ function openRunMetrics(options: {
826
+ readonly metricsHook?: MetricsHook;
827
+ readonly logger?: Logger;
828
+ }): MetricsState | undefined {
829
+ if (!options.metricsHook) {
830
+ return undefined;
831
+ }
832
+
833
+ return {
834
+ metricsHook: options.metricsHook,
835
+ logger: options.logger,
836
+ startedAtMs: Date.now(),
837
+ subRunStartTimes: new Map(),
838
+ totalCost: emptyCost(),
839
+ nestedCost: emptyCost(),
840
+ turns: 0
841
+ };
842
+ }
843
+
844
+ function routeMetricsError(err: unknown, logger: Logger | undefined): void {
845
+ const msg = err instanceof Error ? err.message : String(err);
846
+ try {
847
+ if (logger !== undefined) {
848
+ logger.error("dogpile:metricsHook threw", { error: msg });
849
+ } else {
850
+ console.error("dogpile:metricsHook threw", { error: msg });
851
+ }
852
+ } catch {
853
+ // A logger that throws from error() cannot be helped.
854
+ }
855
+ }
856
+
857
+ function fireHook(
858
+ callback: ((snapshot: RunMetricsSnapshot) => void | Promise<void>) | undefined,
859
+ snapshot: RunMetricsSnapshot,
860
+ logger: Logger | undefined
861
+ ): void {
862
+ if (!callback) {
863
+ return;
864
+ }
865
+
866
+ try {
867
+ const result = callback(snapshot);
868
+ if (result && typeof (result as Promise<void>).catch === "function") {
869
+ (result as Promise<void>).catch((err: unknown) => {
870
+ routeMetricsError(err, logger);
871
+ });
872
+ }
873
+ } catch (err: unknown) {
874
+ routeMetricsError(err, logger);
875
+ }
876
+ }
877
+
878
+ function buildRunSnapshot(
879
+ result: RunResult,
880
+ startedAtMs: number
881
+ ): RunMetricsSnapshot {
882
+ const nestedCosts = nestedSubRunCosts(result);
883
+ const budgetStopEvent = result.trace.events.find((event): event is BudgetStopEvent => event.type === "budget-stop");
884
+ const outcome: RunMetricsSnapshot["outcome"] = budgetStopEvent !== undefined ? "budget-stopped" : "completed";
885
+ const totalInputTokens = result.cost.inputTokens;
886
+ const totalOutputTokens = result.cost.outputTokens;
887
+ const totalCostUsd = result.cost.usd;
888
+ const ownInputTokens =
889
+ totalInputTokens - nestedCosts.reduce((sum, cost) => sum + cost.inputTokens, 0);
890
+ const ownOutputTokens =
891
+ totalOutputTokens - nestedCosts.reduce((sum, cost) => sum + cost.outputTokens, 0);
892
+ const ownCostUsd =
893
+ totalCostUsd - nestedCosts.reduce((sum, cost) => sum + cost.usd, 0);
894
+ const turns = result.trace.events.filter((event) => event.type === "agent-turn").length;
895
+
896
+ return {
897
+ outcome,
898
+ inputTokens: ownInputTokens,
899
+ outputTokens: ownOutputTokens,
900
+ costUsd: ownCostUsd,
901
+ totalInputTokens,
902
+ totalOutputTokens,
903
+ totalCostUsd,
904
+ turns,
905
+ durationMs: Date.now() - startedAtMs
906
+ };
907
+ }
908
+
909
+ function buildSubRunSnapshot(
910
+ subResult: RunResult,
911
+ durationMs: number
912
+ ): RunMetricsSnapshot {
913
+ const nestedCosts = nestedSubRunCosts(subResult);
914
+ const budgetStopEvent = subResult.trace.events.find((event): event is BudgetStopEvent => event.type === "budget-stop");
915
+ const outcome: RunMetricsSnapshot["outcome"] = budgetStopEvent !== undefined ? "budget-stopped" : "completed";
916
+ const totalInputTokens = subResult.cost.inputTokens;
917
+ const totalOutputTokens = subResult.cost.outputTokens;
918
+ const totalCostUsd = subResult.cost.usd;
919
+ const ownInputTokens =
920
+ totalInputTokens - nestedCosts.reduce((sum, cost) => sum + cost.inputTokens, 0);
921
+ const ownOutputTokens =
922
+ totalOutputTokens - nestedCosts.reduce((sum, cost) => sum + cost.outputTokens, 0);
923
+ const ownCostUsd =
924
+ totalCostUsd - nestedCosts.reduce((sum, cost) => sum + cost.usd, 0);
925
+ const turns = subResult.trace.events.filter((event) => event.type === "agent-turn").length;
926
+
927
+ return {
928
+ outcome,
929
+ inputTokens: ownInputTokens,
930
+ outputTokens: ownOutputTokens,
931
+ costUsd: ownCostUsd,
932
+ totalInputTokens,
933
+ totalOutputTokens,
934
+ totalCostUsd,
935
+ turns,
936
+ durationMs
937
+ };
938
+ }
939
+
940
+ function nestedSubRunCosts(result: RunResult): CostSummary[] {
941
+ return result.trace.events.flatMap((event) => {
942
+ if (event.type === "sub-run-completed") {
943
+ return [event.subResult.cost];
944
+ }
945
+ if (event.type === "sub-run-failed") {
946
+ return [event.partialCost];
947
+ }
948
+ return [];
949
+ });
950
+ }
951
+
952
+ function subtractCost(total: CostSummary, nested: CostSummary): CostSummary {
953
+ return {
954
+ usd: total.usd - nested.usd,
955
+ inputTokens: total.inputTokens - nested.inputTokens,
956
+ outputTokens: total.outputTokens - nested.outputTokens,
957
+ totalTokens: total.totalTokens - nested.totalTokens
958
+ };
959
+ }
960
+
961
+ function handleMetricsEvent(state: MetricsState, event: RunEvent): void {
962
+ const parentRunIds = (event as { readonly parentRunIds?: readonly string[] }).parentRunIds;
963
+ if (parentRunIds !== undefined) {
964
+ return;
965
+ }
966
+
967
+ switch (event.type) {
968
+ case "agent-turn": {
969
+ state.totalCost = event.cost;
970
+ state.turns += 1;
971
+ break;
972
+ }
973
+ case "broadcast":
974
+ case "budget-stop":
975
+ case "final": {
976
+ state.totalCost = event.cost;
977
+ break;
978
+ }
979
+ case "sub-run-started": {
980
+ state.subRunStartTimes.set(event.childRunId, Date.now());
981
+ break;
982
+ }
983
+ case "sub-run-completed": {
984
+ state.totalCost = addCost(state.totalCost, event.subResult.cost);
985
+ state.nestedCost = addCost(state.nestedCost, event.subResult.cost);
986
+ const startMs = state.subRunStartTimes.get(event.childRunId);
987
+ const durationMs = startMs !== undefined ? Date.now() - startMs : 0;
988
+ state.subRunStartTimes.delete(event.childRunId);
989
+ const snapshot = buildSubRunSnapshot(event.subResult, durationMs);
990
+ fireHook(state.metricsHook.onSubRunComplete, snapshot, state.logger);
991
+ break;
992
+ }
993
+ case "sub-run-failed": {
994
+ state.totalCost = addCost(state.totalCost, event.partialCost);
995
+ state.nestedCost = addCost(state.nestedCost, event.partialCost);
996
+ state.subRunStartTimes.delete(event.childRunId);
997
+ break;
998
+ }
999
+ default:
1000
+ break;
1001
+ }
1002
+ }
1003
+
1004
+ function closeRunMetrics(state: MetricsState, result: RunResult | undefined): void {
1005
+ if (result !== undefined) {
1006
+ const snapshot = buildRunSnapshot(result, state.startedAtMs);
1007
+ fireHook(state.metricsHook.onRunComplete, snapshot, state.logger);
1008
+ return;
1009
+ }
1010
+
1011
+ const ownCost = subtractCost(state.totalCost, state.nestedCost);
1012
+ const snapshot: RunMetricsSnapshot = {
1013
+ outcome: "aborted",
1014
+ inputTokens: ownCost.inputTokens,
1015
+ outputTokens: ownCost.outputTokens,
1016
+ costUsd: ownCost.usd,
1017
+ totalInputTokens: state.totalCost.inputTokens,
1018
+ totalOutputTokens: state.totalCost.outputTokens,
1019
+ totalCostUsd: state.totalCost.usd,
1020
+ turns: state.turns,
1021
+ durationMs: Date.now() - state.startedAtMs
1022
+ };
1023
+ fireHook(state.metricsHook.onRunComplete, snapshot, state.logger);
1024
+ }
1025
+
1026
+ function handleTracingEvent(state: TracingState, event: RunEvent): void {
1027
+ const parentRunIds = (event as { readonly parentRunIds?: readonly string[] }).parentRunIds;
1028
+ if (parentRunIds !== undefined) {
1029
+ return;
1030
+ }
1031
+
1032
+ if (state.runId === undefined) {
1033
+ state.runId = event.runId;
1034
+ state.runSpan.setAttribute("dogpile.run.id", event.runId);
1035
+ }
1036
+
1037
+ switch (event.type) {
1038
+ case "model-request": {
1039
+ state.pendingModelRequests.set(event.callId, event);
1040
+ state.agentIds.add(event.agentId);
1041
+
1042
+ if (!state.agentTurnSpans.has(event.agentId)) {
1043
+ const turnNumber = (state.agentTurnCounters.get(event.agentId) ?? 0) + 1;
1044
+ state.agentTurnCounters.set(event.agentId, turnNumber);
1045
+ const turnParent = state.subRunSpans.get(event.runId) ?? state.runSpan;
1046
+ const turnSpan = state.tracer.startSpan(DOGPILE_SPAN_NAMES.AGENT_TURN, {
1047
+ parent: turnParent,
1048
+ attributes: {
1049
+ "dogpile.agent.id": event.agentId,
1050
+ "dogpile.agent.role": event.role,
1051
+ "dogpile.turn.number": turnNumber,
1052
+ "dogpile.model.id": event.modelId
1053
+ }
1054
+ });
1055
+ state.agentTurnSpans.set(event.agentId, turnSpan);
1056
+ }
1057
+
1058
+ const callParent =
1059
+ state.agentTurnSpans.get(event.agentId) ??
1060
+ state.subRunSpans.get(event.runId) ??
1061
+ state.runSpan;
1062
+ const callSpan = state.tracer.startSpan(DOGPILE_SPAN_NAMES.MODEL_CALL, {
1063
+ parent: callParent,
1064
+ attributes: {
1065
+ "dogpile.model.id": event.modelId,
1066
+ "dogpile.call.id": event.callId,
1067
+ "dogpile.provider.id": event.providerId
1068
+ }
1069
+ });
1070
+ state.modelCallSpans.set(event.callId, callSpan);
1071
+ break;
1072
+ }
1073
+ case "model-response": {
1074
+ const span = state.modelCallSpans.get(event.callId);
1075
+ if (span) {
1076
+ const inputTokens = event.response.usage?.inputTokens ?? 0;
1077
+ const outputTokens = event.response.usage?.outputTokens ?? 0;
1078
+ const responseCost: CostSummary = {
1079
+ usd: event.response.costUsd ?? 0,
1080
+ inputTokens,
1081
+ outputTokens,
1082
+ totalTokens: event.response.usage?.totalTokens ?? inputTokens + outputTokens
1083
+ };
1084
+ span.setAttribute("dogpile.model.input_tokens", inputTokens);
1085
+ span.setAttribute("dogpile.model.output_tokens", outputTokens);
1086
+ if (event.response.costUsd !== undefined) {
1087
+ span.setAttribute("dogpile.model.cost_usd", event.response.costUsd);
1088
+ }
1089
+ span.setStatus("ok");
1090
+ span.end();
1091
+ state.modelCallSpans.delete(event.callId);
1092
+ const accum = state.turnAccumByAgent.get(event.agentId) ?? {
1093
+ inputTokens: 0,
1094
+ outputTokens: 0,
1095
+ costUsd: 0
1096
+ };
1097
+ accum.inputTokens += inputTokens;
1098
+ accum.outputTokens += outputTokens;
1099
+ accum.costUsd += responseCost.usd;
1100
+ state.turnAccumByAgent.set(event.agentId, accum);
1101
+ state.lastCost = addCost(state.lastCost, responseCost);
1102
+ }
1103
+ state.pendingModelRequests.delete(event.callId);
1104
+ break;
1105
+ }
1106
+ case "agent-turn": {
1107
+ state.agentIds.add(event.agentId);
1108
+ state.turnCount += 1;
1109
+ state.lastCost = event.cost;
1110
+ const turnSpan = state.agentTurnSpans.get(event.agentId);
1111
+ if (turnSpan) {
1112
+ turnSpan.setAttribute("dogpile.agent.role", event.role);
1113
+ const accum = state.turnAccumByAgent.get(event.agentId);
1114
+ turnSpan.setAttribute("dogpile.turn.cost_usd", accum?.costUsd ?? 0);
1115
+ turnSpan.setAttribute("dogpile.turn.input_tokens", accum?.inputTokens ?? 0);
1116
+ turnSpan.setAttribute("dogpile.turn.output_tokens", accum?.outputTokens ?? 0);
1117
+ turnSpan.setStatus("ok");
1118
+ turnSpan.end();
1119
+ state.agentTurnSpans.delete(event.agentId);
1120
+ }
1121
+ state.turnAccumByAgent.delete(event.agentId);
1122
+ break;
1123
+ }
1124
+ case "broadcast":
1125
+ case "budget-stop":
1126
+ case "final": {
1127
+ state.lastCost = event.cost;
1128
+ break;
1129
+ }
1130
+ case "sub-run-started": {
1131
+ const span = state.tracer.startSpan(DOGPILE_SPAN_NAMES.SUB_RUN, {
1132
+ parent: state.runSpan,
1133
+ attributes: {
1134
+ "dogpile.sub_run.child_run_id": event.childRunId,
1135
+ "dogpile.sub_run.parent_run_id": event.parentRunId,
1136
+ "dogpile.sub_run.depth": event.depth
1137
+ }
1138
+ });
1139
+ state.subRunSpans.set(event.childRunId, span);
1140
+ break;
1141
+ }
1142
+ case "sub-run-completed": {
1143
+ const span = state.subRunSpans.get(event.childRunId);
1144
+ if (span) {
1145
+ span.setStatus("ok");
1146
+ span.end();
1147
+ state.subRunSpans.delete(event.childRunId);
1148
+ }
1149
+ break;
1150
+ }
1151
+ case "sub-run-failed": {
1152
+ const span = state.subRunSpans.get(event.childRunId);
1153
+ if (span) {
1154
+ span.setStatus("error", event.error.message);
1155
+ span.end();
1156
+ state.subRunSpans.delete(event.childRunId);
1157
+ }
1158
+ break;
1159
+ }
1160
+ default:
1161
+ break;
1162
+ }
1163
+ }
1164
+
1165
+ function closeRunTracing(state: TracingState, result: RunResult | undefined, error?: unknown): void {
1166
+ if (error !== undefined) {
1167
+ if (state.runId !== undefined) {
1168
+ state.runSpan.setAttribute("dogpile.run.id", state.runId);
1169
+ }
1170
+ state.runSpan.setAttribute("dogpile.run.agent_count", state.agentIds.size);
1171
+ state.runSpan.setAttribute("dogpile.run.turn_count", state.turnCount);
1172
+ state.runSpan.setAttribute("dogpile.run.cost_usd", state.lastCost.usd);
1173
+ state.runSpan.setAttribute("dogpile.run.input_tokens", state.lastCost.inputTokens);
1174
+ state.runSpan.setAttribute("dogpile.run.output_tokens", state.lastCost.outputTokens);
1175
+ state.runSpan.setAttribute("dogpile.run.outcome", "aborted");
1176
+ state.runSpan.setStatus("error", error instanceof Error ? error.message : String(error));
1177
+ closeOpenTracingSpans(state);
1178
+ state.runSpan.end();
1179
+ return;
1180
+ }
1181
+
1182
+ if (result === undefined) {
1183
+ closeOpenTracingSpans(state);
1184
+ state.runSpan.end();
1185
+ return;
1186
+ }
1187
+
1188
+ const budgetStopEvent = result.trace.events.find((event): event is BudgetStopEvent => event.type === "budget-stop");
1189
+ const terminationReason = budgetStopEvent?.reason;
1190
+ const outcome = terminationReason !== undefined ? "budget-stopped" : "completed";
1191
+ state.runSpan.setAttribute("dogpile.run.id", result.trace.runId);
1192
+ state.runSpan.setAttribute("dogpile.run.agent_count", result.trace.agentsUsed.length);
1193
+ state.runSpan.setAttribute("dogpile.run.turn_count", result.trace.events.filter((event) => event.type === "agent-turn").length);
1194
+ state.runSpan.setAttribute("dogpile.run.cost_usd", result.cost.usd);
1195
+ state.runSpan.setAttribute("dogpile.run.input_tokens", result.cost.inputTokens);
1196
+ state.runSpan.setAttribute("dogpile.run.output_tokens", result.cost.outputTokens);
1197
+ state.runSpan.setAttribute("dogpile.run.outcome", outcome);
1198
+ if (terminationReason !== undefined) {
1199
+ state.runSpan.setAttribute("dogpile.run.termination_reason", terminationReason);
1200
+ }
1201
+ state.runSpan.setStatus("ok");
1202
+ closeOpenTracingSpans(state);
1203
+ state.runSpan.end();
1204
+ }
1205
+
1206
+ function closeOpenTracingSpans(state: TracingState): void {
1207
+ for (const span of state.modelCallSpans.values()) {
1208
+ span.end();
1209
+ }
1210
+ state.modelCallSpans.clear();
1211
+ for (const span of state.agentTurnSpans.values()) {
1212
+ span.end();
1213
+ }
1214
+ state.agentTurnSpans.clear();
1215
+ for (const span of state.subRunSpans.values()) {
1216
+ span.end();
1217
+ }
1218
+ state.subRunSpans.clear();
1219
+ }
1220
+
689
1221
  async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Promise<RunResult> {
690
1222
  const failureInstancesByChildRunId = new Map<string, DogpileError>();
691
1223
  const abortLifecycle = createNonStreamingAbortLifecycle({
@@ -728,7 +1260,8 @@ async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Pr
728
1260
  events
729
1261
  }),
730
1262
  eventLog: createRunEventLog(trace.runId, trace.protocol, events),
731
- trace
1263
+ trace,
1264
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
732
1265
  };
733
1266
  const terminalThrow = resolveRuntimeTerminalThrow(runResult.trace, failureInstancesByChildRunId);
734
1267
  if (terminalThrow) {
@@ -781,7 +1314,61 @@ function finalEventWithEvaluation(event: FinalEvent, evaluation: RunEvaluation):
781
1314
  };
782
1315
  }
783
1316
 
784
- function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
1317
+ async function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
1318
+ const tracing = openRunTracing({
1319
+ ...(options.tracer ? { tracer: options.tracer } : {}),
1320
+ ...(options.parentSpan ? { parentSpan: options.parentSpan } : {}),
1321
+ intent: options.intent,
1322
+ protocolKind: options.protocol.kind,
1323
+ tier: options.tier
1324
+ });
1325
+ const metrics = openRunMetrics({
1326
+ ...(options.metricsHook ? { metricsHook: options.metricsHook } : {}),
1327
+ ...(options.logger ? { logger: options.logger } : {})
1328
+ });
1329
+ const emitForProtocol =
1330
+ tracing || metrics || options.emit
1331
+ ? (event: RunEvent): void => {
1332
+ if (tracing) {
1333
+ handleTracingEvent(tracing, event);
1334
+ }
1335
+ if (metrics) {
1336
+ handleMetricsEvent(metrics, event);
1337
+ }
1338
+ options.emit?.(event);
1339
+ }
1340
+ : undefined;
1341
+ const protocolOptions = tracing
1342
+ ? {
1343
+ ...options,
1344
+ subRunSpansByChildId: tracing.subRunSpans
1345
+ }
1346
+ : options;
1347
+
1348
+ try {
1349
+ const result = await runProtocolInner(protocolOptions, emitForProtocol);
1350
+ if (tracing) {
1351
+ closeRunTracing(tracing, result);
1352
+ }
1353
+ if (metrics && (options.currentDepth === 0 || options.currentDepth === undefined)) {
1354
+ closeRunMetrics(metrics, result);
1355
+ }
1356
+ return result;
1357
+ } catch (error) {
1358
+ if (tracing) {
1359
+ closeRunTracing(tracing, undefined, error);
1360
+ }
1361
+ if (metrics && (options.currentDepth === 0 || options.currentDepth === undefined)) {
1362
+ closeRunMetrics(metrics, undefined);
1363
+ }
1364
+ throw error;
1365
+ }
1366
+ }
1367
+
1368
+ function runProtocolInner(
1369
+ options: RunProtocolOptions,
1370
+ emitForProtocol?: (event: RunEvent) => void
1371
+ ): Promise<RunResult> {
785
1372
  switch (options.protocol.kind) {
786
1373
  case "sequential":
787
1374
  return runSequential({
@@ -797,7 +1384,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
797
1384
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
798
1385
  ...(options.terminate ? { terminate: options.terminate } : {}),
799
1386
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
800
- ...(options.emit ? { emit: options.emit } : {})
1387
+ ...(emitForProtocol ? { emit: emitForProtocol } : {})
801
1388
  });
802
1389
  case "broadcast":
803
1390
  return runBroadcast({
@@ -813,7 +1400,10 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
813
1400
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
814
1401
  ...(options.terminate ? { terminate: options.terminate } : {}),
815
1402
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
816
- ...(options.emit ? { emit: options.emit } : {})
1403
+ ...(options.effectiveMaxConcurrentAgentTurns !== undefined
1404
+ ? { maxConcurrentAgentTurns: options.effectiveMaxConcurrentAgentTurns }
1405
+ : {}),
1406
+ ...(emitForProtocol ? { emit: emitForProtocol } : {})
817
1407
  });
818
1408
  case "coordinator":
819
1409
  return runCoordinator({
@@ -829,11 +1419,16 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
829
1419
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
830
1420
  ...(options.terminate ? { terminate: options.terminate } : {}),
831
1421
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
832
- ...(options.emit ? { emit: options.emit } : {}),
1422
+ ...(options.effectiveMaxConcurrentAgentTurns !== undefined
1423
+ ? { maxConcurrentAgentTurns: options.effectiveMaxConcurrentAgentTurns }
1424
+ : {}),
1425
+ ...(emitForProtocol ? { emit: emitForProtocol } : {}),
833
1426
  ...(options.streamEvents !== undefined ? { streamEvents: options.streamEvents } : {}),
834
1427
  currentDepth: options.currentDepth ?? 0,
835
1428
  effectiveMaxDepth: options.effectiveMaxDepth ?? Infinity,
836
1429
  effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN,
1430
+ effectiveMaxConcurrentAgentTurns:
1431
+ options.effectiveMaxConcurrentAgentTurns ?? DEFAULT_MAX_CONCURRENT_AGENT_TURNS,
837
1432
  onChildFailure: options.onChildFailure ?? "continue",
838
1433
  ...(options.parentDeadlineMs !== undefined ? { parentDeadlineMs: options.parentDeadlineMs } : {}),
839
1434
  ...(options.defaultSubRunTimeoutMs !== undefined
@@ -843,11 +1438,17 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
843
1438
  ...(options.failureInstancesByChildRunId !== undefined
844
1439
  ? { failureInstancesByChildRunId: options.failureInstancesByChildRunId }
845
1440
  : {}),
846
- runProtocol: (childInput) =>
847
- runProtocol({
848
- ...childInput,
849
- protocol: normalizeProtocol(childInput.protocol)
850
- })
1441
+ runProtocol: (childInput) => {
1442
+ const { runId: childRunId, ...childProtocolInput } = childInput;
1443
+ const childParent = options.subRunSpansByChildId?.get(childRunId) ?? options.parentSpan;
1444
+ return runProtocol({
1445
+ ...childProtocolInput,
1446
+ protocol: normalizeProtocol(childProtocolInput.protocol),
1447
+ ...(options.tracer ? { tracer: options.tracer } : {}),
1448
+ ...(childParent ? { parentSpan: childParent } : {}),
1449
+ ...(options.logger ? { logger: options.logger } : {})
1450
+ });
1451
+ }
851
1452
  });
852
1453
  case "shared":
853
1454
  return runShared({
@@ -863,7 +1464,10 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
863
1464
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
864
1465
  ...(options.terminate ? { terminate: options.terminate } : {}),
865
1466
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
866
- ...(options.emit ? { emit: options.emit } : {})
1467
+ ...(options.effectiveMaxConcurrentAgentTurns !== undefined
1468
+ ? { maxConcurrentAgentTurns: options.effectiveMaxConcurrentAgentTurns }
1469
+ : {}),
1470
+ ...(emitForProtocol ? { emit: emitForProtocol } : {})
867
1471
  });
868
1472
  }
869
1473
  }
@@ -916,7 +1520,14 @@ export function stream(options: DogpileOptions): StreamHandle {
916
1520
  * the ergonomic {@link RunResult} wrapper from the JSON-serializable
917
1521
  * {@link Trace} returned by a previous `run()`, `stream()`, or
918
1522
  * `Dogpile.pile()` call.
1523
+ *
1524
+ * Tracing and metrics: replay is intentionally tracing-free and metrics-free.
1525
+ * Even when an engine instance has been configured with a `tracer` or
1526
+ * `metricsHook` on its `EngineOptions`, calling this function emits no spans
1527
+ * or callbacks — replaying historical events with current timestamps would
1528
+ * confuse observability backends. See `docs/developer-usage.md`.
919
1529
  */
1530
+ // Tracing/metrics-free: replay never uses EngineOptions tracer or metricsHook.
920
1531
  export function replay(trace: Trace): RunResult {
921
1532
  const cost = trace.finalOutput.cost;
922
1533
  const lastEvent = trace.events.at(-1);
@@ -931,7 +1542,11 @@ export function replay(trace: Trace): RunResult {
931
1542
  }
932
1543
  const baseResult = {
933
1544
  output: trace.finalOutput.output,
934
- eventLog: createRunEventLog(trace.runId, trace.protocol, trace.events),
1545
+ eventLog: createRunEventLog(
1546
+ trace.runId,
1547
+ trace.protocol,
1548
+ synthesizeProviderEvents(trace, trace.providerCalls)
1549
+ ),
935
1550
  trace,
936
1551
  transcript: trace.transcript,
937
1552
  usage: createRunUsage(cost),
@@ -944,7 +1559,8 @@ export function replay(trace: Trace): RunResult {
944
1559
  events: trace.events
945
1560
  }),
946
1561
  accounting,
947
- cost
1562
+ cost,
1563
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
948
1564
  };
949
1565
 
950
1566
  if (lastEvent?.type !== "final") {
@@ -958,6 +1574,60 @@ export function replay(trace: Trace): RunResult {
958
1574
  };
959
1575
  }
960
1576
 
1577
+ function synthesizeProviderEvents(
1578
+ trace: Trace,
1579
+ providerCalls: readonly ReplayTraceProviderCall[]
1580
+ ): readonly RunEvent[] {
1581
+ const hasLiveProvenance = trace.events.some(
1582
+ (event) => event.type === "model-request" || event.type === "model-response"
1583
+ );
1584
+ if (hasLiveProvenance) {
1585
+ return trace.events;
1586
+ }
1587
+
1588
+ const baseEvents = trace.events.filter(
1589
+ (event) => event.type !== "model-request" && event.type !== "model-response"
1590
+ );
1591
+ const result: RunEvent[] = [];
1592
+ let turnCount = 0;
1593
+
1594
+ for (const event of baseEvents) {
1595
+ if (event.type === "agent-turn") {
1596
+ const call = providerCalls[turnCount];
1597
+ if (call !== undefined) {
1598
+ const modelId = typeof call.modelId === "string" && call.modelId.length > 0 ? call.modelId : call.providerId;
1599
+ result.push({
1600
+ type: "model-request",
1601
+ runId: trace.runId,
1602
+ callId: call.callId,
1603
+ providerId: call.providerId,
1604
+ modelId,
1605
+ startedAt: call.startedAt,
1606
+ agentId: call.agentId,
1607
+ role: call.role,
1608
+ request: call.request
1609
+ });
1610
+ result.push({
1611
+ type: "model-response",
1612
+ runId: trace.runId,
1613
+ callId: call.callId,
1614
+ providerId: call.providerId,
1615
+ modelId,
1616
+ startedAt: call.startedAt,
1617
+ completedAt: call.completedAt,
1618
+ agentId: call.agentId,
1619
+ role: call.role,
1620
+ response: call.response
1621
+ });
1622
+ }
1623
+ turnCount += 1;
1624
+ }
1625
+ result.push(event);
1626
+ }
1627
+
1628
+ return result;
1629
+ }
1630
+
961
1631
  function resolveRuntimeTerminalThrow(
962
1632
  trace: Trace,
963
1633
  failureInstancesByChildRunId: ReadonlyMap<string, DogpileError>
@@ -1060,14 +1730,21 @@ function dogpileErrorFromSerializedPayload(input: {
1060
1730
  * Replay a saved completed trace as a stream without invoking a model provider.
1061
1731
  *
1062
1732
  * @remarks
1063
- * This is the streaming counterpart to {@link replay}. It yields the exact
1064
- * saved {@link Trace.events} in order and resolves {@link StreamHandle.result}
1065
- * to the rehydrated {@link RunResult}. Since all data comes from the trace,
1066
- * replay remains storage-free and provider-free.
1733
+ * This is the streaming counterpart to {@link replay}. It yields the same
1734
+ * event sequence exposed by the replayed result event log, including legacy
1735
+ * provenance synthesis when a saved trace predates model request/response
1736
+ * events. Since all data comes from the trace, replay remains storage-free and
1737
+ * provider-free.
1738
+ *
1739
+ * Tracing and metrics: replayStream is intentionally tracing-free and
1740
+ * metrics-free. Even when an engine instance has been configured with a
1741
+ * `tracer` or `metricsHook` on its `EngineOptions`, calling this function
1742
+ * emits no spans or callbacks — replaying historical events with current
1743
+ * timestamps would confuse observability backends. See `docs/developer-usage.md`.
1067
1744
  */
1745
+ // Tracing/metrics-free: replayStream never uses EngineOptions tracer or metricsHook.
1068
1746
  export function replayStream(trace: Trace): StreamHandle {
1069
1747
  const result = Promise.resolve(replay(trace));
1070
- const replayEvents = replayStreamEvents(trace);
1071
1748
 
1072
1749
  return {
1073
1750
  get status(): StreamHandleStatus {
@@ -1078,7 +1755,7 @@ export function replayStream(trace: Trace): StreamHandle {
1078
1755
  // Replay streams are already completed snapshots, so cancellation is a no-op.
1079
1756
  },
1080
1757
  subscribe(subscriber: StreamEventSubscriber) {
1081
- for (const event of replayEvents) {
1758
+ for (const event of replayStreamEvents(trace)) {
1082
1759
  subscriber(event);
1083
1760
  }
1084
1761
 
@@ -1089,34 +1766,24 @@ export function replayStream(trace: Trace): StreamHandle {
1089
1766
  };
1090
1767
  },
1091
1768
  [Symbol.asyncIterator](): AsyncIterator<StreamEvent> {
1092
- let index = 0;
1769
+ const iterator = replayStreamEvents(trace)[Symbol.iterator]();
1093
1770
 
1094
1771
  return {
1095
1772
  next(): Promise<IteratorResult<StreamEvent>> {
1096
- const event = replayEvents[index];
1097
- if (event) {
1098
- index += 1;
1099
- return Promise.resolve({ done: false, value: event });
1100
- }
1101
-
1102
- return Promise.resolve({ done: true, value: undefined });
1773
+ return Promise.resolve(iterator.next());
1103
1774
  }
1104
1775
  };
1105
1776
  }
1106
1777
  };
1107
1778
  }
1108
1779
 
1109
- function replayStreamEvents(trace: Trace, parentRunIds: readonly string[] = []): StreamEvent[] {
1110
- const events: StreamEvent[] = [];
1111
-
1112
- for (const event of trace.events) {
1780
+ function* replayStreamEvents(trace: Trace, parentRunIds: readonly string[] = []): IterableIterator<StreamEvent> {
1781
+ for (const event of synthesizeProviderEvents(trace, trace.providerCalls)) {
1113
1782
  if (event.type === "sub-run-completed") {
1114
- events.push(...replayStreamEvents(event.subResult.trace, [...parentRunIds, trace.runId]));
1783
+ yield* replayStreamEvents(event.subResult.trace, [...parentRunIds, trace.runId]);
1115
1784
  }
1116
- events.push(wrapReplayStreamEvent(event, parentRunIds));
1785
+ yield wrapReplayStreamEvent(event, parentRunIds);
1117
1786
  }
1118
-
1119
- return events;
1120
1787
  }
1121
1788
 
1122
1789
  function wrapReplayStreamEvent(event: RunEvent, parentRunIds: readonly string[]): StreamEvent {