@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.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +55 -0
  3. package/src/AgentAvailability.ts +13 -0
  4. package/src/AgentAvailabilityStatus.ts +5 -0
  5. package/src/AggregateNodeDetailParams.ts +5 -0
  6. package/src/AskOptions.ts +12 -0
  7. package/src/ChatAttemptMeta.ts +7 -0
  8. package/src/ChatAttemptRow.ts +12 -0
  9. package/src/ChatOutputEvent.ts +6 -0
  10. package/src/DiffBundleLike.ts +6 -0
  11. package/src/DiscoveredWorkflow.ts +9 -0
  12. package/src/EnrichedNodeDetail.ts +60 -0
  13. package/src/EventCategory.ts +18 -0
  14. package/src/FindDbWaitOptions.ts +4 -0
  15. package/src/FormatEventLineOptions.ts +4 -0
  16. package/src/HijackCandidate.ts +11 -0
  17. package/src/HijackLaunchSpec.ts +6 -0
  18. package/src/InitWorkflowPackOptions.ts +4 -0
  19. package/src/InitWorkflowPackResult.ts +6 -0
  20. package/src/NativeHijackEngine.ts +8 -0
  21. package/src/NodeDetailAttempt.ts +22 -0
  22. package/src/NodeDetailTokenUsage.ts +11 -0
  23. package/src/NodeDetailToolCall.ts +12 -0
  24. package/src/ParsedNodeOutputEvent.ts +9 -0
  25. package/src/RenderNodeDetailOptions.ts +4 -0
  26. package/src/RunAutoResumeSkipReason.ts +4 -0
  27. package/src/RunDiffCommandInput.ts +13 -0
  28. package/src/RunDiffCommandResult.ts +3 -0
  29. package/src/RunOutputCommandInput.ts +12 -0
  30. package/src/RunOutputCommandResult.ts +3 -0
  31. package/src/RunRewindCommandInput.ts +14 -0
  32. package/src/RunRewindCommandResult.ts +3 -0
  33. package/src/RunTreeCommandInput.ts +14 -0
  34. package/src/RunTreeCommandResult.ts +3 -0
  35. package/src/SmithersEventType.ts +3 -0
  36. package/src/SupervisorOptions.ts +33 -0
  37. package/src/SupervisorPollSummary.ts +6 -0
  38. package/src/TreeRenderOptions.ts +5 -0
  39. package/src/WatchLoopOptions.ts +9 -0
  40. package/src/WatchLoopResult.ts +8 -0
  41. package/src/WatchRenderContext.ts +4 -0
  42. package/src/WhyBlocker.ts +17 -0
  43. package/src/WhyBlockerKind.ts +9 -0
  44. package/src/WhyDiagnosis.ts +10 -0
  45. package/src/WorkflowCta.ts +4 -0
  46. package/src/WorkflowSourceType.ts +1 -0
  47. package/src/agent-detection.js +257 -0
  48. package/src/ask.js +491 -0
  49. package/src/chat.js +226 -0
  50. package/src/diff.js +221 -0
  51. package/src/event-categories.js +141 -0
  52. package/src/find-db.js +93 -0
  53. package/src/format.js +272 -0
  54. package/src/hijack-session.js +207 -0
  55. package/src/hijack.js +226 -0
  56. package/src/index.d.ts +1 -0
  57. package/src/index.js +4868 -0
  58. package/src/mcp/SemanticMcpServerOptions.ts +4 -0
  59. package/src/mcp/SemanticToolCallResult.ts +14 -0
  60. package/src/mcp/SemanticToolContext.ts +6 -0
  61. package/src/mcp/SemanticToolDefinition.ts +13 -0
  62. package/src/mcp/SemanticToolError.ts +6 -0
  63. package/src/mcp/semantic-server.js +41 -0
  64. package/src/mcp/semantic-tools.js +1242 -0
  65. package/src/node-detail.js +682 -0
  66. package/src/output.js +111 -0
  67. package/src/resume-detached.js +37 -0
  68. package/src/rewind.js +88 -0
  69. package/src/scheduler.js +112 -0
  70. package/src/smithersRuntime.js +63 -0
  71. package/src/supervisor.js +418 -0
  72. package/src/tree.js +307 -0
  73. package/src/tui/app.jsx +139 -0
  74. package/src/tui/app.tsx +5 -0
  75. package/src/tui/components/AskModal.jsx +109 -0
  76. package/src/tui/components/AskModal.tsx +3 -0
  77. package/src/tui/components/AttentionPane.jsx +112 -0
  78. package/src/tui/components/AttentionPane.tsx +6 -0
  79. package/src/tui/components/ChatPane.jsx +57 -0
  80. package/src/tui/components/ChatPane.tsx +7 -0
  81. package/src/tui/components/CronList.jsx +87 -0
  82. package/src/tui/components/CronList.tsx +5 -0
  83. package/src/tui/components/DetailsPane.jsx +96 -0
  84. package/src/tui/components/DetailsPane.tsx +7 -0
  85. package/src/tui/components/FramesPane.jsx +147 -0
  86. package/src/tui/components/FramesPane.tsx +8 -0
  87. package/src/tui/components/LogsPane.jsx +46 -0
  88. package/src/tui/components/LogsPane.tsx +6 -0
  89. package/src/tui/components/MetricsPane.jsx +108 -0
  90. package/src/tui/components/MetricsPane.tsx +5 -0
  91. package/src/tui/components/NodeDetailView.jsx +284 -0
  92. package/src/tui/components/NodeDetailView.tsx +7 -0
  93. package/src/tui/components/NodeInspector.jsx +51 -0
  94. package/src/tui/components/NodeInspector.tsx +7 -0
  95. package/src/tui/components/RunDetailView.jsx +190 -0
  96. package/src/tui/components/RunDetailView.tsx +7 -0
  97. package/src/tui/components/RunsList.jsx +184 -0
  98. package/src/tui/components/RunsList.tsx +7 -0
  99. package/src/tui/components/SqliteBrowser.jsx +131 -0
  100. package/src/tui/components/SqliteBrowser.tsx +5 -0
  101. package/src/tui/components/WorkflowLauncher.jsx +63 -0
  102. package/src/tui/components/WorkflowLauncher.tsx +3 -0
  103. package/src/util/CliErrorMapping.ts +7 -0
  104. package/src/util/CliExitCode.ts +10 -0
  105. package/src/util/errorMessage.js +212 -0
  106. package/src/util/exitCodes.js +18 -0
  107. package/src/watch.js +128 -0
  108. package/src/why-diagnosis.js +1000 -0
  109. package/src/workflow-pack.js +2151 -0
  110. 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
+ }