@smithers-orchestrator/engine 0.19.0 → 0.20.1

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.
@@ -0,0 +1,716 @@
1
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { nowMs } from "@smithers-orchestrator/scheduler/nowMs";
4
+ import { normalizeTokenUsage } from "@smithers-orchestrator/agents/BaseCliAgent";
5
+ import { detectAgentFamily } from "@smithers-orchestrator/observability/detectAgentFamily";
6
+ import { detectCaptureMode } from "@smithers-orchestrator/observability/detectCaptureMode";
7
+ import { resolveAgentTraceCapabilities } from "@smithers-orchestrator/observability/resolveAgentTraceCapabilities";
8
+ import { unsupportedKindsForCapabilities } from "@smithers-orchestrator/observability/unsupportedKindsForCapabilities";
9
+ import { kindPhase } from "@smithers-orchestrator/observability/kindPhase";
10
+ import { normalizeStructuredEvent } from "@smithers-orchestrator/observability/normalizeStructuredEvent";
11
+ import { extractProviderSessionCorrelation } from "@smithers-orchestrator/observability/_traceEventNormalizers";
12
+ import { redactValue } from "@smithers-orchestrator/observability/_traceRedaction";
13
+ import { canonicalTraceEventToOtelLogRecord } from "@smithers-orchestrator/observability/canonicalTraceEventToOtelLogRecord";
14
+ import { agentSessionEventToOtelLogRecord } from "@smithers-orchestrator/observability/agentSessionEventToOtelLogRecord";
15
+ import { emitOtelLogRecord } from "@smithers-orchestrator/observability/emitOtelLogRecord";
16
+ import { shouldExportTraceEventToOtel } from "@smithers-orchestrator/observability/_otelLogBuilders";
17
+ import {
18
+ resolveClaudeSessionFile,
19
+ resolveCodexSessionFile,
20
+ resolvePiSessionFile,
21
+ } from "@smithers-orchestrator/observability/_sessionFileResolvers";
22
+
23
+ /**
24
+ * @typedef {import("@smithers-orchestrator/observability/SmithersEvent").SmithersEvent} SmithersEvent
25
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").AgentCaptureMode} AgentCaptureMode
26
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").AgentFamily} AgentFamily
27
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").AgentSessionTranscriptEvent} AgentSessionTranscriptEvent
28
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").AgentTraceCapabilityProfile} AgentTraceCapabilityProfile
29
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").AgentTraceSummary} AgentTraceSummary
30
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").CanonicalAgentTraceEvent} CanonicalAgentTraceEvent
31
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").CanonicalAgentTraceEventKind} CanonicalAgentTraceEventKind
32
+ * @typedef {import("@smithers-orchestrator/observability/agentTrace").TraceCompleteness} TraceCompleteness
33
+ * @typedef {import("./AgentTraceCollectorOptions.ts").AgentTraceCollectorOptions} AgentTraceCollectorOptions
34
+ * @typedef {import("./events.js").EventBus} EventBus
35
+ *
36
+ * @typedef {import("@smithers-orchestrator/observability/_traceEventNormalizers").NormalizedTraceBatch} NormalizedTraceBatch
37
+ * @typedef {import("@smithers-orchestrator/observability/_traceEventNormalizers").NormalizedTraceEvent} NormalizedTraceEvent
38
+ */
39
+
40
+ /**
41
+ * @param {Record<string, string | number | boolean> | undefined} annotations
42
+ * @returns {Record<string, string | number | boolean>}
43
+ */
44
+ function normalizeAnnotations(annotations) {
45
+ /** @type {Record<string, string | number | boolean>} */
46
+ const normalized = {};
47
+ for (const [key, value] of Object.entries(annotations ?? {})) {
48
+ if (["string", "number", "boolean"].includes(typeof value)) {
49
+ normalized[key] = /** @type {string | number | boolean} */ (value);
50
+ }
51
+ }
52
+ return normalized;
53
+ }
54
+
55
+ export class AgentTraceCollector {
56
+ /** @type {EventBus} */
57
+ eventBus;
58
+ /** @type {string} */
59
+ runId;
60
+ /** @type {string | undefined} */
61
+ workflowPath;
62
+ /** @type {string | undefined} */
63
+ workflowHash;
64
+ /** @type {string} */
65
+ cwd;
66
+ /** @type {string} */
67
+ nodeId;
68
+ /** @type {number} */
69
+ iteration;
70
+ /** @type {number} */
71
+ attempt;
72
+ /** @type {any} */
73
+ agent;
74
+ /** @type {AgentFamily} */
75
+ agentFamily;
76
+ /** @type {AgentCaptureMode} */
77
+ captureMode;
78
+ /** @type {string | undefined} */
79
+ agentId;
80
+ /** @type {string | undefined} */
81
+ model;
82
+ /** @type {Record<string, string | number | boolean>} */
83
+ annotations;
84
+ /** @type {string | undefined} */
85
+ logDir;
86
+ /** @type {AgentTraceCapabilityProfile} */
87
+ capabilities;
88
+ /** @type {number} */
89
+ startedAtMs = nowMs();
90
+ /** @type {CanonicalAgentTraceEvent[]} */
91
+ events = [];
92
+ /** @type {AgentSessionTranscriptEvent[]} */
93
+ sessionEvents = [];
94
+ /** @type {string[]} */
95
+ rawArtifactRefs = [];
96
+ /** @type {Set<CanonicalAgentTraceEventKind>} */
97
+ seenKinds = new Set();
98
+ /** @type {Set<string>} */
99
+ seenSessionRows = new Set();
100
+ /** @type {Set<CanonicalAgentTraceEventKind>} */
101
+ directKinds = new Set();
102
+ /** @type {Set<CanonicalAgentTraceEventKind>} */
103
+ expectedKinds = new Set();
104
+ /** @type {string[]} */
105
+ failures = [];
106
+ /** @type {string[]} */
107
+ warnings = [];
108
+ sequence = 0;
109
+ sessionSequence = 0;
110
+ rawEventSequence = 0;
111
+ stdoutBuffer = "";
112
+ stderrBuffer = "";
113
+ assistantTextBuffer = "";
114
+ /** @type {string | null} */
115
+ finalText = null;
116
+ /** @type {string | undefined} */
117
+ providerSessionId;
118
+ /** @type {string | undefined} */
119
+ providerThreadId;
120
+ /** @type {string | undefined} */
121
+ currentRawEventId;
122
+ /** @type {((event: SmithersEvent) => void) | undefined} */
123
+ listener;
124
+
125
+ /** @param {AgentTraceCollectorOptions} opts */
126
+ constructor(opts) {
127
+ this.eventBus = opts.eventBus;
128
+ this.runId = opts.runId;
129
+ this.workflowPath = opts.workflowPath ?? undefined;
130
+ this.workflowHash = opts.workflowHash ?? undefined;
131
+ this.cwd = opts.cwd;
132
+ this.nodeId = opts.nodeId;
133
+ this.iteration = opts.iteration;
134
+ this.attempt = opts.attempt;
135
+ this.agent = opts.agent;
136
+ this.agentFamily = detectAgentFamily(opts.agent);
137
+ this.captureMode = detectCaptureMode(opts.agent);
138
+ this.capabilities = resolveAgentTraceCapabilities(this.agentFamily, this.captureMode);
139
+ this.agentId = opts.agentId;
140
+ this.model = opts.model;
141
+ this.annotations = normalizeAnnotations(opts.annotations);
142
+ this.logDir = opts.logDir;
143
+
144
+ const profile = this.capabilities;
145
+ if (profile.sessionMetadata && this.agentFamily === "pi") {
146
+ this.expectedKinds.add("session.start");
147
+ this.expectedKinds.add("session.end");
148
+ this.expectedKinds.add("turn.start");
149
+ this.expectedKinds.add("turn.end");
150
+ }
151
+ if (profile.finalAssistantMessage) {
152
+ this.expectedKinds.add("assistant.message.final");
153
+ }
154
+ }
155
+
156
+ begin() {
157
+ this.listener = (event) => this.observeSmithersEvent(event);
158
+ this.eventBus.on("event", this.listener);
159
+ }
160
+
161
+ endListener() {
162
+ if (this.listener) this.eventBus.off("event", this.listener);
163
+ this.listener = undefined;
164
+ }
165
+
166
+ /** @param {string} text */
167
+ onStdout(text) {
168
+ this.processChunk("stdout", text);
169
+ }
170
+
171
+ /** @param {string} text */
172
+ onStderr(text) {
173
+ this.processChunk("stderr", text);
174
+ }
175
+
176
+ /** @param {any} result */
177
+ observeResult(result) {
178
+ const text = String(result?.text ?? "").trim();
179
+ const rawEventId = this.nextRawEventId("result");
180
+ if (text &&
181
+ (!this.finalText ||
182
+ (!this.seenKinds.has("assistant.text.delta") &&
183
+ !this.seenKinds.has("assistant.message.final")))) {
184
+ this.finalText = text;
185
+ }
186
+ if (this.captureMode === "sdk-events" && text) {
187
+ this.pushDerived("assistant.message.final", { text }, text, undefined, true, rawEventId);
188
+ }
189
+ const usage = normalizeTokenUsage(result?.usage ?? result?.totalUsage);
190
+ if (usage) {
191
+ this.pushDerived("usage", usage, usage, "usage", true, rawEventId);
192
+ }
193
+ }
194
+
195
+ /** @param {unknown} error */
196
+ observeError(error) {
197
+ this.failures.push(error instanceof Error ? error.message : String(error));
198
+ const rawEventId = this.nextRawEventId("error");
199
+ this.pushDerived("capture.error", { error: this.failures.at(-1) }, { error: this.failures.at(-1) }, "error", true, rawEventId);
200
+ }
201
+
202
+ async flush() {
203
+ this.endListener();
204
+ const finishedAtMs = nowMs();
205
+ this.flushStructuredBuffers();
206
+ await this.importProviderSessionTranscript();
207
+ if (this.captureMode !== "sdk-events" &&
208
+ !this.seenKinds.has("assistant.message.final") &&
209
+ this.finalText &&
210
+ this.failures.length === 0) {
211
+ this.pushDerived("assistant.message.final", { text: this.finalText }, this.finalText, undefined, false);
212
+ }
213
+ if ((this.captureMode === "cli-json-stream" || this.captureMode === "rpc-events") &&
214
+ this.events.length > 0 &&
215
+ !this.seenKinds.has("assistant.message.final") &&
216
+ this.failures.length === 0) {
217
+ this.warnings.push("structured stream ended without a terminal assistant message");
218
+ this.pushDerived("capture.warning", { reason: "missing-terminal-event" }, { reason: "missing-terminal-event" }, "capture");
219
+ }
220
+
221
+ let traceCompleteness = this.resolveCompleteness();
222
+ let missingExpectedEventKinds = [...this.expectedKinds].filter((kind) => !this.directKinds.has(kind));
223
+ this.applyTraceCompleteness(traceCompleteness);
224
+ /** @type {AgentTraceSummary} */
225
+ let summary = {
226
+ traceVersion: "1",
227
+ runId: this.runId,
228
+ workflowPath: this.workflowPath,
229
+ workflowHash: this.workflowHash,
230
+ nodeId: this.nodeId,
231
+ iteration: this.iteration,
232
+ attempt: this.attempt,
233
+ traceStartedAtMs: this.startedAtMs,
234
+ traceFinishedAtMs: finishedAtMs,
235
+ agentFamily: this.agentFamily,
236
+ agentId: this.agentId,
237
+ model: this.model,
238
+ captureMode: this.captureMode,
239
+ traceCompleteness,
240
+ unsupportedEventKinds: unsupportedKindsForCapabilities(this.capabilities).filter((kind) => !this.seenKinds.has(kind)),
241
+ missingExpectedEventKinds,
242
+ rawArtifactRefs: this.rawArtifactRefs,
243
+ };
244
+
245
+ const persistedArtifact = await this.persistNdjson(summary);
246
+ if (persistedArtifact.ok && persistedArtifact.file) {
247
+ const artifactPath = persistedArtifact.file;
248
+ this.rawArtifactRefs.push(artifactPath);
249
+ this.pushDerived("artifact.created", {
250
+ artifactKind: "agent-trace.ndjson",
251
+ artifactPath,
252
+ contentType: "application/x-ndjson",
253
+ }, {
254
+ artifactKind: "agent-trace.ndjson",
255
+ artifactPath,
256
+ contentType: "application/x-ndjson",
257
+ }, "artifact");
258
+ this.applyTraceCompleteness(traceCompleteness);
259
+ summary = { ...summary, rawArtifactRefs: [...this.rawArtifactRefs] };
260
+ try {
261
+ await this.rewriteNdjson(artifactPath, summary);
262
+ } catch (error) {
263
+ const message = error instanceof Error ? error.message : String(error);
264
+ this.warnings.push(message);
265
+ this.pushDerived("capture.warning", { reason: "artifact-rewrite-failed", error: message }, { reason: "artifact-rewrite-failed", error: message }, "artifact");
266
+ traceCompleteness = this.resolveCompleteness();
267
+ missingExpectedEventKinds = [...this.expectedKinds].filter((kind) => !this.directKinds.has(kind));
268
+ this.applyTraceCompleteness(traceCompleteness);
269
+ summary = {
270
+ ...summary,
271
+ traceCompleteness,
272
+ missingExpectedEventKinds,
273
+ rawArtifactRefs: [...this.rawArtifactRefs],
274
+ };
275
+ }
276
+ } else if (!persistedArtifact.ok) {
277
+ this.warnings.push(persistedArtifact.error);
278
+ this.pushDerived("capture.warning", { reason: "artifact-write-failed", error: persistedArtifact.error }, { reason: "artifact-write-failed", error: persistedArtifact.error }, "artifact");
279
+ traceCompleteness = this.resolveCompleteness();
280
+ missingExpectedEventKinds = [...this.expectedKinds].filter((kind) => !this.directKinds.has(kind));
281
+ this.applyTraceCompleteness(traceCompleteness);
282
+ summary = {
283
+ ...summary,
284
+ traceCompleteness,
285
+ missingExpectedEventKinds,
286
+ rawArtifactRefs: [...this.rawArtifactRefs],
287
+ };
288
+ }
289
+
290
+ this.applyTraceCompleteness(traceCompleteness);
291
+ for (const event of this.events) {
292
+ /** @type {SmithersEvent} */
293
+ const smithersEvent = /** @type {any} */ ({
294
+ type: "AgentTraceEvent",
295
+ runId: this.runId,
296
+ nodeId: this.nodeId,
297
+ iteration: this.iteration,
298
+ attempt: this.attempt,
299
+ trace: event,
300
+ timestampMs: event.timestampMs,
301
+ });
302
+ await this.eventBus.emitEventQueued(smithersEvent);
303
+ if (!shouldExportTraceEventToOtel(event)) continue;
304
+ const record = canonicalTraceEventToOtelLogRecord(event, {
305
+ agentId: this.agentId,
306
+ model: this.model,
307
+ });
308
+ await emitOtelLogRecord("agent-trace", record);
309
+ }
310
+ for (const event of this.sessionEvents) {
311
+ /** @type {SmithersEvent} */
312
+ const smithersEvent = /** @type {any} */ ({
313
+ type: "AgentSessionEvent",
314
+ runId: this.runId,
315
+ nodeId: this.nodeId,
316
+ iteration: this.iteration,
317
+ attempt: this.attempt,
318
+ transcript: event,
319
+ timestampMs: event.timestampMs,
320
+ });
321
+ await this.eventBus.emitEventQueued(smithersEvent);
322
+ const record = agentSessionEventToOtelLogRecord(event, {
323
+ agentId: this.agentId,
324
+ model: this.model,
325
+ });
326
+ await emitOtelLogRecord("agent-session", record);
327
+ }
328
+
329
+ await this.eventBus.emitEventQueued(/** @type {any} */ ({
330
+ type: "AgentTraceSummary",
331
+ runId: this.runId,
332
+ nodeId: this.nodeId,
333
+ iteration: this.iteration,
334
+ attempt: this.attempt,
335
+ summary,
336
+ timestampMs: finishedAtMs,
337
+ }));
338
+ }
339
+
340
+ /**
341
+ * @param {"stdout" | "stderr"} stream
342
+ * @param {string} text
343
+ */
344
+ processChunk(stream, text) {
345
+ if (stream === "stderr") {
346
+ this.stderrBuffer += text;
347
+ this.pushObserved("stderr", { text }, text, stream);
348
+ return;
349
+ }
350
+ this.stdoutBuffer += text;
351
+ if (this.captureMode === "cli-text" || this.captureMode === "sdk-events") {
352
+ this.pushObserved("stdout", { text }, text, stream);
353
+ return;
354
+ }
355
+ const lines = this.stdoutBuffer.split(/\r?\n/);
356
+ this.stdoutBuffer = lines.pop() ?? "";
357
+ for (const line of lines) {
358
+ if (!line.trim()) continue;
359
+ this.processStructuredStdoutLine(line);
360
+ }
361
+ }
362
+
363
+ flushStructuredBuffers() {
364
+ if (this.captureMode === "cli-text" || this.captureMode === "sdk-events") {
365
+ this.stdoutBuffer = "";
366
+ this.stderrBuffer = "";
367
+ return;
368
+ }
369
+ const line = this.stdoutBuffer.trim();
370
+ this.stdoutBuffer = "";
371
+ this.stderrBuffer = "";
372
+ if (!line) return;
373
+ this.failures.push(`truncated structured stream: ${line.slice(0, 200)}`);
374
+ this.pushObserved("capture.error", { reason: "truncated-json-stream", linePreview: line.slice(0, 200) }, line, "stdout");
375
+ }
376
+
377
+ /** @param {string} line */
378
+ processStructuredStdoutLine(line) {
379
+ let parsed;
380
+ try {
381
+ parsed = JSON.parse(line);
382
+ } catch {
383
+ this.failures.push(`malformed upstream JSON: ${line.slice(0, 200)}`);
384
+ this.pushObserved("capture.error", { linePreview: line.slice(0, 200), reason: "malformed-json" }, line, "stdout");
385
+ return;
386
+ }
387
+ const rawType = typeof parsed?.type === "string" ? parsed.type : "structured";
388
+ const previousRawEventId = this.currentRawEventId;
389
+ this.currentRawEventId = this.nextRawEventId(rawType);
390
+ try {
391
+ this.observeProviderSessionRow(parsed, "live");
392
+ this.emitObservedBatch(normalizeStructuredEvent(this.agentFamily, parsed, rawType));
393
+ } finally {
394
+ this.currentRawEventId = previousRawEventId;
395
+ }
396
+ }
397
+
398
+ /** @param {string} text */
399
+ appendAssistantText(text) {
400
+ this.assistantTextBuffer += text;
401
+ this.finalText = this.assistantTextBuffer;
402
+ }
403
+
404
+ /** @param {string} text */
405
+ setFinalAssistantText(text) {
406
+ this.assistantTextBuffer = text;
407
+ this.finalText = text;
408
+ }
409
+
410
+ /** @param {SmithersEvent} event */
411
+ observeSmithersEvent(event) {
412
+ const anyEvent = /** @type {any} */ (event);
413
+ const sameAttempt = anyEvent.runId === this.runId &&
414
+ anyEvent.nodeId === this.nodeId &&
415
+ anyEvent.iteration === this.iteration &&
416
+ anyEvent.attempt === this.attempt;
417
+ if (!sameAttempt) return;
418
+ if (this.agentFamily === "pi") return;
419
+ if (event.type === "ToolCallStarted") {
420
+ const rawEventId = this.nextRawEventId(event.type);
421
+ this.pushDerived("tool.execution.start", {
422
+ toolCallId: event.toolCallId,
423
+ toolName: event.toolName,
424
+ }, event, event.type, true, rawEventId);
425
+ this.expectedKinds.add("tool.execution.end");
426
+ }
427
+ if (event.type === "ToolCallFinished") {
428
+ const rawEventId = this.nextRawEventId(event.type);
429
+ this.pushDerived("tool.execution.end", {
430
+ toolCallId: event.toolCallId,
431
+ toolName: event.toolName,
432
+ isError: event.status === "error",
433
+ }, event, event.type, true, rawEventId);
434
+ }
435
+ if (event.type === "TokenUsageReported") {
436
+ const rawEventId = this.nextRawEventId(event.type);
437
+ this.pushDerived("usage", {
438
+ model: event.model,
439
+ agent: event.agent,
440
+ inputTokens: event.inputTokens,
441
+ outputTokens: event.outputTokens,
442
+ cacheReadTokens: event.cacheReadTokens,
443
+ cacheWriteTokens: event.cacheWriteTokens,
444
+ reasoningTokens: event.reasoningTokens,
445
+ }, event, event.type, true, rawEventId);
446
+ }
447
+ }
448
+
449
+ /** @returns {TraceCompleteness} */
450
+ resolveCompleteness() {
451
+ if (this.failures.length > 0) return "capture-failed";
452
+ /** @type {Set<CanonicalAgentTraceEventKind>} */
453
+ const richKinds = new Set([
454
+ "session.start",
455
+ "session.end",
456
+ "turn.start",
457
+ "turn.end",
458
+ "message.start",
459
+ "message.update",
460
+ "message.end",
461
+ "assistant.text.delta",
462
+ "assistant.thinking.delta",
463
+ "tool.execution.start",
464
+ "tool.execution.update",
465
+ "tool.execution.end",
466
+ "tool.result",
467
+ "retry.start",
468
+ "retry.end",
469
+ "compaction.start",
470
+ "compaction.end",
471
+ ]);
472
+ const sawRichStructure = [...this.directKinds].some((kind) => richKinds.has(kind));
473
+ const coarseCaptureMode = this.captureMode === "sdk-events" ||
474
+ this.captureMode === "cli-text" ||
475
+ this.captureMode === "cli-json";
476
+ if (!sawRichStructure && this.warnings.length === 0 && coarseCaptureMode) {
477
+ return "final-only";
478
+ }
479
+ const missing = [...this.expectedKinds].filter((kind) => !this.directKinds.has(kind));
480
+ if (missing.length > 0 || this.warnings.length > 0) return "partial-observed";
481
+ if (coarseCaptureMode) {
482
+ return sawRichStructure ? "partial-observed" : "final-only";
483
+ }
484
+ if (!sawRichStructure) return "final-only";
485
+ return "full-observed";
486
+ }
487
+
488
+ /**
489
+ * @param {CanonicalAgentTraceEventKind} kind
490
+ * @param {Record<string, unknown> | null} payload
491
+ * @param {unknown} raw
492
+ * @param {boolean} observed
493
+ * @param {string} [rawType]
494
+ * @param {boolean} [direct]
495
+ * @param {string} [rawEventId]
496
+ */
497
+ push(kind, payload, raw, observed, rawType, direct = true, rawEventId) {
498
+ const redactedPayload = redactValue(payload);
499
+ const redactedRaw = redactValue(raw);
500
+ /** @type {CanonicalAgentTraceEvent} */
501
+ const event = {
502
+ traceVersion: "1",
503
+ runId: this.runId,
504
+ workflowPath: this.workflowPath,
505
+ workflowHash: this.workflowHash,
506
+ nodeId: this.nodeId,
507
+ iteration: this.iteration,
508
+ attempt: this.attempt,
509
+ timestampMs: nowMs(),
510
+ event: {
511
+ sequence: this.sequence++,
512
+ kind,
513
+ phase: kindPhase(kind),
514
+ },
515
+ source: {
516
+ agentFamily: this.agentFamily,
517
+ captureMode: this.captureMode,
518
+ rawType,
519
+ rawEventId: rawEventId ??
520
+ (observed
521
+ ? (this.currentRawEventId ?? this.nextRawEventId(rawType ?? kind))
522
+ : undefined),
523
+ observed,
524
+ },
525
+ traceCompleteness: "partial-observed",
526
+ payload: /** @type {Record<string, unknown> | null} */ (redactedPayload.value),
527
+ raw: redactedRaw.value,
528
+ redaction: {
529
+ applied: redactedPayload.applied || redactedRaw.applied,
530
+ ruleIds: [...new Set([...redactedPayload.ruleIds, ...redactedRaw.ruleIds])],
531
+ },
532
+ annotations: this.annotations,
533
+ };
534
+ this.events.push(event);
535
+ this.seenKinds.add(kind);
536
+ if (direct) this.directKinds.add(kind);
537
+ }
538
+
539
+ /**
540
+ * @param {CanonicalAgentTraceEventKind} kind
541
+ * @param {Record<string, unknown> | null} payload
542
+ * @param {unknown} raw
543
+ * @param {string} [rawType]
544
+ * @param {string} [rawEventId]
545
+ */
546
+ pushObserved(kind, payload, raw, rawType, rawEventId) {
547
+ this.push(kind, payload, raw, true, rawType, true, rawEventId);
548
+ }
549
+
550
+ /**
551
+ * @param {CanonicalAgentTraceEventKind} kind
552
+ * @param {Record<string, unknown> | null} payload
553
+ * @param {unknown} raw
554
+ * @param {string} [rawType]
555
+ * @param {boolean} [direct]
556
+ * @param {string} [rawEventId]
557
+ */
558
+ pushDerived(kind, payload, raw, rawType, direct = true, rawEventId) {
559
+ this.push(kind, payload, raw, false, rawType, direct, rawEventId);
560
+ }
561
+
562
+ /** @param {TraceCompleteness} traceCompleteness */
563
+ applyTraceCompleteness(traceCompleteness) {
564
+ for (const event of this.events) {
565
+ event.traceCompleteness = traceCompleteness;
566
+ }
567
+ }
568
+
569
+ /**
570
+ * @param {NormalizedTraceBatch} batch
571
+ * @param {string} [rawEventId]
572
+ */
573
+ emitObservedBatch(batch, rawEventId = this.currentRawEventId) {
574
+ for (const kind of batch.expectedKinds ?? []) {
575
+ this.expectedKinds.add(kind);
576
+ }
577
+ for (const event of batch.events) {
578
+ this.observeNormalizedEvent(event);
579
+ if (event.observed) {
580
+ this.pushObserved(event.kind, event.payload, event.raw, event.rawType, rawEventId);
581
+ continue;
582
+ }
583
+ this.pushDerived(event.kind, event.payload, event.raw, event.rawType, true, rawEventId);
584
+ }
585
+ }
586
+
587
+ /** @param {NormalizedTraceEvent} event */
588
+ observeNormalizedEvent(event) {
589
+ if (event.kind === "assistant.text.delta" &&
590
+ typeof event.payload?.text === "string") {
591
+ this.appendAssistantText(event.payload.text);
592
+ return;
593
+ }
594
+ if (event.kind === "assistant.message.final" &&
595
+ typeof event.payload?.text === "string") {
596
+ this.setFinalAssistantText(event.payload.text);
597
+ }
598
+ }
599
+
600
+ /** @param {string} rawType */
601
+ nextRawEventId(rawType) {
602
+ return `${rawType}:${this.rawEventSequence++}`;
603
+ }
604
+
605
+ /**
606
+ * @param {unknown} row
607
+ * @param {"live" | "artifact"} ingestSource
608
+ */
609
+ observeProviderSessionRow(row, ingestSource) {
610
+ if (this.captureMode !== "cli-json-stream" && this.captureMode !== "rpc-events") return;
611
+ const fingerprint = JSON.stringify(row);
612
+ if (this.seenSessionRows.has(fingerprint)) return;
613
+ this.seenSessionRows.add(fingerprint);
614
+ const correlation = extractProviderSessionCorrelation(this.agentFamily, row);
615
+ if (correlation.sessionId) this.providerSessionId = correlation.sessionId;
616
+ if (correlation.threadId) this.providerThreadId = correlation.threadId;
617
+ const redacted = redactValue(row);
618
+ const parsed = /** @type {any} */ (row);
619
+ this.sessionEvents.push({
620
+ transcriptVersion: "1",
621
+ runId: this.runId,
622
+ workflowPath: this.workflowPath,
623
+ workflowHash: this.workflowHash,
624
+ nodeId: this.nodeId,
625
+ iteration: this.iteration,
626
+ attempt: this.attempt,
627
+ timestampMs: nowMs(),
628
+ event: {
629
+ sequence: this.sessionSequence++,
630
+ rowType: typeof parsed?.type === "string" && parsed.type ? parsed.type : "structured",
631
+ },
632
+ source: {
633
+ agentFamily: this.agentFamily,
634
+ captureMode: this.captureMode,
635
+ ingestSource,
636
+ observedLive: ingestSource === "live",
637
+ providerSessionId: this.providerSessionId,
638
+ providerThreadId: this.providerThreadId,
639
+ },
640
+ raw: redacted.value,
641
+ redaction: {
642
+ applied: redacted.applied,
643
+ ruleIds: redacted.ruleIds,
644
+ },
645
+ annotations: this.annotations,
646
+ });
647
+ }
648
+
649
+ async importProviderSessionTranscript() {
650
+ const file = await this.resolveProviderSessionFile();
651
+ if (!file) return;
652
+ let text;
653
+ try {
654
+ text = await readFile(file, "utf8");
655
+ } catch (error) {
656
+ this.warnings.push(error instanceof Error ? error.message : String(error));
657
+ return;
658
+ }
659
+ for (const line of text.split(/\r?\n/)) {
660
+ if (!line.trim()) continue;
661
+ try {
662
+ this.observeProviderSessionRow(JSON.parse(line), "artifact");
663
+ } catch {
664
+ // Keep canonical capture strict; transcript backfill is best effort only.
665
+ }
666
+ }
667
+ }
668
+
669
+ /** @returns {Promise<string | null>} */
670
+ async resolveProviderSessionFile() {
671
+ if (this.agentFamily === "pi") {
672
+ return resolvePiSessionFile(this.agent, this.providerSessionId);
673
+ }
674
+ if (this.agentFamily === "claude-code") {
675
+ return resolveClaudeSessionFile(this.agent, this.cwd, this.providerSessionId);
676
+ }
677
+ if (this.agentFamily === "codex") {
678
+ return resolveCodexSessionFile(this.agent, this.cwd, this.startedAtMs);
679
+ }
680
+ return null;
681
+ }
682
+
683
+ /**
684
+ * @param {AgentTraceSummary} summary
685
+ * @returns {Promise<{ ok: true; file?: string } | { ok: false; error: string }>}
686
+ */
687
+ async persistNdjson(summary) {
688
+ if (!this.logDir) return { ok: true };
689
+ const dir = join(this.logDir, "agent-trace");
690
+ const file = join(dir, `${this.nodeId}-${this.iteration}-${this.attempt}.ndjson`);
691
+ const lines = this.events
692
+ .map((event) => JSON.stringify(event))
693
+ .concat(JSON.stringify({ summary }));
694
+ try {
695
+ await mkdir(dir, { recursive: true });
696
+ await appendFile(file, `${lines.join("\n")}\n`, "utf8");
697
+ return { ok: true, file };
698
+ } catch (error) {
699
+ return {
700
+ ok: false,
701
+ error: error instanceof Error ? error.message : String(error),
702
+ };
703
+ }
704
+ }
705
+
706
+ /**
707
+ * @param {string} file
708
+ * @param {AgentTraceSummary} summary
709
+ */
710
+ async rewriteNdjson(file, summary) {
711
+ const lines = this.events
712
+ .map((event) => JSON.stringify(event))
713
+ .concat(JSON.stringify({ summary }));
714
+ await writeFile(file, `${lines.join("\n")}\n`, "utf8");
715
+ }
716
+ }