@smithers-orchestrator/engine 0.20.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/engine",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Concrete Smithers workflow execution engine",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -33,20 +33,20 @@
33
33
  "react": "^19.2.5",
34
34
  "react-dom": "^19.2.5",
35
35
  "zod": "^4.3.6",
36
- "@smithers-orchestrator/agents": "0.20.0",
37
- "@smithers-orchestrator/driver": "0.20.0",
38
- "@smithers-orchestrator/components": "0.20.0",
39
- "@smithers-orchestrator/db": "0.20.0",
40
- "@smithers-orchestrator/graph": "0.20.0",
41
- "@smithers-orchestrator/memory": "0.20.0",
42
- "@smithers-orchestrator/errors": "0.20.0",
43
- "@smithers-orchestrator/observability": "0.20.0",
44
- "@smithers-orchestrator/react-reconciler": "0.20.0",
45
- "@smithers-orchestrator/sandbox": "0.20.0",
46
- "@smithers-orchestrator/scheduler": "0.20.0",
47
- "@smithers-orchestrator/scorers": "0.20.0",
48
- "@smithers-orchestrator/vcs": "0.20.0",
49
- "@smithers-orchestrator/time-travel": "0.20.0"
36
+ "@smithers-orchestrator/components": "0.20.1",
37
+ "@smithers-orchestrator/db": "0.20.1",
38
+ "@smithers-orchestrator/driver": "0.20.1",
39
+ "@smithers-orchestrator/errors": "0.20.1",
40
+ "@smithers-orchestrator/agents": "0.20.1",
41
+ "@smithers-orchestrator/react-reconciler": "0.20.1",
42
+ "@smithers-orchestrator/memory": "0.20.1",
43
+ "@smithers-orchestrator/observability": "0.20.1",
44
+ "@smithers-orchestrator/graph": "0.20.1",
45
+ "@smithers-orchestrator/sandbox": "0.20.1",
46
+ "@smithers-orchestrator/scheduler": "0.20.1",
47
+ "@smithers-orchestrator/scorers": "0.20.1",
48
+ "@smithers-orchestrator/time-travel": "0.20.1",
49
+ "@smithers-orchestrator/vcs": "0.20.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/bun": "latest",
@@ -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
+ }
@@ -0,0 +1,17 @@
1
+ import type { EventBus } from "./events.js";
2
+
3
+ export type AgentTraceCollectorOptions = {
4
+ eventBus: EventBus;
5
+ runId: string;
6
+ workflowPath?: string | null;
7
+ workflowHash?: string | null;
8
+ cwd: string;
9
+ nodeId: string;
10
+ iteration: number;
11
+ attempt: number;
12
+ agent: unknown;
13
+ agentId?: string;
14
+ model?: string;
15
+ logDir?: string;
16
+ annotations?: Record<string, string | number | boolean>;
17
+ };
package/src/engine.js CHANGED
@@ -20,6 +20,7 @@ import { buildPlanTree, scheduleTasks, buildStateKey, } from "./scheduler.js";
20
20
  import { getDefinedToolMetadata } from "./getDefinedToolMetadata.js";
21
21
  import { captureSnapshotEffect, loadLatestSnapshot, parseSnapshot, } from "@smithers-orchestrator/time-travel/snapshot";
22
22
  import { EventBus } from "./events.js";
23
+ import { AgentTraceCollector } from "./AgentTraceCollector.js";
23
24
  import { getJjPointer, runJj, workspaceAdd } from "@smithers-orchestrator/vcs/jj";
24
25
  import { findVcsRoot } from "@smithers-orchestrator/vcs/find-root";
25
26
  import * as BunContext from "@effect/platform-bun/BunContext";
@@ -117,6 +118,78 @@ function isAbortError(err) {
117
118
  }
118
119
  return false;
119
120
  }
121
+ /**
122
+ * @param {unknown} err
123
+ * @returns {string[]}
124
+ */
125
+ function collectErrorMessages(err) {
126
+ const messages = [];
127
+ let current = err;
128
+ const seen = new Set();
129
+ while (current && typeof current === "object" && !seen.has(current)) {
130
+ seen.add(current);
131
+ const record = /** @type {Record<string, unknown>} */ (current);
132
+ if (typeof record.name === "string")
133
+ messages.push(record.name);
134
+ if (typeof record.message === "string")
135
+ messages.push(record.message);
136
+ current = record.cause;
137
+ }
138
+ if (typeof err === "string") {
139
+ messages.push(err);
140
+ }
141
+ return messages;
142
+ }
143
+ /**
144
+ * @param {unknown} err
145
+ * @returns {boolean}
146
+ */
147
+ function isStructuredOutputParseFailure(err) {
148
+ // AI SDK 6.x names structured-output parse/validation failures with these
149
+ // stable error names. The message fallback catches errors after wrapping.
150
+ const aiSdkErrorName = /^AI_(NoObjectGeneratedError|NoOutputGeneratedError|JSONParseError|TypeValidationError)$/;
151
+ const aiSdkErrorMessage = /No output generated|No object generated|could not parse the response|structured output parse|response did not match schema/i;
152
+ return collectErrorMessages(err).some((message) => aiSdkErrorName.test(message) || aiSdkErrorMessage.test(message));
153
+ }
154
+ /**
155
+ * @param {string} nodeId
156
+ * @returns {string}
157
+ */
158
+ function depsTextAccessHint(nodeId) {
159
+ return /^[A-Za-z_$][\w$]*$/.test(nodeId)
160
+ ? `deps.${nodeId}.text`
161
+ : `deps[${JSON.stringify(nodeId)}].text`;
162
+ }
163
+ /**
164
+ * @param {Pick<TaskDescriptor, "nodeId" | "outputTableName">} desc
165
+ * @param {unknown} cause
166
+ * @param {Record<string, unknown>} [details]
167
+ * @returns {SmithersError}
168
+ */
169
+ function makeStructuredOutputCompatibilityError(desc, cause, details = {}) {
170
+ return new SmithersError("INVALID_OUTPUT", `Task "${desc.nodeId}" expected structured JSON output, but the agent/model did not return valid JSON for the declared output schema. This commonly happens with OpenAI-compatible local model servers such as llama.cpp that do not fully support JSON schema structured output. Use a model that supports structured output, or opt out with OpenAIAgent({ nativeStructuredOutput: false }) so Smithers can use prompt-based JSON extraction.`, {
171
+ nodeId: desc.nodeId,
172
+ outputTable: desc.outputTableName,
173
+ ...details,
174
+ hint: "For plain text, use an object-shaped schema such as z.object({ text: z.string() }) and read it downstream as deps.<task>.text.",
175
+ }, { cause });
176
+ }
177
+ /**
178
+ * @param {Pick<TaskDescriptor, "nodeId" | "outputTableName">} desc
179
+ * @param {string} text
180
+ * @param {unknown} [cause]
181
+ * @param {Record<string, unknown>} [details]
182
+ * @returns {SmithersError}
183
+ */
184
+ function makePlainTextOutputError(desc, text, cause, details = {}) {
185
+ const preview = text.slice(0, 120).replace(/\s+/g, " ").trim();
186
+ return new SmithersError("INVALID_OUTPUT", `Task "${desc.nodeId}" returned plain text, but Smithers task outputs must be JSON objects matching the declared output schema. Plain text cannot be passed through deps directly. Use z.object({ text: z.string() }), return {"text":"..."}, and read it downstream as ${depsTextAccessHint(desc.nodeId)}.`, {
187
+ nodeId: desc.nodeId,
188
+ outputTable: desc.outputTableName,
189
+ ...details,
190
+ textPreview: preview || undefined,
191
+ }, cause === undefined ? undefined : { cause });
192
+ }
120
193
  /**
121
194
  * @param {AbortSignal} [signal]
122
195
  * @returns {Promise<never> | null}
@@ -2582,6 +2655,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
2582
2655
  let responseText = null;
2583
2656
  let effectiveAgent = null;
2584
2657
  let supportsNativeStructuredOutput = false;
2658
+ let structuredOutputAccessError;
2585
2659
  // Resolve effective root once so both caching and execution share it.
2586
2660
  const taskRoot = desc.worktreePath ?? toolConfig.rootDir;
2587
2661
  const stepCacheEnabled = cacheEnabled || Boolean(desc.cachePolicy);
@@ -3053,54 +3127,95 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3053
3127
  }, 100)
3054
3128
  : undefined;
3055
3129
  // Use fallback agent on retry attempts when available
3130
+ const traceCollector = toolConfig.traceContext && effectiveAgent
3131
+ ? new AgentTraceCollector({
3132
+ eventBus,
3133
+ runId,
3134
+ workflowPath: toolConfig.traceContext.workflowPath,
3135
+ workflowHash: toolConfig.traceContext.workflowHash,
3136
+ cwd: taskRoot,
3137
+ nodeId: desc.nodeId,
3138
+ iteration: desc.iteration,
3139
+ attempt: attemptNo,
3140
+ agent: effectiveAgent,
3141
+ agentId: attemptMeta.agentId ?? undefined,
3142
+ model: attemptMeta.agentModel ?? undefined,
3143
+ logDir: toolConfig.traceContext.logDir,
3144
+ annotations: toolConfig.traceContext.annotations,
3145
+ })
3146
+ : null;
3147
+ if (traceCollector) traceCollector.begin();
3056
3148
  let result;
3057
3149
  try {
3058
- result = await runPromisePreservingFailure(withSmithersSpan(smithersSpanNames.agent, Effect.tryPromise({
3059
- try: () => {
3060
- const agentCall = guidedResumeMessages?.length
3061
- ? {
3062
- messages: guidedResumeMessages,
3063
- }
3064
- : {
3065
- prompt: effectivePrompt,
3066
- };
3067
- return effectiveAgent.generate({
3068
- options: undefined,
3069
- abortSignal: taskSignal,
3070
- ...agentCall,
3071
- resumeSession,
3072
- lastHeartbeat: previousHeartbeat,
3073
- rootDir: taskRoot,
3074
- maxOutputBytes: toolConfig.maxOutputBytes,
3075
- timeout: desc.timeoutMs
3076
- ? { totalMs: desc.timeoutMs }
3077
- : undefined,
3078
- onStdout: (text) => {
3079
- recordInternalHeartbeat();
3080
- emitOutput(text, "stdout");
3081
- },
3082
- onStderr: (text) => {
3083
- recordInternalHeartbeat();
3084
- emitOutput(text, "stderr");
3085
- },
3086
- onEvent: handleAgentEvent,
3087
- onStepFinish: handleSdkStepFinish,
3088
- outputSchema: desc.outputSchema,
3089
- });
3090
- },
3091
- catch: (error) => error,
3092
- }), {
3093
- ...taskSpanContext,
3094
- agent: attemptMeta.agentId ??
3095
- attemptMeta.agentEngine ??
3096
- "unknown",
3097
- model: attemptMeta.agentModel,
3098
- }));
3150
+ try {
3151
+ result = await runPromisePreservingFailure(withSmithersSpan(smithersSpanNames.agent, Effect.tryPromise({
3152
+ try: () => {
3153
+ const agentCall = guidedResumeMessages?.length
3154
+ ? {
3155
+ messages: guidedResumeMessages,
3156
+ }
3157
+ : {
3158
+ prompt: effectivePrompt,
3159
+ };
3160
+ return effectiveAgent.generate({
3161
+ options: undefined,
3162
+ abortSignal: taskSignal,
3163
+ ...agentCall,
3164
+ resumeSession,
3165
+ lastHeartbeat: previousHeartbeat,
3166
+ rootDir: taskRoot,
3167
+ maxOutputBytes: toolConfig.maxOutputBytes,
3168
+ timeout: desc.timeoutMs
3169
+ ? { totalMs: desc.timeoutMs }
3170
+ : undefined,
3171
+ onStdout: (text) => {
3172
+ recordInternalHeartbeat();
3173
+ emitOutput(text, "stdout");
3174
+ traceCollector?.onStdout(text);
3175
+ },
3176
+ onStderr: (text) => {
3177
+ recordInternalHeartbeat();
3178
+ emitOutput(text, "stderr");
3179
+ traceCollector?.onStderr(text);
3180
+ },
3181
+ onEvent: handleAgentEvent,
3182
+ onStepFinish: handleSdkStepFinish,
3183
+ outputSchema: desc.outputSchema,
3184
+ });
3185
+ },
3186
+ catch: (error) => error,
3187
+ }), {
3188
+ ...taskSpanContext,
3189
+ agent: attemptMeta.agentId ??
3190
+ attemptMeta.agentEngine ??
3191
+ "unknown",
3192
+ model: attemptMeta.agentModel,
3193
+ }));
3194
+ }
3195
+ finally {
3196
+ if (hijackPollingInterval) {
3197
+ clearInterval(hijackPollingInterval);
3198
+ }
3199
+ }
3099
3200
  }
3100
- finally {
3101
- if (hijackPollingInterval) {
3102
- clearInterval(hijackPollingInterval);
3201
+ catch (error) {
3202
+ const errorDetails = {
3203
+ attempt: attemptNo,
3204
+ iteration: desc.iteration,
3205
+ };
3206
+ const effectiveError = supportsNativeStructuredOutput && desc.outputSchema && isStructuredOutputParseFailure(error)
3207
+ ? makeStructuredOutputCompatibilityError(desc, error, errorDetails)
3208
+ : error;
3209
+ if (traceCollector) {
3210
+ traceCollector.observeError(effectiveError);
3211
+ try { await traceCollector.flush(); }
3212
+ catch { /* trace flush failures must not mask the original error */ }
3103
3213
  }
3214
+ throw effectiveError;
3215
+ }
3216
+ if (traceCollector) {
3217
+ traceCollector.observeResult(result);
3218
+ await traceCollector.flush();
3104
3219
  }
3105
3220
  agentResult = result;
3106
3221
  if (!conversationMessages) {
@@ -3163,8 +3278,9 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3163
3278
  output = result.output;
3164
3279
  }
3165
3280
  }
3166
- catch {
3167
- // Structured output access threw
3281
+ catch (error) {
3282
+ structuredOutputAccessError = error;
3283
+ // Structured output access threw; text parsing below may still recover.
3168
3284
  }
3169
3285
  // Fall back to parsing text/steps for JSON
3170
3286
  if (output === undefined) {
@@ -3389,6 +3505,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3389
3505
  if (output === undefined) {
3390
3506
  // Debug: log what we have
3391
3507
  const finishReason = result.finishReason ?? "unknown";
3508
+ const debugSteps = result.steps ?? [];
3392
3509
  logDebug("agent response did not contain valid JSON output", {
3393
3510
  runId,
3394
3511
  nodeId: desc.nodeId,
@@ -3406,6 +3523,16 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3406
3523
  const tailHint = tail
3407
3524
  ? ` Last 200 chars of response: ${JSON.stringify(tail)}`
3408
3525
  : " Agent returned an empty response.";
3526
+ const errorDetails = {
3527
+ attempt: attemptNo,
3528
+ iteration: desc.iteration,
3529
+ };
3530
+ if (supportsNativeStructuredOutput && structuredOutputAccessError) {
3531
+ throw makeStructuredOutputCompatibilityError(desc, structuredOutputAccessError, errorDetails);
3532
+ }
3533
+ if (text.trim()) {
3534
+ throw makePlainTextOutputError(desc, text, undefined, errorDetails);
3535
+ }
3409
3536
  throw new SmithersError("INVALID_OUTPUT", `No valid JSON output found in agent response (finishReason=${finishReason}, textLength=${text.length}).${tailHint}`);
3410
3537
  }
3411
3538
  }
@@ -3414,8 +3541,11 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3414
3541
  try {
3415
3542
  payload = JSON.parse(output);
3416
3543
  }
3417
- catch {
3418
- throw new SmithersError("INVALID_OUTPUT", `Failed to parse agent output as JSON. Output starts with: "${output.slice(0, 100)}"`);
3544
+ catch (error) {
3545
+ throw makePlainTextOutputError(desc, output, error, {
3546
+ attempt: attemptNo,
3547
+ iteration: desc.iteration,
3548
+ });
3419
3549
  }
3420
3550
  }
3421
3551
  else {
@@ -3469,16 +3599,32 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3469
3599
  * @param {unknown} cause
3470
3600
  * @param {number} schemaRetryAttempts
3471
3601
  */
3472
- const toInvalidOutputError = (cause, schemaRetryAttempts) => new SmithersError("INVALID_OUTPUT", `Task output failed validation for ${desc.outputTableName}`, {
3473
- attempt: attemptNo,
3474
- nodeId: desc.nodeId,
3475
- iteration: desc.iteration,
3476
- outputTable: desc.outputTableName,
3477
- schemaRetryAttempts,
3478
- issues: cause && typeof cause === "object" && "issues" in cause
3479
- ? cause.issues
3480
- : undefined,
3481
- }, { cause });
3602
+ const toInvalidOutputError = (cause, schemaRetryAttempts) => {
3603
+ if (supportsNativeStructuredOutput && structuredOutputAccessError) {
3604
+ return makeStructuredOutputCompatibilityError(desc, structuredOutputAccessError, {
3605
+ attempt: attemptNo,
3606
+ iteration: desc.iteration,
3607
+ schemaRetryAttempts,
3608
+ });
3609
+ }
3610
+ if (typeof payload === "string") {
3611
+ return makePlainTextOutputError(desc, payload, cause, {
3612
+ attempt: attemptNo,
3613
+ iteration: desc.iteration,
3614
+ schemaRetryAttempts,
3615
+ });
3616
+ }
3617
+ return new SmithersError("INVALID_OUTPUT", `Task output failed validation for ${desc.outputTableName}`, {
3618
+ attempt: attemptNo,
3619
+ nodeId: desc.nodeId,
3620
+ iteration: desc.iteration,
3621
+ outputTable: desc.outputTableName,
3622
+ schemaRetryAttempts,
3623
+ issues: cause && typeof cause === "object" && "issues" in cause
3624
+ ? cause.issues
3625
+ : undefined,
3626
+ }, { cause });
3627
+ };
3482
3628
  // Schema-validation retry: if the agent returned parseable JSON but it
3483
3629
  // doesn't match the Zod schema, resume the SAME agent conversation with
3484
3630
  // the validation error up to 3 times before giving up. These attempts
@@ -3511,6 +3657,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3511
3657
  }
3512
3658
  while (!validation.ok && desc.agent && schemaRetry < MAX_SCHEMA_RETRIES) {
3513
3659
  schemaRetry++;
3660
+ structuredOutputAccessError = undefined;
3514
3661
  const schemaDesc = describeSchemaShape(desc.outputTable, desc.outputSchema);
3515
3662
  const zodIssues = validation.error?.issues
3516
3663
  ?.map((iss) => ` - ${(iss.path ?? []).join(".")}: ${iss.message}`)
@@ -3598,7 +3745,8 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3598
3745
  retryOutput = schemaRetryResult.output;
3599
3746
  }
3600
3747
  }
3601
- catch {
3748
+ catch (error) {
3749
+ structuredOutputAccessError = error;
3602
3750
  // Structured output access threw; fall back to text parsing.
3603
3751
  }
3604
3752
  }
@@ -4280,6 +4428,12 @@ async function runWorkflowBodyDriver(workflow, opts) {
4280
4428
  allowNetwork,
4281
4429
  maxOutputBytes,
4282
4430
  toolTimeoutMs,
4431
+ traceContext: {
4432
+ workflowPath: resolvedWorkflowPath ?? opts.workflowPath ?? null,
4433
+ workflowHash: runMetadata.workflowHash ?? null,
4434
+ logDir: logDir ?? undefined,
4435
+ annotations: opts.annotations,
4436
+ },
4283
4437
  };
4284
4438
  let frameNo = ((await adapter.getLastFrame(runId))?.frameNo ?? 0);
4285
4439
  let defaultIteration = 0;
@@ -5665,6 +5819,12 @@ async function runWorkflowBodyLegacy(workflow, opts) {
5665
5819
  allowNetwork,
5666
5820
  maxOutputBytes,
5667
5821
  toolTimeoutMs,
5822
+ traceContext: {
5823
+ workflowPath: resolvedWorkflowPath ?? opts.workflowPath ?? null,
5824
+ workflowHash: runMetadata.workflowHash ?? null,
5825
+ logDir: logDir ?? undefined,
5826
+ annotations: opts.annotations,
5827
+ },
5668
5828
  };
5669
5829
  const schedulerExecutionConcurrency = Math.max(1, maxConcurrency);
5670
5830
  /**