@smithers-orchestrator/engine 0.20.0 → 0.20.3
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 +15 -15
- package/src/AgentTraceCollector.js +716 -0
- package/src/AgentTraceCollectorOptions.ts +17 -0
- package/src/engine.js +219 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/engine",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
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.
|
|
37
|
-
"@smithers-orchestrator/
|
|
38
|
-
"@smithers-orchestrator/components": "0.20.
|
|
39
|
-
"@smithers-orchestrator/
|
|
40
|
-
"@smithers-orchestrator/graph": "0.20.
|
|
41
|
-
"@smithers-orchestrator/memory": "0.20.
|
|
42
|
-
"@smithers-orchestrator/
|
|
43
|
-
"@smithers-orchestrator/
|
|
44
|
-
"@smithers-orchestrator/
|
|
45
|
-
"@smithers-orchestrator/
|
|
46
|
-
"@smithers-orchestrator/
|
|
47
|
-
"@smithers-orchestrator/
|
|
48
|
-
"@smithers-orchestrator/vcs": "0.20.
|
|
49
|
-
"@smithers-orchestrator/
|
|
36
|
+
"@smithers-orchestrator/agents": "0.20.3",
|
|
37
|
+
"@smithers-orchestrator/db": "0.20.3",
|
|
38
|
+
"@smithers-orchestrator/components": "0.20.3",
|
|
39
|
+
"@smithers-orchestrator/errors": "0.20.3",
|
|
40
|
+
"@smithers-orchestrator/graph": "0.20.3",
|
|
41
|
+
"@smithers-orchestrator/memory": "0.20.3",
|
|
42
|
+
"@smithers-orchestrator/react-reconciler": "0.20.3",
|
|
43
|
+
"@smithers-orchestrator/sandbox": "0.20.3",
|
|
44
|
+
"@smithers-orchestrator/scheduler": "0.20.3",
|
|
45
|
+
"@smithers-orchestrator/scorers": "0.20.3",
|
|
46
|
+
"@smithers-orchestrator/observability": "0.20.3",
|
|
47
|
+
"@smithers-orchestrator/time-travel": "0.20.3",
|
|
48
|
+
"@smithers-orchestrator/vcs": "0.20.3",
|
|
49
|
+
"@smithers-orchestrator/driver": "0.20.3"
|
|
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
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
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
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
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
|
/**
|