@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
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Health diagnostics computation for completed run traces.
3
+ *
4
+ * @module
5
+ */
6
+ import type { HealthAnomaly, RunHealthSummary, Trace } from "../types.js";
7
+ import type { TurnEvent } from "../types/events.js";
8
+
9
+ // Re-export types so callers who import from this subpath get them directly.
10
+ export type { HealthAnomaly, RunHealthSummary } from "../types.js";
11
+
12
+ /**
13
+ * Thresholds for health anomaly detection.
14
+ *
15
+ * Both fields are optional. When absent, the corresponding threshold-gated
16
+ * anomaly is suppressed entirely. Threshold-free anomalies (`empty-contribution`)
17
+ * always fire when qualifying events are present regardless of this config.
18
+ *
19
+ * Note: `provider-error-recovered` is in the AnomalyCode union but is never
20
+ * emitted by computeHealth in Phase 7 - no trace signal exists without an
21
+ * event-shape change. See STATE.md: "Phase 6 is the only event-shape change."
22
+ */
23
+ export interface HealthThresholds {
24
+ /**
25
+ * Per-agent turn count threshold. If an agent produces more than this many
26
+ * agent-turn events, a "runaway-turns" anomaly is emitted with severity "error".
27
+ * The threshold value in the anomaly record equals this number.
28
+ */
29
+ readonly runawayTurns?: number;
30
+ /**
31
+ * Budget utilization percentage threshold (0-100). If budget utilization
32
+ * (finalCost / maxUsd * 100) >= this value, a "budget-near-miss" anomaly is
33
+ * emitted with severity "warning". Suppressed when no USD cap is configured.
34
+ */
35
+ readonly budgetNearMissPct?: number;
36
+ }
37
+
38
+ /**
39
+ * Default health thresholds used for `result.health` auto-computation.
40
+ *
41
+ * Both threshold-gated anomalies (runaway-turns, budget-near-miss) are suppressed
42
+ * by default. Only threshold-free anomalies (empty-contribution) can fire on the
43
+ * auto-compute path.
44
+ */
45
+ export const DEFAULT_HEALTH_THRESHOLDS: HealthThresholds = Object.freeze({});
46
+
47
+ /**
48
+ * Compute a health summary from a completed run trace.
49
+ *
50
+ * Pure function - no side effects, no I/O, no storage access. Deterministic:
51
+ * given the same trace and thresholds, always produces the same result.
52
+ *
53
+ * @param trace - Completed run trace (from RunResult.trace or a stored trace).
54
+ * @param thresholds - Optional threshold overrides. Defaults to DEFAULT_HEALTH_THRESHOLDS.
55
+ */
56
+ export function computeHealth(
57
+ trace: Trace,
58
+ thresholds: HealthThresholds = DEFAULT_HEALTH_THRESHOLDS
59
+ ): RunHealthSummary {
60
+ assertFiniteNonNegativeThreshold(thresholds.runawayTurns, "runawayTurns");
61
+ assertBudgetNearMissThreshold(thresholds.budgetNearMissPct);
62
+
63
+ const turnEvents = trace.events.filter((event): event is TurnEvent => event.type === "agent-turn");
64
+ const agentIds = new Set(turnEvents.map((event) => event.agentId));
65
+ const totalTurns = turnEvents.length;
66
+ const agentCount = agentIds.size;
67
+
68
+ const maxUsd = trace.budget.caps?.maxUsd;
69
+ const finalCost = trace.finalOutput.cost.usd;
70
+ const budgetUtilizationPct: number | null =
71
+ maxUsd !== undefined ? (maxUsd === 0 ? (finalCost === 0 ? 0 : 100) : (finalCost / maxUsd) * 100) : null;
72
+
73
+ const anomalies: HealthAnomaly[] = [];
74
+
75
+ if (thresholds.runawayTurns !== undefined) {
76
+ for (const agentId of agentIds) {
77
+ const count = turnEvents.filter((event) => event.agentId === agentId).length;
78
+ if (count > thresholds.runawayTurns) {
79
+ anomalies.push({
80
+ code: "runaway-turns",
81
+ severity: "error",
82
+ value: count,
83
+ threshold: thresholds.runawayTurns,
84
+ agentId
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ if (thresholds.budgetNearMissPct !== undefined && budgetUtilizationPct !== null) {
91
+ if (budgetUtilizationPct >= thresholds.budgetNearMissPct) {
92
+ anomalies.push({
93
+ code: "budget-near-miss",
94
+ severity: "warning",
95
+ value: budgetUtilizationPct,
96
+ threshold: thresholds.budgetNearMissPct
97
+ });
98
+ }
99
+ }
100
+
101
+ for (const event of turnEvents) {
102
+ if (event.output.trim() === "") {
103
+ anomalies.push({
104
+ code: "empty-contribution",
105
+ severity: "error",
106
+ value: 0,
107
+ threshold: 0,
108
+ agentId: event.agentId
109
+ });
110
+ }
111
+ }
112
+
113
+ // provider-error-recovered is deferred: no trace signal exists in Phase 7.
114
+ return {
115
+ anomalies,
116
+ stats: {
117
+ totalTurns,
118
+ agentCount,
119
+ budgetUtilizationPct
120
+ }
121
+ };
122
+ }
123
+
124
+ function assertFiniteNonNegativeThreshold(value: number | undefined, name: string): void {
125
+ if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
126
+ throw new RangeError(`${name} must be a finite non-negative number`);
127
+ }
128
+ }
129
+
130
+ function assertBudgetNearMissThreshold(value: number | undefined): void {
131
+ assertFiniteNonNegativeThreshold(value, "budgetNearMissPct");
132
+
133
+ if (value !== undefined && value > 100) {
134
+ throw new RangeError("budgetNearMissPct must be between 0 and 100");
135
+ }
136
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Typed event query function for filtering completed trace events.
3
+ *
4
+ * @module
5
+ */
6
+ import type {
7
+ BroadcastEvent,
8
+ BudgetStopEvent,
9
+ FinalEvent,
10
+ ModelOutputChunkEvent,
11
+ ModelRequestEvent,
12
+ ModelResponseEvent,
13
+ RoleAssignmentEvent,
14
+ RunEvent,
15
+ SubRunBudgetClampedEvent,
16
+ SubRunCompletedEvent,
17
+ SubRunConcurrencyClampedEvent,
18
+ SubRunFailedEvent,
19
+ SubRunParentAbortedEvent,
20
+ SubRunQueuedEvent,
21
+ SubRunStartedEvent,
22
+ ToolCallEvent,
23
+ ToolResultEvent,
24
+ TurnEvent
25
+ } from "../types.js";
26
+
27
+ /**
28
+ * Filter criteria for querying a completed trace event log.
29
+ *
30
+ * All fields are optional. AND semantics: all present fields must match.
31
+ * An empty filter object returns all events. An unmatched filter returns [].
32
+ *
33
+ * `costRange` matches only events with a `cost.usd` field: TurnEvent and
34
+ * BroadcastEvent. Events without a cost field are excluded from results when
35
+ * `costRange` is set (not returned as unmatched - silently excluded).
36
+ *
37
+ * `turnRange` uses the global 1-based position of agent-turn events across
38
+ * all agents. Position 1 is the first TurnEvent in the event array regardless
39
+ * of which agent produced it. BroadcastEvent.round is a separate concept and
40
+ * is not matched by turnRange.
41
+ */
42
+ export interface EventQueryFilter {
43
+ /** Filter to events with this exact type discriminant. */
44
+ readonly type?: RunEvent["type"];
45
+ /** Filter to events where agentId === this value. Events without agentId are excluded. */
46
+ readonly agentId?: string;
47
+ /**
48
+ * Filter to agent-turn events at the specified global 1-based position range.
49
+ * Only TurnEvents are included in results when this filter is set.
50
+ */
51
+ readonly turnRange?: {
52
+ readonly min?: number;
53
+ readonly max?: number;
54
+ };
55
+ /**
56
+ * Filter to events where cost.usd is within [min, max].
57
+ * Only TurnEvent and BroadcastEvent have cost.usd - all other events are excluded.
58
+ */
59
+ readonly costRange?: {
60
+ readonly min?: number;
61
+ readonly max?: number;
62
+ };
63
+ }
64
+
65
+ // One overload per RunEvent discriminant (D-03: hand-written overloads, IDE-reliable)
66
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "role-assignment" }): RoleAssignmentEvent[];
67
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "model-request" }): ModelRequestEvent[];
68
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "model-response" }): ModelResponseEvent[];
69
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "model-output-chunk" }): ModelOutputChunkEvent[];
70
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "tool-call" }): ToolCallEvent[];
71
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "tool-result" }): ToolResultEvent[];
72
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "agent-turn" }): TurnEvent[];
73
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "broadcast" }): BroadcastEvent[];
74
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "sub-run-started" }): SubRunStartedEvent[];
75
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "sub-run-completed" }): SubRunCompletedEvent[];
76
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "sub-run-failed" }): SubRunFailedEvent[];
77
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "sub-run-parent-aborted" }): SubRunParentAbortedEvent[];
78
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "sub-run-budget-clamped" }): SubRunBudgetClampedEvent[];
79
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "sub-run-queued" }): SubRunQueuedEvent[];
80
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "sub-run-concurrency-clamped" }): SubRunConcurrencyClampedEvent[];
81
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "budget-stop" }): BudgetStopEvent[];
82
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter & { type: "final" }): FinalEvent[];
83
+ // Fallback overload: no type constraint -> returns full RunEvent[]
84
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter): RunEvent[];
85
+ // Implementation signature (not visible to callers):
86
+ export function queryEvents(events: readonly RunEvent[], filter: EventQueryFilter): RunEvent[] {
87
+ let result: RunEvent[] = filter.type !== undefined
88
+ ? events.filter((event) => event.type === filter.type)
89
+ : [...events];
90
+
91
+ if (filter.agentId !== undefined) {
92
+ const { agentId } = filter;
93
+ result = result.filter((event) => "agentId" in event && (event as { agentId?: string }).agentId === agentId);
94
+ }
95
+
96
+ if (filter.turnRange !== undefined) {
97
+ const { min, max } = filter.turnRange;
98
+ const agentTurnEvents = events.filter((event): event is TurnEvent => event.type === "agent-turn");
99
+ const inRangeSet = new Set<RunEvent>(
100
+ agentTurnEvents.filter((_, index) => {
101
+ const position = index + 1;
102
+ return (min === undefined || position >= min) && (max === undefined || position <= max);
103
+ })
104
+ );
105
+
106
+ result = result.filter((event) => event.type === "agent-turn" && inRangeSet.has(event));
107
+ }
108
+
109
+ if (filter.costRange !== undefined) {
110
+ const { min, max } = filter.costRange;
111
+ result = result.filter((event) => {
112
+ if (event.type !== "agent-turn" && event.type !== "broadcast") {
113
+ return false;
114
+ }
115
+
116
+ const usd = event.cost.usd;
117
+ return (min === undefined || usd >= min) && (max === undefined || usd <= max);
118
+ });
119
+ }
120
+
121
+ return result;
122
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Metrics hook interface for run-completion counters (Phase 10 / METR-01..METR-02).
3
+ *
4
+ * The SDK does not import any metrics backend. Callers provide an object
5
+ * satisfying `MetricsHook` to receive named counters at run and sub-run
6
+ * completion. When absent, zero overhead — no allocations, no branch cost.
7
+ *
8
+ * `replay()` and `replayStream()` ignore `metricsHook` on engine options —
9
+ * counters for historical replays would be misleading.
10
+ */
11
+
12
+ export interface RunMetricsSnapshot {
13
+ readonly outcome: "completed" | "budget-stopped" | "aborted";
14
+ /** Direct tokens for this run, excluding nested sub-runs. */
15
+ readonly inputTokens: number;
16
+ /** Direct tokens for this run, excluding nested sub-runs. */
17
+ readonly outputTokens: number;
18
+ /** Direct cost for this run, excluding nested sub-runs. */
19
+ readonly costUsd: number;
20
+ /** Total tokens including the full sub-run subtree (already rolled up). */
21
+ readonly totalInputTokens: number;
22
+ /** Total tokens including the full sub-run subtree. */
23
+ readonly totalOutputTokens: number;
24
+ /** Total cost including the full sub-run subtree. */
25
+ readonly totalCostUsd: number;
26
+ /** Count of agent-turn events directly in this run (own-only, not nested sub-runs). */
27
+ readonly turns: number;
28
+ /** Wall-clock duration in milliseconds from run start to terminal state. */
29
+ readonly durationMs: number;
30
+ }
31
+
32
+ export interface MetricsHook {
33
+ /**
34
+ * Called once at every terminal state of the top-level run (completed,
35
+ * budget-stopped, aborted). When the hook is async, the SDK attaches
36
+ * `.catch` and does NOT await — hook latency never delays run completion.
37
+ */
38
+ readonly onRunComplete?: (snapshot: RunMetricsSnapshot) => void | Promise<void>;
39
+ /**
40
+ * Called once for each coordinator-dispatched child run that completes.
41
+ * Fires from the parent run's emit closure on the `sub-run-completed` event.
42
+ * Does NOT fire for failed sub-runs (`sub-run-failed`).
43
+ */
44
+ readonly onSubRunComplete?: (snapshot: RunMetricsSnapshot) => void | Promise<void>;
45
+ }
@@ -24,18 +24,33 @@ type ModelUsage = NonNullable<ModelResponse["usage"]>;
24
24
 
25
25
  export async function generateModelTurn(options: GenerateModelTurnOptions): Promise<ModelResponse> {
26
26
  const startedAt = new Date().toISOString();
27
+ const modelId = options.model.modelId ?? options.model.id;
28
+ const traceRequest = requestForTrace(options.request);
27
29
  let response: ModelResponse;
28
30
 
29
31
  throwIfAborted(options.request.signal, options.model.id);
30
32
 
33
+ options.emit({
34
+ type: "model-request",
35
+ runId: options.runId,
36
+ callId: options.callId,
37
+ providerId: options.model.id,
38
+ modelId,
39
+ startedAt,
40
+ agentId: options.agent.id,
41
+ role: options.agent.role,
42
+ request: traceRequest
43
+ });
44
+
31
45
  if (!options.model.stream) {
32
46
  response = await options.model.generate(options.request);
33
47
  throwIfAborted(options.request.signal, options.model.id);
34
- recordProviderCall(response, startedAt, options);
48
+ recordProviderCall(response, startedAt, modelId, traceRequest, options);
35
49
  return response;
36
50
  }
37
51
 
38
- let text = "";
52
+ const chunks: string[] = [];
53
+ let outputLength = 0;
39
54
  let chunkIndex = 0;
40
55
  let usage: ModelUsage | undefined;
41
56
  let costUsd: number | undefined;
@@ -45,7 +60,8 @@ export async function generateModelTurn(options: GenerateModelTurnOptions): Prom
45
60
 
46
61
  for await (const chunk of options.model.stream(options.request)) {
47
62
  throwIfAborted(options.request.signal, options.model.id);
48
- text += chunk.text;
63
+ chunks.push(chunk.text);
64
+ outputLength += chunk.text.length;
49
65
 
50
66
  options.emit({
51
67
  type: "model-output-chunk",
@@ -56,7 +72,7 @@ export async function generateModelTurn(options: GenerateModelTurnOptions): Prom
56
72
  input: options.input,
57
73
  chunkIndex,
58
74
  text: chunk.text,
59
- output: text
75
+ outputLength
60
76
  });
61
77
  chunkIndex += 1;
62
78
 
@@ -77,6 +93,7 @@ export async function generateModelTurn(options: GenerateModelTurnOptions): Prom
77
93
  }
78
94
  }
79
95
 
96
+ const text = chunks.join("");
80
97
  response = {
81
98
  text,
82
99
  ...(finishReason !== undefined ? { finishReason } : {}),
@@ -86,32 +103,50 @@ export async function generateModelTurn(options: GenerateModelTurnOptions): Prom
86
103
  ...(metadata !== undefined ? { metadata } : {})
87
104
  };
88
105
  throwIfAborted(options.request.signal, options.model.id);
89
- recordProviderCall(response, startedAt, options);
106
+ recordProviderCall(response, startedAt, modelId, traceRequest, options);
90
107
  return response;
91
108
  }
92
109
 
93
110
  function recordProviderCall(
94
111
  response: ModelResponse,
95
112
  startedAt: string,
113
+ modelId: string,
114
+ request: ModelRequest,
96
115
  options: GenerateModelTurnOptions
97
116
  ): void {
117
+ const completedAt = new Date().toISOString();
118
+
119
+ options.emit({
120
+ type: "model-response",
121
+ runId: options.runId,
122
+ callId: options.callId,
123
+ providerId: options.model.id,
124
+ modelId,
125
+ startedAt,
126
+ completedAt,
127
+ agentId: options.agent.id,
128
+ role: options.agent.role,
129
+ response
130
+ });
131
+
98
132
  options.onProviderCall?.({
99
133
  kind: "replay-trace-provider-call",
100
134
  callId: options.callId,
101
135
  providerId: options.model.id,
136
+ modelId,
102
137
  startedAt,
103
- completedAt: new Date().toISOString(),
138
+ completedAt,
104
139
  agentId: options.agent.id,
105
140
  role: options.agent.role,
106
- request: requestForTrace(options.request),
141
+ request,
107
142
  response
108
143
  });
109
144
  }
110
145
 
111
146
  function requestForTrace(request: ModelRequest): ModelRequest {
112
147
  return {
113
- messages: request.messages,
148
+ messages: request.messages.map((message) => ({ ...message })),
114
149
  temperature: request.temperature,
115
- metadata: request.metadata
150
+ metadata: JSON.parse(JSON.stringify(request.metadata)) as ModelRequest["metadata"]
116
151
  };
117
152
  }
@@ -0,0 +1,43 @@
1
+ import type { ModelRequestEvent, ModelResponseEvent } from "../types.js";
2
+
3
+ /**
4
+ * Normalized provenance fields from a completed model-response event.
5
+ * All five fields are present and JSON-serializable.
6
+ */
7
+ export interface ProvenanceRecord {
8
+ readonly modelId: string;
9
+ readonly providerId: string;
10
+ readonly callId: string;
11
+ readonly startedAt: string;
12
+ readonly completedAt: string;
13
+ }
14
+
15
+ /**
16
+ * Normalized provenance fields from a model-request event.
17
+ * completedAt is absent because the call has not completed at this point.
18
+ */
19
+ export interface PartialProvenanceRecord {
20
+ readonly modelId: string;
21
+ readonly providerId: string;
22
+ readonly callId: string;
23
+ readonly startedAt: string;
24
+ }
25
+
26
+ export function getProvenance(event: ModelResponseEvent): ProvenanceRecord;
27
+ export function getProvenance(event: ModelRequestEvent): PartialProvenanceRecord;
28
+ export function getProvenance(
29
+ event: ModelRequestEvent | ModelResponseEvent
30
+ ): ProvenanceRecord | PartialProvenanceRecord {
31
+ const base: PartialProvenanceRecord = {
32
+ modelId: event.modelId,
33
+ providerId: event.providerId,
34
+ callId: event.callId,
35
+ startedAt: event.startedAt
36
+ };
37
+
38
+ if (event.type === "model-response") {
39
+ return { ...base, completedAt: event.completedAt };
40
+ }
41
+
42
+ return base;
43
+ }