@dogpile/sdk 0.4.0 → 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 (83) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/browser/index.js +726 -176
  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 +1 -0
  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.map +1 -1
  16. package/dist/runtime/broadcast.js +39 -36
  17. package/dist/runtime/broadcast.js.map +1 -1
  18. package/dist/runtime/coordinator.d.ts +5 -0
  19. package/dist/runtime/coordinator.d.ts.map +1 -1
  20. package/dist/runtime/coordinator.js +50 -39
  21. package/dist/runtime/coordinator.js.map +1 -1
  22. package/dist/runtime/defaults.d.ts.map +1 -1
  23. package/dist/runtime/defaults.js +12 -4
  24. package/dist/runtime/defaults.js.map +1 -1
  25. package/dist/runtime/engine.d.ts +17 -4
  26. package/dist/runtime/engine.d.ts.map +1 -1
  27. package/dist/runtime/engine.js +523 -18
  28. package/dist/runtime/engine.js.map +1 -1
  29. package/dist/runtime/health.d.ts +51 -0
  30. package/dist/runtime/health.d.ts.map +1 -0
  31. package/dist/runtime/health.js +85 -0
  32. package/dist/runtime/health.js.map +1 -0
  33. package/dist/runtime/introspection.d.ts +96 -0
  34. package/dist/runtime/introspection.d.ts.map +1 -0
  35. package/dist/runtime/introspection.js +31 -0
  36. package/dist/runtime/introspection.js.map +1 -0
  37. package/dist/runtime/metrics.d.ts +44 -0
  38. package/dist/runtime/metrics.d.ts.map +1 -0
  39. package/dist/runtime/metrics.js +12 -0
  40. package/dist/runtime/metrics.js.map +1 -0
  41. package/dist/runtime/model.d.ts.map +1 -1
  42. package/dist/runtime/model.js +34 -7
  43. package/dist/runtime/model.js.map +1 -1
  44. package/dist/runtime/provenance.d.ts +25 -0
  45. package/dist/runtime/provenance.d.ts.map +1 -0
  46. package/dist/runtime/provenance.js +13 -0
  47. package/dist/runtime/provenance.js.map +1 -0
  48. package/dist/runtime/sequential.d.ts.map +1 -1
  49. package/dist/runtime/sequential.js +39 -36
  50. package/dist/runtime/sequential.js.map +1 -1
  51. package/dist/runtime/shared.d.ts.map +1 -1
  52. package/dist/runtime/shared.js +39 -36
  53. package/dist/runtime/shared.js.map +1 -1
  54. package/dist/runtime/tracing.d.ts +31 -0
  55. package/dist/runtime/tracing.d.ts.map +1 -0
  56. package/dist/runtime/tracing.js +18 -0
  57. package/dist/runtime/tracing.js.map +1 -0
  58. package/dist/types/events.d.ts +10 -4
  59. package/dist/types/events.d.ts.map +1 -1
  60. package/dist/types/replay.d.ts +2 -0
  61. package/dist/types/replay.d.ts.map +1 -1
  62. package/dist/types.d.ts +124 -1
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/package.json +39 -1
  66. package/src/index.ts +5 -0
  67. package/src/providers/openai-compatible.ts +1 -0
  68. package/src/runtime/audit.ts +121 -0
  69. package/src/runtime/broadcast.ts +40 -37
  70. package/src/runtime/coordinator.ts +54 -39
  71. package/src/runtime/defaults.ts +13 -4
  72. package/src/runtime/engine.ts +648 -18
  73. package/src/runtime/health.ts +136 -0
  74. package/src/runtime/introspection.ts +122 -0
  75. package/src/runtime/metrics.ts +45 -0
  76. package/src/runtime/model.ts +38 -6
  77. package/src/runtime/provenance.ts +43 -0
  78. package/src/runtime/sequential.ts +40 -37
  79. package/src/runtime/shared.ts +40 -37
  80. package/src/runtime/tracing.ts +35 -0
  81. package/src/types/events.ts +10 -4
  82. package/src/types/replay.ts +2 -0
  83. package/src/types.ts +132 -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,6 +63,9 @@ 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;
@@ -130,6 +140,9 @@ export function createEngine(options: EngineOptions): Engine {
130
140
  ...(terminate ? { terminate } : {}),
131
141
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
132
142
  ...(options.evaluate ? { evaluate: options.evaluate } : {}),
143
+ ...(options.tracer ? { tracer: options.tracer } : {}),
144
+ ...(options.metricsHook ? { metricsHook: options.metricsHook } : {}),
145
+ ...(options.logger ? { logger: options.logger } : {}),
133
146
  currentDepth: 0,
134
147
  effectiveMaxDepth,
135
148
  effectiveMaxConcurrentChildren,
@@ -258,6 +271,9 @@ export function createEngine(options: EngineOptions): Engine {
258
271
  ...(options.defaultSubRunTimeoutMs !== undefined
259
272
  ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs }
260
273
  : {}),
274
+ ...(options.tracer ? { tracer: options.tracer } : {}),
275
+ ...(options.metricsHook ? { metricsHook: options.metricsHook } : {}),
276
+ ...(options.logger ? { logger: options.logger } : {}),
261
277
  streamEvents: true,
262
278
  emit(event: RunEvent): void {
263
279
  if (status !== "running") {
@@ -682,10 +698,489 @@ interface RunProtocolOptions {
682
698
  readonly defaultSubRunTimeoutMs?: number;
683
699
  readonly registerAbortDrain?: (drain: AbortDrainFn) => void;
684
700
  readonly failureInstancesByChildRunId?: Map<string, DogpileError>;
701
+ readonly tracer?: EngineOptions["tracer"];
702
+ readonly metricsHook?: EngineOptions["metricsHook"];
703
+ readonly logger?: EngineOptions["logger"];
704
+ /**
705
+ * Optional parent span for the next runProtocol invocation. Threaded by the
706
+ * coordinator when dispatching child runs so that the child's `dogpile.run`
707
+ * span is correctly nested under its parent's `dogpile.sub-run` span.
708
+ * Internal-only; not part of the public surface.
709
+ */
710
+ readonly parentSpan?: DogpileSpan;
711
+ /**
712
+ * Per-child sub-run span lookup, keyed by childRunId. Populated by the
713
+ * parent's emit closure on `sub-run-started`. The coordinator dispatcher
714
+ * reads this to thread the correct per-child span as parent for the
715
+ * recursive runProtocol call. Internal-only.
716
+ */
717
+ readonly subRunSpansByChildId?: ReadonlyMap<string, DogpileSpan>;
685
718
  }
686
719
 
687
720
  type NonStreamingProtocolOptions = Omit<RunProtocolOptions, "emit"> & Pick<EngineOptions, "evaluate">;
688
721
 
722
+ interface TracingState {
723
+ readonly tracer: DogpileTracer;
724
+ readonly runSpan: DogpileSpan;
725
+ readonly subRunSpans: Map<string, DogpileSpan>;
726
+ readonly agentTurnSpans: Map<string, DogpileSpan>;
727
+ readonly modelCallSpans: Map<string, DogpileSpan>;
728
+ readonly pendingModelRequests: Map<string, ModelRequestEvent>;
729
+ readonly agentTurnCounters: Map<string, number>;
730
+ readonly turnAccumByAgent: Map<string, TurnAccum>;
731
+ readonly agentIds: Set<string>;
732
+ runId?: string;
733
+ turnCount: number;
734
+ lastCost: CostSummary;
735
+ }
736
+
737
+ interface TurnAccum {
738
+ inputTokens: number;
739
+ outputTokens: number;
740
+ costUsd: number;
741
+ }
742
+
743
+ function openRunTracing(options: {
744
+ readonly tracer?: DogpileTracer;
745
+ readonly parentSpan?: DogpileSpan;
746
+ readonly intent: string;
747
+ readonly protocolKind: string;
748
+ readonly tier: unknown;
749
+ }): TracingState | undefined {
750
+ if (!options.tracer) {
751
+ return undefined;
752
+ }
753
+
754
+ const runSpan = options.tracer.startSpan(DOGPILE_SPAN_NAMES.RUN, {
755
+ ...(options.parentSpan ? { parent: options.parentSpan } : {}),
756
+ attributes: {
757
+ "dogpile.run.protocol": options.protocolKind,
758
+ "dogpile.run.tier": String(options.tier),
759
+ "dogpile.run.intent": options.intent.slice(0, 200)
760
+ }
761
+ });
762
+
763
+ return {
764
+ tracer: options.tracer,
765
+ runSpan,
766
+ subRunSpans: new Map(),
767
+ agentTurnSpans: new Map(),
768
+ modelCallSpans: new Map(),
769
+ pendingModelRequests: new Map(),
770
+ agentTurnCounters: new Map(),
771
+ turnAccumByAgent: new Map(),
772
+ agentIds: new Set(),
773
+ turnCount: 0,
774
+ lastCost: emptyCost()
775
+ };
776
+ }
777
+
778
+ interface MetricsState {
779
+ readonly metricsHook: MetricsHook;
780
+ readonly logger: Logger | undefined;
781
+ readonly startedAtMs: number;
782
+ readonly subRunStartTimes: Map<string, number>;
783
+ totalCost: CostSummary;
784
+ nestedCost: CostSummary;
785
+ turns: number;
786
+ }
787
+
788
+ function openRunMetrics(options: {
789
+ readonly metricsHook?: MetricsHook;
790
+ readonly logger?: Logger;
791
+ }): MetricsState | undefined {
792
+ if (!options.metricsHook) {
793
+ return undefined;
794
+ }
795
+
796
+ return {
797
+ metricsHook: options.metricsHook,
798
+ logger: options.logger,
799
+ startedAtMs: Date.now(),
800
+ subRunStartTimes: new Map(),
801
+ totalCost: emptyCost(),
802
+ nestedCost: emptyCost(),
803
+ turns: 0
804
+ };
805
+ }
806
+
807
+ function routeMetricsError(err: unknown, logger: Logger | undefined): void {
808
+ const msg = err instanceof Error ? err.message : String(err);
809
+ try {
810
+ if (logger !== undefined) {
811
+ logger.error("dogpile:metricsHook threw", { error: msg });
812
+ } else {
813
+ console.error("dogpile:metricsHook threw", { error: msg });
814
+ }
815
+ } catch {
816
+ // A logger that throws from error() cannot be helped.
817
+ }
818
+ }
819
+
820
+ function fireHook(
821
+ callback: ((snapshot: RunMetricsSnapshot) => void | Promise<void>) | undefined,
822
+ snapshot: RunMetricsSnapshot,
823
+ logger: Logger | undefined
824
+ ): void {
825
+ if (!callback) {
826
+ return;
827
+ }
828
+
829
+ try {
830
+ const result = callback(snapshot);
831
+ if (result && typeof (result as Promise<void>).catch === "function") {
832
+ (result as Promise<void>).catch((err: unknown) => {
833
+ routeMetricsError(err, logger);
834
+ });
835
+ }
836
+ } catch (err: unknown) {
837
+ routeMetricsError(err, logger);
838
+ }
839
+ }
840
+
841
+ function buildRunSnapshot(
842
+ result: RunResult,
843
+ startedAtMs: number
844
+ ): RunMetricsSnapshot {
845
+ const nestedCosts = nestedSubRunCosts(result);
846
+ const budgetStopEvent = result.trace.events.find((event): event is BudgetStopEvent => event.type === "budget-stop");
847
+ const outcome: RunMetricsSnapshot["outcome"] = budgetStopEvent !== undefined ? "budget-stopped" : "completed";
848
+ const totalInputTokens = result.cost.inputTokens;
849
+ const totalOutputTokens = result.cost.outputTokens;
850
+ const totalCostUsd = result.cost.usd;
851
+ const ownInputTokens =
852
+ totalInputTokens - nestedCosts.reduce((sum, cost) => sum + cost.inputTokens, 0);
853
+ const ownOutputTokens =
854
+ totalOutputTokens - nestedCosts.reduce((sum, cost) => sum + cost.outputTokens, 0);
855
+ const ownCostUsd =
856
+ totalCostUsd - nestedCosts.reduce((sum, cost) => sum + cost.usd, 0);
857
+ const turns = result.trace.events.filter((event) => event.type === "agent-turn").length;
858
+
859
+ return {
860
+ outcome,
861
+ inputTokens: ownInputTokens,
862
+ outputTokens: ownOutputTokens,
863
+ costUsd: ownCostUsd,
864
+ totalInputTokens,
865
+ totalOutputTokens,
866
+ totalCostUsd,
867
+ turns,
868
+ durationMs: Date.now() - startedAtMs
869
+ };
870
+ }
871
+
872
+ function buildSubRunSnapshot(
873
+ subResult: RunResult,
874
+ durationMs: number
875
+ ): RunMetricsSnapshot {
876
+ const nestedCosts = nestedSubRunCosts(subResult);
877
+ const budgetStopEvent = subResult.trace.events.find((event): event is BudgetStopEvent => event.type === "budget-stop");
878
+ const outcome: RunMetricsSnapshot["outcome"] = budgetStopEvent !== undefined ? "budget-stopped" : "completed";
879
+ const totalInputTokens = subResult.cost.inputTokens;
880
+ const totalOutputTokens = subResult.cost.outputTokens;
881
+ const totalCostUsd = subResult.cost.usd;
882
+ const ownInputTokens =
883
+ totalInputTokens - nestedCosts.reduce((sum, cost) => sum + cost.inputTokens, 0);
884
+ const ownOutputTokens =
885
+ totalOutputTokens - nestedCosts.reduce((sum, cost) => sum + cost.outputTokens, 0);
886
+ const ownCostUsd =
887
+ totalCostUsd - nestedCosts.reduce((sum, cost) => sum + cost.usd, 0);
888
+ const turns = subResult.trace.events.filter((event) => event.type === "agent-turn").length;
889
+
890
+ return {
891
+ outcome,
892
+ inputTokens: ownInputTokens,
893
+ outputTokens: ownOutputTokens,
894
+ costUsd: ownCostUsd,
895
+ totalInputTokens,
896
+ totalOutputTokens,
897
+ totalCostUsd,
898
+ turns,
899
+ durationMs
900
+ };
901
+ }
902
+
903
+ function nestedSubRunCosts(result: RunResult): CostSummary[] {
904
+ return result.trace.events.flatMap((event) => {
905
+ if (event.type === "sub-run-completed") {
906
+ return [event.subResult.cost];
907
+ }
908
+ if (event.type === "sub-run-failed") {
909
+ return [event.partialCost];
910
+ }
911
+ return [];
912
+ });
913
+ }
914
+
915
+ function subtractCost(total: CostSummary, nested: CostSummary): CostSummary {
916
+ return {
917
+ usd: total.usd - nested.usd,
918
+ inputTokens: total.inputTokens - nested.inputTokens,
919
+ outputTokens: total.outputTokens - nested.outputTokens,
920
+ totalTokens: total.totalTokens - nested.totalTokens
921
+ };
922
+ }
923
+
924
+ function handleMetricsEvent(state: MetricsState, event: RunEvent): void {
925
+ const parentRunIds = (event as { readonly parentRunIds?: readonly string[] }).parentRunIds;
926
+ if (parentRunIds !== undefined) {
927
+ return;
928
+ }
929
+
930
+ switch (event.type) {
931
+ case "agent-turn": {
932
+ state.totalCost = event.cost;
933
+ state.turns += 1;
934
+ break;
935
+ }
936
+ case "broadcast":
937
+ case "budget-stop":
938
+ case "final": {
939
+ state.totalCost = event.cost;
940
+ break;
941
+ }
942
+ case "sub-run-started": {
943
+ state.subRunStartTimes.set(event.childRunId, Date.now());
944
+ break;
945
+ }
946
+ case "sub-run-completed": {
947
+ state.totalCost = addCost(state.totalCost, event.subResult.cost);
948
+ state.nestedCost = addCost(state.nestedCost, event.subResult.cost);
949
+ const startMs = state.subRunStartTimes.get(event.childRunId);
950
+ const durationMs = startMs !== undefined ? Date.now() - startMs : 0;
951
+ state.subRunStartTimes.delete(event.childRunId);
952
+ const snapshot = buildSubRunSnapshot(event.subResult, durationMs);
953
+ fireHook(state.metricsHook.onSubRunComplete, snapshot, state.logger);
954
+ break;
955
+ }
956
+ case "sub-run-failed": {
957
+ state.totalCost = addCost(state.totalCost, event.partialCost);
958
+ state.nestedCost = addCost(state.nestedCost, event.partialCost);
959
+ state.subRunStartTimes.delete(event.childRunId);
960
+ break;
961
+ }
962
+ default:
963
+ break;
964
+ }
965
+ }
966
+
967
+ function closeRunMetrics(state: MetricsState, result: RunResult | undefined): void {
968
+ if (result !== undefined) {
969
+ const snapshot = buildRunSnapshot(result, state.startedAtMs);
970
+ fireHook(state.metricsHook.onRunComplete, snapshot, state.logger);
971
+ return;
972
+ }
973
+
974
+ const ownCost = subtractCost(state.totalCost, state.nestedCost);
975
+ const snapshot: RunMetricsSnapshot = {
976
+ outcome: "aborted",
977
+ inputTokens: ownCost.inputTokens,
978
+ outputTokens: ownCost.outputTokens,
979
+ costUsd: ownCost.usd,
980
+ totalInputTokens: state.totalCost.inputTokens,
981
+ totalOutputTokens: state.totalCost.outputTokens,
982
+ totalCostUsd: state.totalCost.usd,
983
+ turns: state.turns,
984
+ durationMs: Date.now() - state.startedAtMs
985
+ };
986
+ fireHook(state.metricsHook.onRunComplete, snapshot, state.logger);
987
+ }
988
+
989
+ function handleTracingEvent(state: TracingState, event: RunEvent): void {
990
+ const parentRunIds = (event as { readonly parentRunIds?: readonly string[] }).parentRunIds;
991
+ if (parentRunIds !== undefined) {
992
+ return;
993
+ }
994
+
995
+ if (state.runId === undefined) {
996
+ state.runId = event.runId;
997
+ state.runSpan.setAttribute("dogpile.run.id", event.runId);
998
+ }
999
+
1000
+ switch (event.type) {
1001
+ case "model-request": {
1002
+ state.pendingModelRequests.set(event.callId, event);
1003
+ state.agentIds.add(event.agentId);
1004
+
1005
+ if (!state.agentTurnSpans.has(event.agentId)) {
1006
+ const turnNumber = (state.agentTurnCounters.get(event.agentId) ?? 0) + 1;
1007
+ state.agentTurnCounters.set(event.agentId, turnNumber);
1008
+ const turnParent = state.subRunSpans.get(event.runId) ?? state.runSpan;
1009
+ const turnSpan = state.tracer.startSpan(DOGPILE_SPAN_NAMES.AGENT_TURN, {
1010
+ parent: turnParent,
1011
+ attributes: {
1012
+ "dogpile.agent.id": event.agentId,
1013
+ "dogpile.agent.role": event.role,
1014
+ "dogpile.turn.number": turnNumber,
1015
+ "dogpile.model.id": event.modelId
1016
+ }
1017
+ });
1018
+ state.agentTurnSpans.set(event.agentId, turnSpan);
1019
+ }
1020
+
1021
+ const callParent =
1022
+ state.agentTurnSpans.get(event.agentId) ??
1023
+ state.subRunSpans.get(event.runId) ??
1024
+ state.runSpan;
1025
+ const callSpan = state.tracer.startSpan(DOGPILE_SPAN_NAMES.MODEL_CALL, {
1026
+ parent: callParent,
1027
+ attributes: {
1028
+ "dogpile.model.id": event.modelId,
1029
+ "dogpile.call.id": event.callId,
1030
+ "dogpile.provider.id": event.providerId
1031
+ }
1032
+ });
1033
+ state.modelCallSpans.set(event.callId, callSpan);
1034
+ break;
1035
+ }
1036
+ case "model-response": {
1037
+ const span = state.modelCallSpans.get(event.callId);
1038
+ if (span) {
1039
+ const inputTokens = event.response.usage?.inputTokens ?? 0;
1040
+ const outputTokens = event.response.usage?.outputTokens ?? 0;
1041
+ const responseCost: CostSummary = {
1042
+ usd: event.response.costUsd ?? 0,
1043
+ inputTokens,
1044
+ outputTokens,
1045
+ totalTokens: event.response.usage?.totalTokens ?? inputTokens + outputTokens
1046
+ };
1047
+ span.setAttribute("dogpile.model.input_tokens", inputTokens);
1048
+ span.setAttribute("dogpile.model.output_tokens", outputTokens);
1049
+ if (event.response.costUsd !== undefined) {
1050
+ span.setAttribute("dogpile.model.cost_usd", event.response.costUsd);
1051
+ }
1052
+ span.setStatus("ok");
1053
+ span.end();
1054
+ state.modelCallSpans.delete(event.callId);
1055
+ const accum = state.turnAccumByAgent.get(event.agentId) ?? {
1056
+ inputTokens: 0,
1057
+ outputTokens: 0,
1058
+ costUsd: 0
1059
+ };
1060
+ accum.inputTokens += inputTokens;
1061
+ accum.outputTokens += outputTokens;
1062
+ accum.costUsd += responseCost.usd;
1063
+ state.turnAccumByAgent.set(event.agentId, accum);
1064
+ state.lastCost = addCost(state.lastCost, responseCost);
1065
+ }
1066
+ state.pendingModelRequests.delete(event.callId);
1067
+ break;
1068
+ }
1069
+ case "agent-turn": {
1070
+ state.agentIds.add(event.agentId);
1071
+ state.turnCount += 1;
1072
+ state.lastCost = event.cost;
1073
+ const turnSpan = state.agentTurnSpans.get(event.agentId);
1074
+ if (turnSpan) {
1075
+ turnSpan.setAttribute("dogpile.agent.role", event.role);
1076
+ const accum = state.turnAccumByAgent.get(event.agentId);
1077
+ turnSpan.setAttribute("dogpile.turn.cost_usd", accum?.costUsd ?? 0);
1078
+ turnSpan.setAttribute("dogpile.turn.input_tokens", accum?.inputTokens ?? 0);
1079
+ turnSpan.setAttribute("dogpile.turn.output_tokens", accum?.outputTokens ?? 0);
1080
+ turnSpan.setStatus("ok");
1081
+ turnSpan.end();
1082
+ state.agentTurnSpans.delete(event.agentId);
1083
+ }
1084
+ state.turnAccumByAgent.delete(event.agentId);
1085
+ break;
1086
+ }
1087
+ case "broadcast":
1088
+ case "budget-stop":
1089
+ case "final": {
1090
+ state.lastCost = event.cost;
1091
+ break;
1092
+ }
1093
+ case "sub-run-started": {
1094
+ const span = state.tracer.startSpan(DOGPILE_SPAN_NAMES.SUB_RUN, {
1095
+ parent: state.runSpan,
1096
+ attributes: {
1097
+ "dogpile.sub_run.child_run_id": event.childRunId,
1098
+ "dogpile.sub_run.parent_run_id": event.parentRunId,
1099
+ "dogpile.sub_run.depth": event.depth
1100
+ }
1101
+ });
1102
+ state.subRunSpans.set(event.childRunId, span);
1103
+ break;
1104
+ }
1105
+ case "sub-run-completed": {
1106
+ const span = state.subRunSpans.get(event.childRunId);
1107
+ if (span) {
1108
+ span.setStatus("ok");
1109
+ span.end();
1110
+ state.subRunSpans.delete(event.childRunId);
1111
+ }
1112
+ break;
1113
+ }
1114
+ case "sub-run-failed": {
1115
+ const span = state.subRunSpans.get(event.childRunId);
1116
+ if (span) {
1117
+ span.setStatus("error", event.error.message);
1118
+ span.end();
1119
+ state.subRunSpans.delete(event.childRunId);
1120
+ }
1121
+ break;
1122
+ }
1123
+ default:
1124
+ break;
1125
+ }
1126
+ }
1127
+
1128
+ function closeRunTracing(state: TracingState, result: RunResult | undefined, error?: unknown): void {
1129
+ if (error !== undefined) {
1130
+ if (state.runId !== undefined) {
1131
+ state.runSpan.setAttribute("dogpile.run.id", state.runId);
1132
+ }
1133
+ state.runSpan.setAttribute("dogpile.run.agent_count", state.agentIds.size);
1134
+ state.runSpan.setAttribute("dogpile.run.turn_count", state.turnCount);
1135
+ state.runSpan.setAttribute("dogpile.run.cost_usd", state.lastCost.usd);
1136
+ state.runSpan.setAttribute("dogpile.run.input_tokens", state.lastCost.inputTokens);
1137
+ state.runSpan.setAttribute("dogpile.run.output_tokens", state.lastCost.outputTokens);
1138
+ state.runSpan.setAttribute("dogpile.run.outcome", "aborted");
1139
+ state.runSpan.setStatus("error", error instanceof Error ? error.message : String(error));
1140
+ closeOpenTracingSpans(state);
1141
+ state.runSpan.end();
1142
+ return;
1143
+ }
1144
+
1145
+ if (result === undefined) {
1146
+ closeOpenTracingSpans(state);
1147
+ state.runSpan.end();
1148
+ return;
1149
+ }
1150
+
1151
+ const budgetStopEvent = result.trace.events.find((event): event is BudgetStopEvent => event.type === "budget-stop");
1152
+ const terminationReason = budgetStopEvent?.reason;
1153
+ const outcome = terminationReason !== undefined ? "budget-stopped" : "completed";
1154
+ state.runSpan.setAttribute("dogpile.run.id", result.trace.runId);
1155
+ state.runSpan.setAttribute("dogpile.run.agent_count", result.trace.agentsUsed.length);
1156
+ state.runSpan.setAttribute("dogpile.run.turn_count", result.trace.events.filter((event) => event.type === "agent-turn").length);
1157
+ state.runSpan.setAttribute("dogpile.run.cost_usd", result.cost.usd);
1158
+ state.runSpan.setAttribute("dogpile.run.input_tokens", result.cost.inputTokens);
1159
+ state.runSpan.setAttribute("dogpile.run.output_tokens", result.cost.outputTokens);
1160
+ state.runSpan.setAttribute("dogpile.run.outcome", outcome);
1161
+ if (terminationReason !== undefined) {
1162
+ state.runSpan.setAttribute("dogpile.run.termination_reason", terminationReason);
1163
+ }
1164
+ state.runSpan.setStatus("ok");
1165
+ closeOpenTracingSpans(state);
1166
+ state.runSpan.end();
1167
+ }
1168
+
1169
+ function closeOpenTracingSpans(state: TracingState): void {
1170
+ for (const span of state.modelCallSpans.values()) {
1171
+ span.end();
1172
+ }
1173
+ state.modelCallSpans.clear();
1174
+ for (const span of state.agentTurnSpans.values()) {
1175
+ span.end();
1176
+ }
1177
+ state.agentTurnSpans.clear();
1178
+ for (const span of state.subRunSpans.values()) {
1179
+ span.end();
1180
+ }
1181
+ state.subRunSpans.clear();
1182
+ }
1183
+
689
1184
  async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Promise<RunResult> {
690
1185
  const failureInstancesByChildRunId = new Map<string, DogpileError>();
691
1186
  const abortLifecycle = createNonStreamingAbortLifecycle({
@@ -728,7 +1223,8 @@ async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Pr
728
1223
  events
729
1224
  }),
730
1225
  eventLog: createRunEventLog(trace.runId, trace.protocol, events),
731
- trace
1226
+ trace,
1227
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
732
1228
  };
733
1229
  const terminalThrow = resolveRuntimeTerminalThrow(runResult.trace, failureInstancesByChildRunId);
734
1230
  if (terminalThrow) {
@@ -781,7 +1277,61 @@ function finalEventWithEvaluation(event: FinalEvent, evaluation: RunEvaluation):
781
1277
  };
782
1278
  }
783
1279
 
784
- function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
1280
+ async function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
1281
+ const tracing = openRunTracing({
1282
+ ...(options.tracer ? { tracer: options.tracer } : {}),
1283
+ ...(options.parentSpan ? { parentSpan: options.parentSpan } : {}),
1284
+ intent: options.intent,
1285
+ protocolKind: options.protocol.kind,
1286
+ tier: options.tier
1287
+ });
1288
+ const metrics = openRunMetrics({
1289
+ ...(options.metricsHook ? { metricsHook: options.metricsHook } : {}),
1290
+ ...(options.logger ? { logger: options.logger } : {})
1291
+ });
1292
+ const emitForProtocol =
1293
+ tracing || metrics || options.emit
1294
+ ? (event: RunEvent): void => {
1295
+ if (tracing) {
1296
+ handleTracingEvent(tracing, event);
1297
+ }
1298
+ if (metrics) {
1299
+ handleMetricsEvent(metrics, event);
1300
+ }
1301
+ options.emit?.(event);
1302
+ }
1303
+ : undefined;
1304
+ const protocolOptions = tracing
1305
+ ? {
1306
+ ...options,
1307
+ subRunSpansByChildId: tracing.subRunSpans
1308
+ }
1309
+ : options;
1310
+
1311
+ try {
1312
+ const result = await runProtocolInner(protocolOptions, emitForProtocol);
1313
+ if (tracing) {
1314
+ closeRunTracing(tracing, result);
1315
+ }
1316
+ if (metrics && (options.currentDepth === 0 || options.currentDepth === undefined)) {
1317
+ closeRunMetrics(metrics, result);
1318
+ }
1319
+ return result;
1320
+ } catch (error) {
1321
+ if (tracing) {
1322
+ closeRunTracing(tracing, undefined, error);
1323
+ }
1324
+ if (metrics && (options.currentDepth === 0 || options.currentDepth === undefined)) {
1325
+ closeRunMetrics(metrics, undefined);
1326
+ }
1327
+ throw error;
1328
+ }
1329
+ }
1330
+
1331
+ function runProtocolInner(
1332
+ options: RunProtocolOptions,
1333
+ emitForProtocol?: (event: RunEvent) => void
1334
+ ): Promise<RunResult> {
785
1335
  switch (options.protocol.kind) {
786
1336
  case "sequential":
787
1337
  return runSequential({
@@ -797,7 +1347,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
797
1347
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
798
1348
  ...(options.terminate ? { terminate: options.terminate } : {}),
799
1349
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
800
- ...(options.emit ? { emit: options.emit } : {})
1350
+ ...(emitForProtocol ? { emit: emitForProtocol } : {})
801
1351
  });
802
1352
  case "broadcast":
803
1353
  return runBroadcast({
@@ -813,7 +1363,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
813
1363
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
814
1364
  ...(options.terminate ? { terminate: options.terminate } : {}),
815
1365
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
816
- ...(options.emit ? { emit: options.emit } : {})
1366
+ ...(emitForProtocol ? { emit: emitForProtocol } : {})
817
1367
  });
818
1368
  case "coordinator":
819
1369
  return runCoordinator({
@@ -829,7 +1379,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
829
1379
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
830
1380
  ...(options.terminate ? { terminate: options.terminate } : {}),
831
1381
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
832
- ...(options.emit ? { emit: options.emit } : {}),
1382
+ ...(emitForProtocol ? { emit: emitForProtocol } : {}),
833
1383
  ...(options.streamEvents !== undefined ? { streamEvents: options.streamEvents } : {}),
834
1384
  currentDepth: options.currentDepth ?? 0,
835
1385
  effectiveMaxDepth: options.effectiveMaxDepth ?? Infinity,
@@ -843,11 +1393,17 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
843
1393
  ...(options.failureInstancesByChildRunId !== undefined
844
1394
  ? { failureInstancesByChildRunId: options.failureInstancesByChildRunId }
845
1395
  : {}),
846
- runProtocol: (childInput) =>
847
- runProtocol({
848
- ...childInput,
849
- protocol: normalizeProtocol(childInput.protocol)
850
- })
1396
+ runProtocol: (childInput) => {
1397
+ const { runId: childRunId, ...childProtocolInput } = childInput;
1398
+ const childParent = options.subRunSpansByChildId?.get(childRunId) ?? options.parentSpan;
1399
+ return runProtocol({
1400
+ ...childProtocolInput,
1401
+ protocol: normalizeProtocol(childProtocolInput.protocol),
1402
+ ...(options.tracer ? { tracer: options.tracer } : {}),
1403
+ ...(childParent ? { parentSpan: childParent } : {}),
1404
+ ...(options.logger ? { logger: options.logger } : {})
1405
+ });
1406
+ }
851
1407
  });
852
1408
  case "shared":
853
1409
  return runShared({
@@ -863,7 +1419,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
863
1419
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
864
1420
  ...(options.terminate ? { terminate: options.terminate } : {}),
865
1421
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
866
- ...(options.emit ? { emit: options.emit } : {})
1422
+ ...(emitForProtocol ? { emit: emitForProtocol } : {})
867
1423
  });
868
1424
  }
869
1425
  }
@@ -916,7 +1472,14 @@ export function stream(options: DogpileOptions): StreamHandle {
916
1472
  * the ergonomic {@link RunResult} wrapper from the JSON-serializable
917
1473
  * {@link Trace} returned by a previous `run()`, `stream()`, or
918
1474
  * `Dogpile.pile()` call.
1475
+ *
1476
+ * Tracing and metrics: replay is intentionally tracing-free and metrics-free.
1477
+ * Even when an engine instance has been configured with a `tracer` or
1478
+ * `metricsHook` on its `EngineOptions`, calling this function emits no spans
1479
+ * or callbacks — replaying historical events with current timestamps would
1480
+ * confuse observability backends. See `docs/developer-usage.md`.
919
1481
  */
1482
+ // Tracing/metrics-free: replay never uses EngineOptions tracer or metricsHook.
920
1483
  export function replay(trace: Trace): RunResult {
921
1484
  const cost = trace.finalOutput.cost;
922
1485
  const lastEvent = trace.events.at(-1);
@@ -931,7 +1494,11 @@ export function replay(trace: Trace): RunResult {
931
1494
  }
932
1495
  const baseResult = {
933
1496
  output: trace.finalOutput.output,
934
- eventLog: createRunEventLog(trace.runId, trace.protocol, trace.events),
1497
+ eventLog: createRunEventLog(
1498
+ trace.runId,
1499
+ trace.protocol,
1500
+ synthesizeProviderEvents(trace, trace.providerCalls)
1501
+ ),
935
1502
  trace,
936
1503
  transcript: trace.transcript,
937
1504
  usage: createRunUsage(cost),
@@ -944,7 +1511,8 @@ export function replay(trace: Trace): RunResult {
944
1511
  events: trace.events
945
1512
  }),
946
1513
  accounting,
947
- cost
1514
+ cost,
1515
+ health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
948
1516
  };
949
1517
 
950
1518
  if (lastEvent?.type !== "final") {
@@ -958,6 +1526,60 @@ export function replay(trace: Trace): RunResult {
958
1526
  };
959
1527
  }
960
1528
 
1529
+ function synthesizeProviderEvents(
1530
+ trace: Trace,
1531
+ providerCalls: readonly ReplayTraceProviderCall[]
1532
+ ): readonly RunEvent[] {
1533
+ const hasLiveProvenance = trace.events.some(
1534
+ (event) => event.type === "model-request" || event.type === "model-response"
1535
+ );
1536
+ if (hasLiveProvenance) {
1537
+ return trace.events;
1538
+ }
1539
+
1540
+ const baseEvents = trace.events.filter(
1541
+ (event) => event.type !== "model-request" && event.type !== "model-response"
1542
+ );
1543
+ const result: RunEvent[] = [];
1544
+ let turnCount = 0;
1545
+
1546
+ for (const event of baseEvents) {
1547
+ if (event.type === "agent-turn") {
1548
+ const call = providerCalls[turnCount];
1549
+ if (call !== undefined) {
1550
+ const modelId = typeof call.modelId === "string" && call.modelId.length > 0 ? call.modelId : call.providerId;
1551
+ result.push({
1552
+ type: "model-request",
1553
+ runId: trace.runId,
1554
+ callId: call.callId,
1555
+ providerId: call.providerId,
1556
+ modelId,
1557
+ startedAt: call.startedAt,
1558
+ agentId: call.agentId,
1559
+ role: call.role,
1560
+ request: call.request
1561
+ });
1562
+ result.push({
1563
+ type: "model-response",
1564
+ runId: trace.runId,
1565
+ callId: call.callId,
1566
+ providerId: call.providerId,
1567
+ modelId,
1568
+ startedAt: call.startedAt,
1569
+ completedAt: call.completedAt,
1570
+ agentId: call.agentId,
1571
+ role: call.role,
1572
+ response: call.response
1573
+ });
1574
+ }
1575
+ turnCount += 1;
1576
+ }
1577
+ result.push(event);
1578
+ }
1579
+
1580
+ return result;
1581
+ }
1582
+
961
1583
  function resolveRuntimeTerminalThrow(
962
1584
  trace: Trace,
963
1585
  failureInstancesByChildRunId: ReadonlyMap<string, DogpileError>
@@ -1060,11 +1682,19 @@ function dogpileErrorFromSerializedPayload(input: {
1060
1682
  * Replay a saved completed trace as a stream without invoking a model provider.
1061
1683
  *
1062
1684
  * @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.
1685
+ * This is the streaming counterpart to {@link replay}. It yields the same
1686
+ * event sequence exposed by the replayed result event log, including legacy
1687
+ * provenance synthesis when a saved trace predates model request/response
1688
+ * events. Since all data comes from the trace, replay remains storage-free and
1689
+ * provider-free.
1690
+ *
1691
+ * Tracing and metrics: replayStream is intentionally tracing-free and
1692
+ * metrics-free. Even when an engine instance has been configured with a
1693
+ * `tracer` or `metricsHook` on its `EngineOptions`, calling this function
1694
+ * emits no spans or callbacks — replaying historical events with current
1695
+ * timestamps would confuse observability backends. See `docs/developer-usage.md`.
1067
1696
  */
1697
+ // Tracing/metrics-free: replayStream never uses EngineOptions tracer or metricsHook.
1068
1698
  export function replayStream(trace: Trace): StreamHandle {
1069
1699
  const result = Promise.resolve(replay(trace));
1070
1700
  const replayEvents = replayStreamEvents(trace);
@@ -1109,7 +1739,7 @@ export function replayStream(trace: Trace): StreamHandle {
1109
1739
  function replayStreamEvents(trace: Trace, parentRunIds: readonly string[] = []): StreamEvent[] {
1110
1740
  const events: StreamEvent[] = [];
1111
1741
 
1112
- for (const event of trace.events) {
1742
+ for (const event of synthesizeProviderEvents(trace, trace.providerCalls)) {
1113
1743
  if (event.type === "sub-run-completed") {
1114
1744
  events.push(...replayStreamEvents(event.subResult.trace, [...parentRunIds, trace.runId]));
1115
1745
  }