@smithers-orchestrator/cli 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +55 -0
- package/src/AgentAvailability.ts +13 -0
- package/src/AgentAvailabilityStatus.ts +5 -0
- package/src/AggregateNodeDetailParams.ts +5 -0
- package/src/AskOptions.ts +12 -0
- package/src/ChatAttemptMeta.ts +7 -0
- package/src/ChatAttemptRow.ts +12 -0
- package/src/ChatOutputEvent.ts +6 -0
- package/src/DiffBundleLike.ts +6 -0
- package/src/DiscoveredWorkflow.ts +9 -0
- package/src/EnrichedNodeDetail.ts +60 -0
- package/src/EventCategory.ts +18 -0
- package/src/FindDbWaitOptions.ts +4 -0
- package/src/FormatEventLineOptions.ts +4 -0
- package/src/HijackCandidate.ts +11 -0
- package/src/HijackLaunchSpec.ts +6 -0
- package/src/InitWorkflowPackOptions.ts +4 -0
- package/src/InitWorkflowPackResult.ts +6 -0
- package/src/NativeHijackEngine.ts +8 -0
- package/src/NodeDetailAttempt.ts +22 -0
- package/src/NodeDetailTokenUsage.ts +11 -0
- package/src/NodeDetailToolCall.ts +12 -0
- package/src/ParsedNodeOutputEvent.ts +9 -0
- package/src/RenderNodeDetailOptions.ts +4 -0
- package/src/RunAutoResumeSkipReason.ts +4 -0
- package/src/RunDiffCommandInput.ts +13 -0
- package/src/RunDiffCommandResult.ts +3 -0
- package/src/RunOutputCommandInput.ts +12 -0
- package/src/RunOutputCommandResult.ts +3 -0
- package/src/RunRewindCommandInput.ts +14 -0
- package/src/RunRewindCommandResult.ts +3 -0
- package/src/RunTreeCommandInput.ts +14 -0
- package/src/RunTreeCommandResult.ts +3 -0
- package/src/SmithersEventType.ts +3 -0
- package/src/SupervisorOptions.ts +33 -0
- package/src/SupervisorPollSummary.ts +6 -0
- package/src/TreeRenderOptions.ts +5 -0
- package/src/WatchLoopOptions.ts +9 -0
- package/src/WatchLoopResult.ts +8 -0
- package/src/WatchRenderContext.ts +4 -0
- package/src/WhyBlocker.ts +17 -0
- package/src/WhyBlockerKind.ts +9 -0
- package/src/WhyDiagnosis.ts +10 -0
- package/src/WorkflowCta.ts +4 -0
- package/src/WorkflowSourceType.ts +1 -0
- package/src/agent-detection.js +257 -0
- package/src/ask.js +491 -0
- package/src/chat.js +226 -0
- package/src/diff.js +221 -0
- package/src/event-categories.js +141 -0
- package/src/find-db.js +93 -0
- package/src/format.js +272 -0
- package/src/hijack-session.js +207 -0
- package/src/hijack.js +226 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +4868 -0
- package/src/mcp/SemanticMcpServerOptions.ts +4 -0
- package/src/mcp/SemanticToolCallResult.ts +14 -0
- package/src/mcp/SemanticToolContext.ts +6 -0
- package/src/mcp/SemanticToolDefinition.ts +13 -0
- package/src/mcp/SemanticToolError.ts +6 -0
- package/src/mcp/semantic-server.js +41 -0
- package/src/mcp/semantic-tools.js +1242 -0
- package/src/node-detail.js +682 -0
- package/src/output.js +111 -0
- package/src/resume-detached.js +37 -0
- package/src/rewind.js +88 -0
- package/src/scheduler.js +112 -0
- package/src/smithersRuntime.js +63 -0
- package/src/supervisor.js +418 -0
- package/src/tree.js +307 -0
- package/src/tui/app.jsx +139 -0
- package/src/tui/app.tsx +5 -0
- package/src/tui/components/AskModal.jsx +109 -0
- package/src/tui/components/AskModal.tsx +3 -0
- package/src/tui/components/AttentionPane.jsx +112 -0
- package/src/tui/components/AttentionPane.tsx +6 -0
- package/src/tui/components/ChatPane.jsx +57 -0
- package/src/tui/components/ChatPane.tsx +7 -0
- package/src/tui/components/CronList.jsx +87 -0
- package/src/tui/components/CronList.tsx +5 -0
- package/src/tui/components/DetailsPane.jsx +96 -0
- package/src/tui/components/DetailsPane.tsx +7 -0
- package/src/tui/components/FramesPane.jsx +147 -0
- package/src/tui/components/FramesPane.tsx +8 -0
- package/src/tui/components/LogsPane.jsx +46 -0
- package/src/tui/components/LogsPane.tsx +6 -0
- package/src/tui/components/MetricsPane.jsx +108 -0
- package/src/tui/components/MetricsPane.tsx +5 -0
- package/src/tui/components/NodeDetailView.jsx +284 -0
- package/src/tui/components/NodeDetailView.tsx +7 -0
- package/src/tui/components/NodeInspector.jsx +51 -0
- package/src/tui/components/NodeInspector.tsx +7 -0
- package/src/tui/components/RunDetailView.jsx +190 -0
- package/src/tui/components/RunDetailView.tsx +7 -0
- package/src/tui/components/RunsList.jsx +184 -0
- package/src/tui/components/RunsList.tsx +7 -0
- package/src/tui/components/SqliteBrowser.jsx +131 -0
- package/src/tui/components/SqliteBrowser.tsx +5 -0
- package/src/tui/components/WorkflowLauncher.jsx +63 -0
- package/src/tui/components/WorkflowLauncher.tsx +3 -0
- package/src/util/CliErrorMapping.ts +7 -0
- package/src/util/CliExitCode.ts +10 -0
- package/src/util/errorMessage.js +212 -0
- package/src/util/exitCodes.js +18 -0
- package/src/watch.js +128 -0
- package/src/why-diagnosis.js +1000 -0
- package/src/workflow-pack.js +2151 -0
- package/src/workflows.js +122 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SmithersError } from "@smithers-orchestrator/errors";
|
|
3
|
+
/** @typedef {import("./AggregateNodeDetailParams.ts").AggregateNodeDetailParams} AggregateNodeDetailParams */
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{ total: number; failed: number; cancelled: number; succeeded: number; waiting: number; }} AttemptSummary
|
|
6
|
+
*/
|
|
7
|
+
/** @typedef {import("./EnrichedNodeDetail.ts").EnrichedNodeDetail} EnrichedNodeDetail */
|
|
8
|
+
/** @typedef {import("./NodeDetailAttempt.ts").NodeDetailAttempt} NodeDetailAttempt */
|
|
9
|
+
/** @typedef {import("./NodeDetailTokenUsage.ts").NodeDetailTokenUsage} NodeDetailTokenUsage */
|
|
10
|
+
/** @typedef {import("./NodeDetailToolCall.ts").NodeDetailToolCall} NodeDetailToolCall */
|
|
11
|
+
/** @typedef {import("./RenderNodeDetailOptions.ts").RenderNodeDetailOptions} RenderNodeDetailOptions */
|
|
12
|
+
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
13
|
+
|
|
14
|
+
const MAX_TOOL_PAYLOAD_BYTES_HUMAN = 1024;
|
|
15
|
+
const MAX_VALIDATED_OUTPUT_BYTES_HUMAN = 10 * 1024;
|
|
16
|
+
const DEFAULT_EXPANDED_ATTEMPT_LIMIT = 5;
|
|
17
|
+
/**
|
|
18
|
+
* @returns {NodeDetailTokenUsage}
|
|
19
|
+
*/
|
|
20
|
+
const emptyTokenUsage = () => ({
|
|
21
|
+
inputTokens: 0,
|
|
22
|
+
outputTokens: 0,
|
|
23
|
+
cacheReadTokens: 0,
|
|
24
|
+
cacheWriteTokens: 0,
|
|
25
|
+
reasoningTokens: 0,
|
|
26
|
+
costUsd: null,
|
|
27
|
+
eventCount: 0,
|
|
28
|
+
models: [],
|
|
29
|
+
agents: [],
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* @param {unknown} value
|
|
33
|
+
* @returns {number | null}
|
|
34
|
+
*/
|
|
35
|
+
function asNumber(value) {
|
|
36
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
37
|
+
return value;
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
const parsed = Number(value);
|
|
40
|
+
if (Number.isFinite(parsed))
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @param {number | null} startedAtMs
|
|
47
|
+
* @param {number | null} finishedAtMs
|
|
48
|
+
*/
|
|
49
|
+
function normalizeDurationMs(startedAtMs, finishedAtMs) {
|
|
50
|
+
if (startedAtMs == null || finishedAtMs == null)
|
|
51
|
+
return null;
|
|
52
|
+
const duration = finishedAtMs - startedAtMs;
|
|
53
|
+
return duration >= 0 ? duration : null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* @param {string | null | undefined} raw
|
|
57
|
+
* @returns {unknown | null}
|
|
58
|
+
*/
|
|
59
|
+
function parseJsonValue(raw) {
|
|
60
|
+
if (!raw)
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return raw;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @param {string | null | undefined} raw
|
|
71
|
+
*/
|
|
72
|
+
function parseErrorSummary(raw) {
|
|
73
|
+
if (!raw)
|
|
74
|
+
return { message: null, detail: null };
|
|
75
|
+
const parsed = parseJsonValue(raw);
|
|
76
|
+
if (typeof parsed === "string") {
|
|
77
|
+
return { message: parsed, detail: parsed };
|
|
78
|
+
}
|
|
79
|
+
if (parsed && typeof parsed === "object") {
|
|
80
|
+
const name = typeof parsed.name === "string"
|
|
81
|
+
? parsed.name
|
|
82
|
+
: null;
|
|
83
|
+
const message = typeof parsed.message === "string"
|
|
84
|
+
? parsed.message
|
|
85
|
+
: null;
|
|
86
|
+
if (name && message) {
|
|
87
|
+
return { message: `${name}: ${message}`, detail: parsed };
|
|
88
|
+
}
|
|
89
|
+
if (message) {
|
|
90
|
+
return { message, detail: parsed };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return { message: JSON.stringify(parsed), detail: parsed };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { message: String(parsed), detail: parsed };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { message: String(parsed), detail: parsed };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* @param {DbEventRow} row
|
|
103
|
+
* @param {{ nodeId: string; iteration: number }} params
|
|
104
|
+
* @returns {ParsedTokenUsageEvent | null}
|
|
105
|
+
*/
|
|
106
|
+
function parseTokenUsageEvent(row, params) {
|
|
107
|
+
if (row.type !== "TokenUsageReported")
|
|
108
|
+
return null;
|
|
109
|
+
const payload = parseJsonValue(row.payloadJson);
|
|
110
|
+
if (!payload || typeof payload !== "object")
|
|
111
|
+
return null;
|
|
112
|
+
const entry = payload;
|
|
113
|
+
if (String(entry.nodeId ?? "") !== params.nodeId)
|
|
114
|
+
return null;
|
|
115
|
+
const iteration = asNumber(entry.iteration);
|
|
116
|
+
if (iteration == null || Math.trunc(iteration) !== params.iteration)
|
|
117
|
+
return null;
|
|
118
|
+
const attempt = asNumber(entry.attempt);
|
|
119
|
+
if (attempt == null)
|
|
120
|
+
return null;
|
|
121
|
+
const model = typeof entry.model === "string" ? entry.model : null;
|
|
122
|
+
const agent = typeof entry.agent === "string" ? entry.agent : null;
|
|
123
|
+
const inputTokens = asNumber(entry.inputTokens) ?? 0;
|
|
124
|
+
const outputTokens = asNumber(entry.outputTokens) ?? 0;
|
|
125
|
+
const cacheReadTokens = asNumber(entry.cacheReadTokens) ?? 0;
|
|
126
|
+
const cacheWriteTokens = asNumber(entry.cacheWriteTokens) ?? 0;
|
|
127
|
+
const reasoningTokens = asNumber(entry.reasoningTokens) ?? 0;
|
|
128
|
+
const costUsd = asNumber(entry.costUsd) ?? asNumber(entry.cost);
|
|
129
|
+
return {
|
|
130
|
+
attempt: Math.trunc(attempt),
|
|
131
|
+
model,
|
|
132
|
+
agent,
|
|
133
|
+
inputTokens,
|
|
134
|
+
outputTokens,
|
|
135
|
+
cacheReadTokens,
|
|
136
|
+
cacheWriteTokens,
|
|
137
|
+
reasoningTokens,
|
|
138
|
+
costUsd,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @param {NodeDetailTokenUsage} current
|
|
143
|
+
* @param {ParsedTokenUsageEvent} event
|
|
144
|
+
* @returns {NodeDetailTokenUsage}
|
|
145
|
+
*/
|
|
146
|
+
function mergeTokenUsage(current, event) {
|
|
147
|
+
const nextModels = new Set(current.models);
|
|
148
|
+
if (event.model)
|
|
149
|
+
nextModels.add(event.model);
|
|
150
|
+
const nextAgents = new Set(current.agents);
|
|
151
|
+
if (event.agent)
|
|
152
|
+
nextAgents.add(event.agent);
|
|
153
|
+
const mergedCost = current.costUsd == null && event.costUsd == null
|
|
154
|
+
? null
|
|
155
|
+
: (current.costUsd ?? 0) + (event.costUsd ?? 0);
|
|
156
|
+
return {
|
|
157
|
+
inputTokens: current.inputTokens + event.inputTokens,
|
|
158
|
+
outputTokens: current.outputTokens + event.outputTokens,
|
|
159
|
+
cacheReadTokens: current.cacheReadTokens + event.cacheReadTokens,
|
|
160
|
+
cacheWriteTokens: current.cacheWriteTokens + event.cacheWriteTokens,
|
|
161
|
+
reasoningTokens: current.reasoningTokens + event.reasoningTokens,
|
|
162
|
+
costUsd: mergedCost,
|
|
163
|
+
eventCount: current.eventCount + 1,
|
|
164
|
+
models: [...nextModels],
|
|
165
|
+
agents: [...nextAgents],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* @param {DbAttemptRow[]} attempts
|
|
170
|
+
* @returns {AttemptSummary}
|
|
171
|
+
*/
|
|
172
|
+
function summarizeAttempts(attempts) {
|
|
173
|
+
let failed = 0;
|
|
174
|
+
let cancelled = 0;
|
|
175
|
+
let succeeded = 0;
|
|
176
|
+
let waiting = 0;
|
|
177
|
+
for (const attempt of attempts) {
|
|
178
|
+
if (attempt.state === "failed")
|
|
179
|
+
failed += 1;
|
|
180
|
+
else if (attempt.state === "cancelled")
|
|
181
|
+
cancelled += 1;
|
|
182
|
+
else if (attempt.state === "finished")
|
|
183
|
+
succeeded += 1;
|
|
184
|
+
else
|
|
185
|
+
waiting += 1;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
total: attempts.length,
|
|
189
|
+
failed,
|
|
190
|
+
cancelled,
|
|
191
|
+
succeeded,
|
|
192
|
+
waiting,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* @param {DbAttemptRow[]} attempts
|
|
197
|
+
*/
|
|
198
|
+
function computeNodeDurationMs(attempts) {
|
|
199
|
+
if (attempts.length === 0)
|
|
200
|
+
return null;
|
|
201
|
+
let minStart = null;
|
|
202
|
+
let maxFinish = null;
|
|
203
|
+
for (const attempt of attempts) {
|
|
204
|
+
const startedAtMs = asNumber(attempt.startedAtMs);
|
|
205
|
+
if (startedAtMs != null) {
|
|
206
|
+
minStart = minStart == null ? startedAtMs : Math.min(minStart, startedAtMs);
|
|
207
|
+
}
|
|
208
|
+
const finishedAtMs = asNumber(attempt.finishedAtMs);
|
|
209
|
+
if (finishedAtMs != null) {
|
|
210
|
+
maxFinish = maxFinish == null ? finishedAtMs : Math.max(maxFinish, finishedAtMs);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return normalizeDurationMs(minStart, maxFinish);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* @param {Record<string, unknown> | null} row
|
|
217
|
+
* @returns {unknown | null}
|
|
218
|
+
*/
|
|
219
|
+
function normalizeRawOutput(row) {
|
|
220
|
+
if (!row || typeof row !== "object")
|
|
221
|
+
return null;
|
|
222
|
+
const clone = { ...row };
|
|
223
|
+
delete clone.run_id;
|
|
224
|
+
delete clone.node_id;
|
|
225
|
+
delete clone.iteration;
|
|
226
|
+
for (const [key, value] of Object.entries(clone)) {
|
|
227
|
+
if (typeof value === "string" &&
|
|
228
|
+
(value.trimStart().startsWith("{") || value.trimStart().startsWith("["))) {
|
|
229
|
+
clone[key] = parseJsonValue(value);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return clone;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* @param {unknown} value
|
|
236
|
+
*/
|
|
237
|
+
function deepJsonString(value) {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.stringify(value);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* @param {unknown | null} rawOutput
|
|
247
|
+
* @param {DbCacheRow[]} cacheRows
|
|
248
|
+
*/
|
|
249
|
+
function pickValidatedOutput(rawOutput, cacheRows) {
|
|
250
|
+
const parsedCache = cacheRows
|
|
251
|
+
.map((row) => ({
|
|
252
|
+
cacheKey: row.cacheKey,
|
|
253
|
+
payload: parseJsonValue(row.payloadJson),
|
|
254
|
+
}))
|
|
255
|
+
.filter((row) => row.payload !== null);
|
|
256
|
+
const rawEncoded = deepJsonString(rawOutput);
|
|
257
|
+
if (rawEncoded != null) {
|
|
258
|
+
for (const candidate of parsedCache) {
|
|
259
|
+
if (deepJsonString(candidate.payload) === rawEncoded) {
|
|
260
|
+
return {
|
|
261
|
+
validated: candidate.payload,
|
|
262
|
+
source: "cache",
|
|
263
|
+
cacheKey: candidate.cacheKey,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (parsedCache.length > 0) {
|
|
269
|
+
return {
|
|
270
|
+
validated: parsedCache[0].payload,
|
|
271
|
+
source: "cache",
|
|
272
|
+
cacheKey: parsedCache[0].cacheKey,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (rawOutput != null) {
|
|
276
|
+
return {
|
|
277
|
+
validated: rawOutput,
|
|
278
|
+
source: "output-table",
|
|
279
|
+
cacheKey: null,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
validated: null,
|
|
284
|
+
source: "none",
|
|
285
|
+
cacheKey: null,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* @param {number | null} ms
|
|
290
|
+
*/
|
|
291
|
+
function formatDuration(ms) {
|
|
292
|
+
if (ms == null)
|
|
293
|
+
return "—";
|
|
294
|
+
if (ms < 1000)
|
|
295
|
+
return `${Math.round(ms)}ms`;
|
|
296
|
+
const seconds = ms / 1000;
|
|
297
|
+
return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* @param {number} value
|
|
301
|
+
*/
|
|
302
|
+
function formatCount(value) {
|
|
303
|
+
return value.toLocaleString("en-US");
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* @param {number} value
|
|
307
|
+
*/
|
|
308
|
+
function formatScore(value) {
|
|
309
|
+
const rounded = value.toFixed(3);
|
|
310
|
+
return rounded.replace(/\.?0+$/, "");
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* @param {number | null} value
|
|
314
|
+
*/
|
|
315
|
+
function formatCostUsd(value) {
|
|
316
|
+
if (value == null)
|
|
317
|
+
return null;
|
|
318
|
+
return value.toFixed(4);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* @param {NodeDetailToolCall} tool
|
|
322
|
+
*/
|
|
323
|
+
function describeToolResult(tool) {
|
|
324
|
+
if (tool.status !== "success") {
|
|
325
|
+
return tool.error ? `${tool.status}: ${tool.error}` : tool.status;
|
|
326
|
+
}
|
|
327
|
+
const output = tool.output;
|
|
328
|
+
if (output == null)
|
|
329
|
+
return "ok";
|
|
330
|
+
if (Array.isArray(output))
|
|
331
|
+
return `${output.length} items`;
|
|
332
|
+
if (typeof output === "object") {
|
|
333
|
+
const record = output;
|
|
334
|
+
const results = record.results;
|
|
335
|
+
if (Array.isArray(results))
|
|
336
|
+
return `${results.length} results`;
|
|
337
|
+
if (typeof record.ok === "boolean")
|
|
338
|
+
return record.ok ? "ok" : "failed";
|
|
339
|
+
const keys = Object.keys(record);
|
|
340
|
+
return keys.length === 0 ? "ok" : `${keys.length} fields`;
|
|
341
|
+
}
|
|
342
|
+
if (typeof output === "string") {
|
|
343
|
+
const compact = output.replace(/\s+/g, " ").trim();
|
|
344
|
+
if (!compact)
|
|
345
|
+
return "ok";
|
|
346
|
+
return compact.length > 72 ? `${compact.slice(0, 69)}...` : compact;
|
|
347
|
+
}
|
|
348
|
+
return String(output);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* @param {unknown} payload
|
|
352
|
+
* @returns {string}
|
|
353
|
+
*/
|
|
354
|
+
function stringifyForHuman(payload) {
|
|
355
|
+
if (typeof payload === "string")
|
|
356
|
+
return payload;
|
|
357
|
+
try {
|
|
358
|
+
return JSON.stringify(payload, null, 2);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return String(payload);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* @param {string} text
|
|
366
|
+
* @param {number} maxBytes
|
|
367
|
+
*/
|
|
368
|
+
function truncateForHuman(text, maxBytes) {
|
|
369
|
+
const suffix = "... (truncated, use --json for full output)";
|
|
370
|
+
const bytes = Buffer.from(text, "utf8");
|
|
371
|
+
if (bytes.byteLength <= maxBytes) {
|
|
372
|
+
return text;
|
|
373
|
+
}
|
|
374
|
+
const clippedBytes = bytes.slice(0, maxBytes);
|
|
375
|
+
const clipped = clippedBytes.toString("utf8");
|
|
376
|
+
return `${clipped}${suffix}`;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* @param {string[]} lines
|
|
380
|
+
* @param {string} prefix
|
|
381
|
+
* @param {string} text
|
|
382
|
+
* @param {string} blockIndent
|
|
383
|
+
*/
|
|
384
|
+
function appendPrefixedBlock(lines, prefix, text, blockIndent) {
|
|
385
|
+
if (!text.includes("\n")) {
|
|
386
|
+
lines.push(`${prefix} ${text}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
lines.push(prefix);
|
|
390
|
+
for (const line of text.split(/\r?\n/)) {
|
|
391
|
+
lines.push(`${blockIndent}${line}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* @param {SmithersDb} adapter
|
|
396
|
+
* @param {AggregateNodeDetailParams} params
|
|
397
|
+
* @returns {Effect.Effect<EnrichedNodeDetail, SmithersError>}
|
|
398
|
+
*/
|
|
399
|
+
export function aggregateNodeDetailEffect(adapter, params) {
|
|
400
|
+
return Effect.gen(function* () {
|
|
401
|
+
const nodeRows = (yield* adapter.listNodeIterationsEffect(params.runId, params.nodeId));
|
|
402
|
+
if (nodeRows.length === 0) {
|
|
403
|
+
return yield* Effect.fail(new SmithersError("NODE_NOT_FOUND", `Node not found: ${params.nodeId}`, {
|
|
404
|
+
runId: params.runId,
|
|
405
|
+
nodeId: params.nodeId,
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
const resolvedIteration = params.iteration ?? Math.max(...nodeRows.map((row) => row.iteration));
|
|
409
|
+
const node = nodeRows.find((row) => row.iteration === resolvedIteration);
|
|
410
|
+
if (!node) {
|
|
411
|
+
return yield* Effect.fail(new SmithersError("NODE_NOT_FOUND", `Node not found: ${params.nodeId} (iteration ${resolvedIteration})`, {
|
|
412
|
+
runId: params.runId,
|
|
413
|
+
nodeId: params.nodeId,
|
|
414
|
+
iteration: resolvedIteration,
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
const [attemptRows, toolCallRows, tokenEventRows, scorerRows, rawOutputRow, cacheRows] = yield* Effect.all([
|
|
418
|
+
adapter.listAttemptsEffect(params.runId, params.nodeId, resolvedIteration),
|
|
419
|
+
adapter.listToolCallsEffect(params.runId, params.nodeId, resolvedIteration),
|
|
420
|
+
adapter.listEventsByTypeEffect(params.runId, "TokenUsageReported"),
|
|
421
|
+
adapter.listScorerResultsEffect(params.runId, params.nodeId),
|
|
422
|
+
node.outputTable
|
|
423
|
+
? adapter.getRawNodeOutputForIterationEffect(node.outputTable, params.runId, params.nodeId, resolvedIteration)
|
|
424
|
+
: Effect.succeed(null),
|
|
425
|
+
node.outputTable
|
|
426
|
+
? adapter.listCacheByNodeEffect(params.nodeId, node.outputTable, 20)
|
|
427
|
+
: Effect.succeed([]),
|
|
428
|
+
]);
|
|
429
|
+
const attemptsDesc = attemptRows;
|
|
430
|
+
const attempts = [...attemptsDesc].sort((left, right) => left.attempt - right.attempt);
|
|
431
|
+
const toolCallsRaw = toolCallRows;
|
|
432
|
+
const eventsRaw = tokenEventRows;
|
|
433
|
+
const scorerRowsFiltered = scorerRows
|
|
434
|
+
.filter((row) => row.iteration === resolvedIteration)
|
|
435
|
+
.sort((left, right) => left.scoredAtMs - right.scoredAtMs);
|
|
436
|
+
const cacheRowsTyped = cacheRows;
|
|
437
|
+
yield* Effect.logDebug("aggregated node detail").pipe(Effect.annotateLogs({
|
|
438
|
+
runId: params.runId,
|
|
439
|
+
nodeId: params.nodeId,
|
|
440
|
+
iteration: resolvedIteration,
|
|
441
|
+
attemptCount: attempts.length,
|
|
442
|
+
toolCallCount: toolCallsRaw.length,
|
|
443
|
+
}));
|
|
444
|
+
const tokenByAttempt = new Map();
|
|
445
|
+
for (const eventRow of eventsRaw) {
|
|
446
|
+
const parsed = parseTokenUsageEvent(eventRow, {
|
|
447
|
+
nodeId: params.nodeId,
|
|
448
|
+
iteration: resolvedIteration,
|
|
449
|
+
});
|
|
450
|
+
if (!parsed)
|
|
451
|
+
continue;
|
|
452
|
+
const existing = tokenByAttempt.get(parsed.attempt) ?? emptyTokenUsage();
|
|
453
|
+
tokenByAttempt.set(parsed.attempt, mergeTokenUsage(existing, parsed));
|
|
454
|
+
}
|
|
455
|
+
const toolCalls = toolCallsRaw.map((call) => {
|
|
456
|
+
const parsedError = parseErrorSummary(call.errorJson);
|
|
457
|
+
const finishedAtMs = asNumber(call.finishedAtMs);
|
|
458
|
+
return {
|
|
459
|
+
attempt: call.attempt,
|
|
460
|
+
seq: call.seq,
|
|
461
|
+
name: call.toolName,
|
|
462
|
+
status: call.status,
|
|
463
|
+
startedAtMs: call.startedAtMs,
|
|
464
|
+
finishedAtMs,
|
|
465
|
+
durationMs: normalizeDurationMs(asNumber(call.startedAtMs), finishedAtMs),
|
|
466
|
+
input: parseJsonValue(call.inputJson),
|
|
467
|
+
output: parseJsonValue(call.outputJson),
|
|
468
|
+
error: parsedError.message,
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
const toolCallsByAttempt = new Map();
|
|
472
|
+
for (const toolCall of toolCalls) {
|
|
473
|
+
const list = toolCallsByAttempt.get(toolCall.attempt) ?? [];
|
|
474
|
+
list.push(toolCall);
|
|
475
|
+
toolCallsByAttempt.set(toolCall.attempt, list);
|
|
476
|
+
}
|
|
477
|
+
const attemptsDetailed = attempts.map((attempt) => {
|
|
478
|
+
const parsedError = parseErrorSummary(attempt.errorJson);
|
|
479
|
+
const finishedAtMs = asNumber(attempt.finishedAtMs);
|
|
480
|
+
const usage = tokenByAttempt.get(attempt.attempt) ?? emptyTokenUsage();
|
|
481
|
+
return {
|
|
482
|
+
runId: attempt.runId,
|
|
483
|
+
nodeId: attempt.nodeId,
|
|
484
|
+
iteration: attempt.iteration,
|
|
485
|
+
attempt: attempt.attempt,
|
|
486
|
+
state: attempt.state,
|
|
487
|
+
startedAtMs: attempt.startedAtMs,
|
|
488
|
+
finishedAtMs,
|
|
489
|
+
durationMs: normalizeDurationMs(asNumber(attempt.startedAtMs), finishedAtMs),
|
|
490
|
+
error: parsedError.message,
|
|
491
|
+
errorDetail: parsedError.detail,
|
|
492
|
+
tokenUsage: usage,
|
|
493
|
+
toolCalls: toolCallsByAttempt.get(attempt.attempt)?.sort((left, right) => left.seq - right.seq) ?? [],
|
|
494
|
+
meta: parseJsonValue(attempt.metaJson),
|
|
495
|
+
responseText: attempt.responseText ?? null,
|
|
496
|
+
cached: Boolean(attempt.cached),
|
|
497
|
+
jjPointer: attempt.jjPointer ?? null,
|
|
498
|
+
jjCwd: attempt.jjCwd ?? null,
|
|
499
|
+
};
|
|
500
|
+
});
|
|
501
|
+
const totalUsage = attemptsDetailed.reduce((acc, attempt) => mergeTokenUsage(acc, {
|
|
502
|
+
attempt: attempt.attempt,
|
|
503
|
+
model: null,
|
|
504
|
+
agent: null,
|
|
505
|
+
inputTokens: attempt.tokenUsage.inputTokens,
|
|
506
|
+
outputTokens: attempt.tokenUsage.outputTokens,
|
|
507
|
+
cacheReadTokens: attempt.tokenUsage.cacheReadTokens,
|
|
508
|
+
cacheWriteTokens: attempt.tokenUsage.cacheWriteTokens,
|
|
509
|
+
reasoningTokens: attempt.tokenUsage.reasoningTokens,
|
|
510
|
+
costUsd: attempt.tokenUsage.costUsd,
|
|
511
|
+
}), emptyTokenUsage());
|
|
512
|
+
const tokenUsage = {
|
|
513
|
+
...totalUsage,
|
|
514
|
+
byAttempt: attemptsDetailed.map((attempt) => ({
|
|
515
|
+
attempt: attempt.attempt,
|
|
516
|
+
usage: attempt.tokenUsage,
|
|
517
|
+
})),
|
|
518
|
+
};
|
|
519
|
+
const rawOutput = normalizeRawOutput(rawOutputRow ?? null);
|
|
520
|
+
const validatedOutput = pickValidatedOutput(rawOutput, cacheRowsTyped);
|
|
521
|
+
return {
|
|
522
|
+
node: {
|
|
523
|
+
runId: node.runId,
|
|
524
|
+
nodeId: node.nodeId,
|
|
525
|
+
iteration: node.iteration,
|
|
526
|
+
state: node.state,
|
|
527
|
+
lastAttempt: node.lastAttempt ?? null,
|
|
528
|
+
updatedAtMs: node.updatedAtMs ?? null,
|
|
529
|
+
outputTable: node.outputTable ?? null,
|
|
530
|
+
label: node.label ?? null,
|
|
531
|
+
},
|
|
532
|
+
status: node.state,
|
|
533
|
+
durationMs: computeNodeDurationMs(attempts),
|
|
534
|
+
attemptsSummary: summarizeAttempts(attempts),
|
|
535
|
+
attempts: attemptsDetailed,
|
|
536
|
+
toolCalls,
|
|
537
|
+
tokenUsage,
|
|
538
|
+
scorers: scorerRowsFiltered.map((row) => ({
|
|
539
|
+
id: row.id,
|
|
540
|
+
attempt: row.attempt,
|
|
541
|
+
scorerId: row.scorerId,
|
|
542
|
+
scorerName: row.scorerName,
|
|
543
|
+
source: row.source,
|
|
544
|
+
score: row.score,
|
|
545
|
+
reason: row.reason ?? null,
|
|
546
|
+
latencyMs: row.latencyMs ?? null,
|
|
547
|
+
durationMs: row.durationMs ?? null,
|
|
548
|
+
scoredAtMs: row.scoredAtMs,
|
|
549
|
+
meta: parseJsonValue(row.metaJson),
|
|
550
|
+
input: parseJsonValue(row.inputJson),
|
|
551
|
+
output: parseJsonValue(row.outputJson),
|
|
552
|
+
})),
|
|
553
|
+
output: {
|
|
554
|
+
validated: validatedOutput.validated,
|
|
555
|
+
raw: rawOutput,
|
|
556
|
+
source: validatedOutput.source,
|
|
557
|
+
cacheKey: validatedOutput.cacheKey,
|
|
558
|
+
},
|
|
559
|
+
limits: {
|
|
560
|
+
toolPayloadBytesHuman: MAX_TOOL_PAYLOAD_BYTES_HUMAN,
|
|
561
|
+
validatedOutputBytesHuman: MAX_VALIDATED_OUTPUT_BYTES_HUMAN,
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}).pipe(Effect.annotateLogs({
|
|
565
|
+
runId: params.runId,
|
|
566
|
+
nodeId: params.nodeId,
|
|
567
|
+
}), Effect.withLogSpan("cli:node-detail"));
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* @param {NodeDetailAttempt[]} attempts
|
|
571
|
+
*/
|
|
572
|
+
function summarizeAttemptStatesForHuman(attempts) {
|
|
573
|
+
let failed = 0;
|
|
574
|
+
let cancelled = 0;
|
|
575
|
+
let finished = 0;
|
|
576
|
+
let other = 0;
|
|
577
|
+
for (const attempt of attempts) {
|
|
578
|
+
if (attempt.state === "failed")
|
|
579
|
+
failed += 1;
|
|
580
|
+
else if (attempt.state === "cancelled")
|
|
581
|
+
cancelled += 1;
|
|
582
|
+
else if (attempt.state === "finished")
|
|
583
|
+
finished += 1;
|
|
584
|
+
else
|
|
585
|
+
other += 1;
|
|
586
|
+
}
|
|
587
|
+
const parts = [];
|
|
588
|
+
if (failed > 0)
|
|
589
|
+
parts.push(`${failed} failed`);
|
|
590
|
+
if (cancelled > 0)
|
|
591
|
+
parts.push(`${cancelled} cancelled`);
|
|
592
|
+
if (finished > 0)
|
|
593
|
+
parts.push(`${finished} succeeded`);
|
|
594
|
+
if (other > 0)
|
|
595
|
+
parts.push(`${other} other`);
|
|
596
|
+
return parts.join(", ");
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* @param {unknown} payload
|
|
600
|
+
* @param {number} maxBytes
|
|
601
|
+
*/
|
|
602
|
+
function renderHumanPayload(payload, maxBytes) {
|
|
603
|
+
return truncateForHuman(stringifyForHuman(payload), maxBytes);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* @param {EnrichedNodeDetail} detail
|
|
607
|
+
* @param {RenderNodeDetailOptions} options
|
|
608
|
+
*/
|
|
609
|
+
export function renderNodeDetailHuman(detail, options) {
|
|
610
|
+
const lines = [];
|
|
611
|
+
const attempts = detail.attempts;
|
|
612
|
+
const attemptsSummaryParts = summarizeAttemptStatesForHuman(attempts);
|
|
613
|
+
lines.push(`Node: ${detail.node.nodeId} (iteration ${detail.node.iteration})`);
|
|
614
|
+
lines.push(`Status: ${detail.status}`);
|
|
615
|
+
lines.push(`Duration: ${formatDuration(detail.durationMs)}`);
|
|
616
|
+
lines.push(attemptsSummaryParts
|
|
617
|
+
? `Attempts: ${detail.attemptsSummary.total} (${attemptsSummaryParts})`
|
|
618
|
+
: `Attempts: ${detail.attemptsSummary.total}`);
|
|
619
|
+
const expandAll = options.expandAttempts ||
|
|
620
|
+
attempts.length <= DEFAULT_EXPANDED_ATTEMPT_LIMIT;
|
|
621
|
+
const expandedAttempts = expandAll ? attempts : attempts.slice(-1);
|
|
622
|
+
const summarizedPrior = expandAll ? [] : attempts.slice(0, Math.max(0, attempts.length - 1));
|
|
623
|
+
if (summarizedPrior.length > 0) {
|
|
624
|
+
const priorSummary = summarizeAttemptStatesForHuman(summarizedPrior);
|
|
625
|
+
lines.push("");
|
|
626
|
+
lines.push(priorSummary
|
|
627
|
+
? `${summarizedPrior.length} prior attempts (${priorSummary})`
|
|
628
|
+
: `${summarizedPrior.length} prior attempts`);
|
|
629
|
+
}
|
|
630
|
+
const latestAttemptNumber = attempts.length > 0
|
|
631
|
+
? attempts[attempts.length - 1].attempt
|
|
632
|
+
: null;
|
|
633
|
+
for (const attempt of expandedAttempts) {
|
|
634
|
+
lines.push("");
|
|
635
|
+
lines.push(`Attempt ${attempt.attempt} - ${attempt.state} (${formatDuration(attempt.durationMs)})`);
|
|
636
|
+
if (attempt.error) {
|
|
637
|
+
lines.push(` Error: ${attempt.error}`);
|
|
638
|
+
}
|
|
639
|
+
const usage = attempt.tokenUsage;
|
|
640
|
+
if (usage.inputTokens > 0 ||
|
|
641
|
+
usage.outputTokens > 0 ||
|
|
642
|
+
usage.cacheReadTokens > 0 ||
|
|
643
|
+
usage.cacheWriteTokens > 0) {
|
|
644
|
+
const cost = formatCostUsd(usage.costUsd);
|
|
645
|
+
lines.push(` Tokens: ${formatCount(usage.inputTokens)} in / ${formatCount(usage.outputTokens)} out${cost ? ` ($${cost})` : ""}`);
|
|
646
|
+
}
|
|
647
|
+
if (attempt.toolCalls.length > 0) {
|
|
648
|
+
lines.push(" Tool calls:");
|
|
649
|
+
for (const toolCall of attempt.toolCalls) {
|
|
650
|
+
const duration = formatDuration(toolCall.durationMs);
|
|
651
|
+
lines.push(` ${toolCall.name} (${duration}) -> ${describeToolResult(toolCall)}`);
|
|
652
|
+
if (options.expandTools) {
|
|
653
|
+
if (toolCall.input != null) {
|
|
654
|
+
appendPrefixedBlock(lines, " Input:", renderHumanPayload(toolCall.input, MAX_TOOL_PAYLOAD_BYTES_HUMAN), " ");
|
|
655
|
+
}
|
|
656
|
+
if (toolCall.output != null) {
|
|
657
|
+
appendPrefixedBlock(lines, " Output:", renderHumanPayload(toolCall.output, MAX_TOOL_PAYLOAD_BYTES_HUMAN), " ");
|
|
658
|
+
}
|
|
659
|
+
if (toolCall.error) {
|
|
660
|
+
lines.push(` Error: ${toolCall.error}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (latestAttemptNumber != null &&
|
|
666
|
+
attempt.attempt === latestAttemptNumber) {
|
|
667
|
+
if (detail.output.validated != null) {
|
|
668
|
+
appendPrefixedBlock(lines, " Output (validated):", renderHumanPayload(detail.output.validated, MAX_VALIDATED_OUTPUT_BYTES_HUMAN), " ");
|
|
669
|
+
}
|
|
670
|
+
else if (detail.output.raw != null) {
|
|
671
|
+
appendPrefixedBlock(lines, " Output (raw):", renderHumanPayload(detail.output.raw, MAX_VALIDATED_OUTPUT_BYTES_HUMAN), " ");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (detail.scorers.length > 0) {
|
|
676
|
+
lines.push("");
|
|
677
|
+
for (const scorer of detail.scorers) {
|
|
678
|
+
lines.push(`Scorer: ${scorer.scorerName} -> ${formatScore(scorer.score)}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return lines.join("\n");
|
|
682
|
+
}
|