@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,1242 @@
1
+ import { basename, extname, resolve } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { Effect } from "effect";
4
+ import { z } from "zod";
5
+ import { ensureSmithersTables } from "@smithers-orchestrator/db/ensure";
6
+ import { SmithersDb } from "@smithers-orchestrator/db/adapter";
7
+ import { findAndOpenDb } from "../find-db.js";
8
+ import { aggregateNodeDetailEffect, } from "../node-detail.js";
9
+ import { diagnoseRunEffect, } from "../why-diagnosis.js";
10
+ import { chatAttemptKey, parseChatAttemptMeta, parseNodeOutputEvent, selectChatAttempts, } from "../chat.js";
11
+ import { WATCH_MIN_INTERVAL_MS } from "../watch.js";
12
+ import { discoverWorkflows, resolveWorkflow } from "../workflows.js";
13
+ import { mdxPlugin } from "smithers-orchestrator/mdx-plugin";
14
+ import { approveNode, denyNode } from "@smithers-orchestrator/engine/approvals";
15
+ import { runWorkflow } from "@smithers-orchestrator/engine";
16
+ import { revertToAttempt } from "@smithers-orchestrator/time-travel/revert";
17
+ import { runPromise } from "../smithersRuntime.js";
18
+ import { SmithersError } from "@smithers-orchestrator/errors";
19
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
20
+ /**
21
+ * @typedef {{ content: Array<{ type: "text"; text: string; }>; structuredContent: { ok: boolean; data?: unknown; error?: z.infer<typeof toolErrorSchema>; }; isError?: boolean; }} SemanticToolCallResult
22
+ */
23
+ /**
24
+ * @typedef {{ cwd: () => string; openDb: typeof findAndOpenDb; }} SemanticToolContext
25
+ */
26
+ /** @typedef {import("./SemanticToolDefinition.ts").SemanticToolDefinition} SemanticToolDefinition */
27
+
28
+ export const SEMANTIC_TOOL_NAMES = [
29
+ "list_workflows",
30
+ "run_workflow",
31
+ "list_runs",
32
+ "get_run",
33
+ "watch_run",
34
+ "explain_run",
35
+ "list_pending_approvals",
36
+ "resolve_approval",
37
+ "get_node_detail",
38
+ "revert_attempt",
39
+ "list_artifacts",
40
+ "get_chat_transcript",
41
+ "get_run_events",
42
+ ];
43
+ const workflowSummarySchema = z.object({
44
+ id: z.string(),
45
+ displayName: z.string(),
46
+ entryFile: z.string(),
47
+ sourceType: z.enum(["seeded", "user", "generated"]),
48
+ });
49
+ const timerSchema = z.object({
50
+ nodeId: z.string(),
51
+ iteration: z.number().int(),
52
+ firesAtMs: z.number(),
53
+ remainingMs: z.number(),
54
+ timerType: z.enum(["duration", "absolute"]),
55
+ });
56
+ const runSummarySchema = z.object({
57
+ runId: z.string(),
58
+ workflowName: z.string().nullable(),
59
+ workflowPath: z.string().nullable(),
60
+ parentRunId: z.string().nullable(),
61
+ status: z.string(),
62
+ createdAtMs: z.number(),
63
+ startedAtMs: z.number().nullable(),
64
+ finishedAtMs: z.number().nullable(),
65
+ heartbeatAtMs: z.number().nullable(),
66
+ activeNodeId: z.string().nullable(),
67
+ activeNodeLabel: z.string().nullable(),
68
+ pendingApprovalCount: z.number().int(),
69
+ waitingTimers: z.array(timerSchema),
70
+ countsByState: z.record(z.string(), z.number().int()),
71
+ });
72
+ const runStepSchema = z.object({
73
+ nodeId: z.string(),
74
+ iteration: z.number().int(),
75
+ state: z.string(),
76
+ lastAttempt: z.number().int().nullable(),
77
+ updatedAtMs: z.number().nullable(),
78
+ outputTable: z.string().nullable(),
79
+ label: z.string().nullable(),
80
+ });
81
+ const pendingApprovalSchema = z.object({
82
+ runId: z.string(),
83
+ nodeId: z.string(),
84
+ iteration: z.number().int(),
85
+ status: z.string(),
86
+ requestedAtMs: z.number().nullable().optional(),
87
+ decidedAtMs: z.number().nullable().optional(),
88
+ note: z.string().nullable().optional(),
89
+ decidedBy: z.string().nullable().optional(),
90
+ request: z.unknown().nullable().optional(),
91
+ decision: z.unknown().nullable().optional(),
92
+ autoApproved: z.boolean().optional(),
93
+ workflowName: z.string().nullable().optional(),
94
+ runStatus: z.string().nullable().optional(),
95
+ nodeLabel: z.string().nullable().optional(),
96
+ });
97
+ const runLoopSchema = z.object({
98
+ loopId: z.string(),
99
+ iteration: z.number().int(),
100
+ maxIterations: z.number().int().nullable(),
101
+ });
102
+ const runDetailSchema = runSummarySchema.extend({
103
+ steps: z.array(runStepSchema),
104
+ approvals: z.array(pendingApprovalSchema),
105
+ loops: z.array(runLoopSchema),
106
+ continuedFromRunIds: z.array(z.string()),
107
+ activeDescendantRunId: z.string().nullable(),
108
+ config: z.unknown().nullable(),
109
+ error: z.unknown().nullable(),
110
+ });
111
+ const runWatchSnapshotSchema = z.object({
112
+ observedAtMs: z.number(),
113
+ run: runSummarySchema,
114
+ });
115
+ const nodeDetailSchema = z.object({
116
+ node: z.object({
117
+ runId: z.string(),
118
+ nodeId: z.string(),
119
+ iteration: z.number().int(),
120
+ state: z.string(),
121
+ lastAttempt: z.number().int().nullable(),
122
+ updatedAtMs: z.number().nullable(),
123
+ outputTable: z.string().nullable(),
124
+ label: z.string().nullable(),
125
+ }),
126
+ status: z.string(),
127
+ durationMs: z.number().nullable(),
128
+ attemptsSummary: z.object({
129
+ total: z.number().int(),
130
+ failed: z.number().int(),
131
+ cancelled: z.number().int(),
132
+ succeeded: z.number().int(),
133
+ waiting: z.number().int(),
134
+ }),
135
+ attempts: z.array(z.unknown()),
136
+ toolCalls: z.array(z.unknown()),
137
+ tokenUsage: z.unknown(),
138
+ scorers: z.array(z.unknown()),
139
+ output: z.object({
140
+ validated: z.unknown().nullable(),
141
+ raw: z.unknown().nullable(),
142
+ source: z.enum(["cache", "output-table", "none"]),
143
+ cacheKey: z.string().nullable(),
144
+ }),
145
+ limits: z.object({
146
+ toolPayloadBytesHuman: z.number().int(),
147
+ validatedOutputBytesHuman: z.number().int(),
148
+ }),
149
+ });
150
+ const diagnosisSchema = z.object({
151
+ runId: z.string(),
152
+ status: z.string(),
153
+ summary: z.string(),
154
+ generatedAtMs: z.number(),
155
+ blockers: z.array(z.object({
156
+ kind: z.string(),
157
+ nodeId: z.string(),
158
+ iteration: z.number().nullable(),
159
+ reason: z.string(),
160
+ waitingSince: z.number(),
161
+ unblocker: z.string(),
162
+ context: z.string().optional(),
163
+ signalName: z.string().nullable().optional(),
164
+ dependencyNodeId: z.string().nullable().optional(),
165
+ firesAtMs: z.number().nullable().optional(),
166
+ remainingMs: z.number().nullable().optional(),
167
+ attempt: z.number().nullable().optional(),
168
+ maxAttempts: z.number().nullable().optional(),
169
+ })),
170
+ currentNodeId: z.string().nullable(),
171
+ });
172
+ const eventSchema = z.object({
173
+ runId: z.string(),
174
+ seq: z.number().int(),
175
+ timestampMs: z.number(),
176
+ type: z.string(),
177
+ payload: z.unknown().nullable(),
178
+ });
179
+ const artifactSchema = z.object({
180
+ artifactId: z.string(),
181
+ kind: z.literal("node-output"),
182
+ runId: z.string(),
183
+ nodeId: z.string(),
184
+ iteration: z.number().int(),
185
+ label: z.string().nullable(),
186
+ state: z.string(),
187
+ outputTable: z.string().nullable(),
188
+ source: z.enum(["cache", "output-table", "none"]),
189
+ cacheKey: z.string().nullable(),
190
+ value: z.unknown().nullable(),
191
+ rawValue: z.unknown().nullable().optional(),
192
+ });
193
+ const chatAttemptSchema = z.object({
194
+ attemptKey: z.string(),
195
+ nodeId: z.string(),
196
+ iteration: z.number().int(),
197
+ attempt: z.number().int(),
198
+ state: z.string(),
199
+ startedAtMs: z.number(),
200
+ finishedAtMs: z.number().nullable(),
201
+ cached: z.boolean(),
202
+ meta: z.unknown().nullable(),
203
+ });
204
+ const chatMessageSchema = z.object({
205
+ id: z.string(),
206
+ attemptKey: z.string(),
207
+ nodeId: z.string(),
208
+ iteration: z.number().int(),
209
+ attempt: z.number().int(),
210
+ role: z.enum(["user", "assistant", "stderr"]),
211
+ stream: z.enum(["stdout", "stderr"]).nullable(),
212
+ timestampMs: z.number(),
213
+ text: z.string(),
214
+ source: z.enum(["prompt", "event", "responseText"]),
215
+ });
216
+ const toolErrorSchema = z.object({
217
+ code: z.string(),
218
+ message: z.string(),
219
+ details: z.record(z.string(), z.unknown()).nullable().optional(),
220
+ docsUrl: z.string().nullable().optional(),
221
+ });
222
+ /**
223
+ * @template Data
224
+ * @param {Data} data
225
+ */
226
+ function resultSchema(data) {
227
+ return z.object({
228
+ ok: z.boolean(),
229
+ data: data.optional(),
230
+ error: toolErrorSchema.optional(),
231
+ });
232
+ }
233
+ const listWorkflowsInputSchema = z.object({});
234
+ const listWorkflowsDataSchema = z.object({
235
+ workflows: z.array(workflowSummarySchema),
236
+ });
237
+ const runWorkflowInputSchema = z.object({
238
+ workflowId: z.string().describe("Discovered workflow ID from .smithers/workflows"),
239
+ input: z.record(z.string(), z.unknown()).optional(),
240
+ prompt: z.string().optional(),
241
+ runId: z.string().optional(),
242
+ resume: z.boolean().default(false),
243
+ force: z.boolean().default(false),
244
+ waitForTerminal: z.boolean().default(false),
245
+ waitForStartMs: z.number().int().min(0).default(1_000),
246
+ maxConcurrency: z.number().int().min(1).optional(),
247
+ rootDir: z.string().optional(),
248
+ logDir: z.string().optional(),
249
+ allowNetwork: z.boolean().default(false),
250
+ maxOutputBytes: z.number().int().min(1).optional(),
251
+ toolTimeoutMs: z.number().int().min(1).optional(),
252
+ hot: z.boolean().default(false),
253
+ }).superRefine((value, ctx) => {
254
+ if (value.resume && !value.runId) {
255
+ ctx.addIssue({
256
+ code: z.ZodIssueCode.custom,
257
+ message: "runId is required when resume=true",
258
+ path: ["runId"],
259
+ });
260
+ }
261
+ });
262
+ const runWorkflowDataSchema = z.object({
263
+ workflow: workflowSummarySchema,
264
+ runId: z.string(),
265
+ launchMode: z.enum(["background", "waited"]),
266
+ requestedResume: z.boolean(),
267
+ status: z.string(),
268
+ observedRun: runSummarySchema.nullable(),
269
+ result: z.object({
270
+ runId: z.string(),
271
+ status: z.string(),
272
+ output: z.unknown().optional(),
273
+ error: z.unknown().optional(),
274
+ }).nullable(),
275
+ });
276
+ const listRunsInputSchema = z.object({
277
+ limit: z.number().int().min(1).max(200).default(20),
278
+ status: z.string().optional(),
279
+ });
280
+ const listRunsDataSchema = z.object({
281
+ runs: z.array(runSummarySchema),
282
+ });
283
+ const getRunInputSchema = z.object({
284
+ runId: z.string(),
285
+ });
286
+ const getRunDataSchema = z.object({
287
+ run: runDetailSchema,
288
+ });
289
+ const watchRunInputSchema = z.object({
290
+ runId: z.string(),
291
+ intervalMs: z.number().int().min(1).default(1_000),
292
+ timeoutMs: z.number().int().min(0).default(30_000),
293
+ });
294
+ const watchRunDataSchema = z.object({
295
+ runId: z.string(),
296
+ intervalMs: z.number().int(),
297
+ pollCount: z.number().int(),
298
+ reachedTerminal: z.boolean(),
299
+ timedOut: z.boolean(),
300
+ finalRun: runSummarySchema,
301
+ snapshots: z.array(runWatchSnapshotSchema),
302
+ });
303
+ const explainRunInputSchema = z.object({
304
+ runId: z.string(),
305
+ });
306
+ const explainRunDataSchema = z.object({
307
+ diagnosis: diagnosisSchema,
308
+ });
309
+ const listPendingApprovalsInputSchema = z.object({
310
+ runId: z.string().optional(),
311
+ workflowName: z.string().optional(),
312
+ nodeId: z.string().optional(),
313
+ });
314
+ const listPendingApprovalsDataSchema = z.object({
315
+ approvals: z.array(pendingApprovalSchema),
316
+ });
317
+ const resolveApprovalInputSchema = z.object({
318
+ action: z.enum(["approve", "deny"]),
319
+ runId: z.string().optional(),
320
+ workflowName: z.string().optional(),
321
+ nodeId: z.string().optional(),
322
+ iteration: z.number().int().min(0).optional(),
323
+ note: z.string().optional(),
324
+ decidedBy: z.string().optional(),
325
+ decision: z.unknown().optional(),
326
+ });
327
+ const resolveApprovalDataSchema = z.object({
328
+ action: z.enum(["approve", "deny"]),
329
+ approval: pendingApprovalSchema,
330
+ run: runSummarySchema.nullable(),
331
+ });
332
+ const getNodeDetailInputSchema = z.object({
333
+ runId: z.string(),
334
+ nodeId: z.string(),
335
+ iteration: z.number().int().min(0).optional(),
336
+ });
337
+ const getNodeDetailDataSchema = z.object({
338
+ detail: nodeDetailSchema,
339
+ });
340
+ const revertAttemptInputSchema = z.object({
341
+ runId: z.string(),
342
+ nodeId: z.string(),
343
+ iteration: z.number().int().min(0).default(0),
344
+ attempt: z.number().int().min(1),
345
+ });
346
+ const revertAttemptDataSchema = z.object({
347
+ runId: z.string(),
348
+ nodeId: z.string(),
349
+ iteration: z.number().int(),
350
+ attempt: z.number().int(),
351
+ success: z.boolean(),
352
+ error: z.string().optional(),
353
+ jjPointer: z.string().optional(),
354
+ run: runSummarySchema.nullable(),
355
+ });
356
+ const listArtifactsInputSchema = z.object({
357
+ runId: z.string(),
358
+ nodeId: z.string().optional(),
359
+ includeRaw: z.boolean().default(false),
360
+ });
361
+ const listArtifactsDataSchema = z.object({
362
+ artifacts: z.array(artifactSchema),
363
+ });
364
+ const getChatTranscriptInputSchema = z.object({
365
+ runId: z.string(),
366
+ all: z.boolean().default(false),
367
+ includeStderr: z.boolean().default(true),
368
+ tail: z.number().int().min(1).optional(),
369
+ });
370
+ const getChatTranscriptDataSchema = z.object({
371
+ runId: z.string(),
372
+ attempts: z.array(chatAttemptSchema),
373
+ messages: z.array(chatMessageSchema),
374
+ });
375
+ const getRunEventsInputSchema = z.object({
376
+ runId: z.string(),
377
+ afterSeq: z.number().int().optional(),
378
+ limit: z.number().int().min(1).max(10_000).default(200),
379
+ nodeId: z.string().optional(),
380
+ types: z.array(z.string()).optional(),
381
+ sinceTimestampMs: z.number().int().optional(),
382
+ });
383
+ const getRunEventsDataSchema = z.object({
384
+ runId: z.string(),
385
+ events: z.array(eventSchema),
386
+ });
387
+ /**
388
+ * @param {number} ms
389
+ */
390
+ function sleep(ms) {
391
+ return new Promise((resolvePromise) => {
392
+ setTimeout(resolvePromise, ms);
393
+ });
394
+ }
395
+ /**
396
+ * @param {string | null | undefined} raw
397
+ * @returns {unknown | null}
398
+ */
399
+ function parseJsonValue(raw) {
400
+ if (!raw)
401
+ return null;
402
+ try {
403
+ return JSON.parse(raw);
404
+ }
405
+ catch {
406
+ return raw;
407
+ }
408
+ }
409
+ /**
410
+ * @param {Pick<RunRow, "workflowName" | "workflowPath">} run
411
+ */
412
+ function resolveWorkflowName(run) {
413
+ const fromPath = run.workflowPath
414
+ ? basename(run.workflowPath, extname(run.workflowPath))
415
+ : null;
416
+ if (run.workflowName && run.workflowName !== "workflow") {
417
+ return run.workflowName;
418
+ }
419
+ return fromPath ?? run.workflowName ?? null;
420
+ }
421
+ /**
422
+ * @param {string | null} [metaJson]
423
+ */
424
+ function parseWaitingTimerInfo(metaJson) {
425
+ if (!metaJson)
426
+ return null;
427
+ try {
428
+ const parsed = JSON.parse(metaJson);
429
+ const timer = parsed?.timer;
430
+ if (!timer || typeof timer !== "object")
431
+ return null;
432
+ const firesAtMs = Number(timer.firesAtMs);
433
+ if (!Number.isFinite(firesAtMs))
434
+ return null;
435
+ return {
436
+ firesAtMs: Math.floor(firesAtMs),
437
+ timerType: timer.timerType === "absolute" ? "absolute" : "duration",
438
+ };
439
+ }
440
+ catch {
441
+ return null;
442
+ }
443
+ }
444
+ /**
445
+ * @param {SmithersDb} adapter
446
+ * @param {string} runId
447
+ */
448
+ async function listWaitingTimers(adapter, runId) {
449
+ const nodes = await adapter.listNodes(runId);
450
+ const waits = [];
451
+ for (const node of nodes) {
452
+ if (node.state !== "waiting-timer")
453
+ continue;
454
+ const attempts = await adapter.listAttempts(runId, node.nodeId, node.iteration ?? 0);
455
+ const waitingAttempt = attempts.find((attempt) => attempt.state === "waiting-timer") ??
456
+ attempts[0];
457
+ const parsed = parseWaitingTimerInfo(waitingAttempt?.metaJson);
458
+ if (!parsed)
459
+ continue;
460
+ waits.push({
461
+ nodeId: node.nodeId,
462
+ iteration: node.iteration ?? 0,
463
+ firesAtMs: parsed.firesAtMs,
464
+ remainingMs: parsed.firesAtMs - Date.now(),
465
+ timerType: parsed.timerType,
466
+ });
467
+ }
468
+ waits.sort((left, right) => left.firesAtMs - right.firesAtMs);
469
+ return waits;
470
+ }
471
+ /**
472
+ * @param {SmithersDb} adapter
473
+ * @param {RunRow} run
474
+ */
475
+ async function buildRunSummary(adapter, run) {
476
+ const [nodes, approvals, waitingTimers, countsByStateRows] = await Promise.all([
477
+ adapter.listNodes(run.runId),
478
+ adapter.listPendingApprovals(run.runId),
479
+ run.status === "waiting-timer"
480
+ ? listWaitingTimers(adapter, run.runId)
481
+ : Promise.resolve([]),
482
+ adapter.countNodesByState(run.runId),
483
+ ]);
484
+ const activeNode = nodes
485
+ .filter((node) => node.state === "in-progress")
486
+ .sort((left, right) => {
487
+ const leftUpdated = Number(left.updatedAtMs ?? 0);
488
+ const rightUpdated = Number(right.updatedAtMs ?? 0);
489
+ return rightUpdated - leftUpdated;
490
+ })[0];
491
+ const countsByState = Object.fromEntries(countsByStateRows.map((row) => [
492
+ String(row.state),
493
+ Number(row.count ?? 0),
494
+ ]));
495
+ return {
496
+ runId: run.runId,
497
+ workflowName: resolveWorkflowName(run),
498
+ workflowPath: run.workflowPath ?? null,
499
+ parentRunId: run.parentRunId ?? null,
500
+ status: run.status,
501
+ createdAtMs: run.createdAtMs,
502
+ startedAtMs: run.startedAtMs ?? null,
503
+ finishedAtMs: run.finishedAtMs ?? null,
504
+ heartbeatAtMs: run.heartbeatAtMs ?? null,
505
+ activeNodeId: activeNode?.nodeId ?? null,
506
+ activeNodeLabel: activeNode?.label ?? activeNode?.nodeId ?? null,
507
+ pendingApprovalCount: approvals.length,
508
+ waitingTimers,
509
+ countsByState,
510
+ };
511
+ }
512
+ /**
513
+ * @param {SmithersDb} adapter
514
+ * @param {string} runId
515
+ */
516
+ async function buildRunDetail(adapter, runId) {
517
+ const run = await requireRun(adapter, runId);
518
+ const [summary, nodes, approvals, loops, ancestry] = await Promise.all([
519
+ buildRunSummary(adapter, run),
520
+ adapter.listNodes(runId),
521
+ adapter.listPendingApprovals(runId),
522
+ adapter.listRalph(runId),
523
+ adapter.listRunAncestry(runId, 1_000),
524
+ ]);
525
+ let activeDescendantRunId = null;
526
+ const seen = new Set([runId]);
527
+ let cursor = runId;
528
+ while (true) {
529
+ const child = await adapter.getLatestChildRun(cursor);
530
+ if (!child || !child.runId || seen.has(child.runId))
531
+ break;
532
+ activeDescendantRunId = child.runId;
533
+ seen.add(child.runId);
534
+ cursor = child.runId;
535
+ }
536
+ return {
537
+ ...summary,
538
+ steps: nodes
539
+ .map((node) => ({
540
+ nodeId: node.nodeId,
541
+ iteration: node.iteration ?? 0,
542
+ state: node.state,
543
+ lastAttempt: node.lastAttempt ?? null,
544
+ updatedAtMs: node.updatedAtMs ?? null,
545
+ outputTable: node.outputTable ?? null,
546
+ label: node.label ?? null,
547
+ }))
548
+ .sort((left, right) => {
549
+ if (left.nodeId !== right.nodeId) {
550
+ return left.nodeId.localeCompare(right.nodeId);
551
+ }
552
+ return left.iteration - right.iteration;
553
+ }),
554
+ approvals: approvals.map((approval) => ({
555
+ runId: approval.runId,
556
+ nodeId: approval.nodeId,
557
+ iteration: approval.iteration ?? 0,
558
+ status: approval.status,
559
+ requestedAtMs: approval.requestedAtMs ?? null,
560
+ decidedAtMs: approval.decidedAtMs ?? null,
561
+ note: approval.note ?? null,
562
+ decidedBy: approval.decidedBy ?? null,
563
+ request: parseJsonValue(approval.requestJson),
564
+ decision: parseJsonValue(approval.decisionJson),
565
+ autoApproved: Boolean(approval.autoApproved),
566
+ })),
567
+ loops: loops.map((loop) => ({
568
+ loopId: loop.ralphId,
569
+ iteration: loop.iteration,
570
+ maxIterations: loop.maxIterations ?? null,
571
+ })),
572
+ continuedFromRunIds: ancestry.slice(1).map((row) => row.runId),
573
+ activeDescendantRunId,
574
+ config: parseJsonValue(run.configJson),
575
+ error: parseJsonValue(run.errorJson),
576
+ };
577
+ }
578
+ /**
579
+ * @param {SmithersDb} adapter
580
+ * @param {string} runId
581
+ */
582
+ async function requireRun(adapter, runId) {
583
+ const run = await adapter.getRun(runId);
584
+ if (!run) {
585
+ throw new SmithersError("RUN_NOT_FOUND", `Run not found: ${runId}`, {
586
+ runId,
587
+ });
588
+ }
589
+ return run;
590
+ }
591
+ /**
592
+ * @param {SmithersDb} adapter
593
+ * @param {string} runId
594
+ */
595
+ async function listAllEvents(adapter, runId) {
596
+ const events = [];
597
+ let lastSeq = -1;
598
+ while (true) {
599
+ const batch = await adapter.listEvents(runId, lastSeq, 1_000);
600
+ if (batch.length === 0)
601
+ break;
602
+ events.push(...batch);
603
+ lastSeq = batch[batch.length - 1].seq;
604
+ if (batch.length < 1_000)
605
+ break;
606
+ }
607
+ return events;
608
+ }
609
+ /**
610
+ * @param {string} workflowId
611
+ * @param {string} cwd
612
+ */
613
+ async function loadWorkflowById(workflowId, cwd) {
614
+ const discovered = resolveWorkflow(workflowId, cwd);
615
+ mdxPlugin();
616
+ const moduleUrl = pathToFileURL(resolve(discovered.entryFile)).href;
617
+ const mod = await import(moduleUrl);
618
+ if (!mod.default) {
619
+ throw new SmithersError("WORKFLOW_MISSING_DEFAULT", `Workflow ${workflowId} must export default`, { workflowId, entryFile: discovered.entryFile });
620
+ }
621
+ const workflow = mod.default;
622
+ ensureSmithersTables(workflow.db);
623
+ return {
624
+ workflow,
625
+ summary: {
626
+ id: discovered.id,
627
+ displayName: discovered.displayName,
628
+ entryFile: discovered.entryFile,
629
+ sourceType: discovered.sourceType,
630
+ },
631
+ };
632
+ }
633
+ /**
634
+ * @param {SmithersDb} adapter
635
+ * @param {string} runId
636
+ * @param {number} waitForStartMs
637
+ */
638
+ async function waitForObservedRun(adapter, runId, waitForStartMs) {
639
+ const deadline = Date.now() + Math.max(0, waitForStartMs);
640
+ while (true) {
641
+ const run = await adapter.getRun(runId);
642
+ if (run) {
643
+ return buildRunSummary(adapter, run);
644
+ }
645
+ if (Date.now() >= deadline) {
646
+ return null;
647
+ }
648
+ await sleep(25);
649
+ }
650
+ }
651
+ /**
652
+ * @param {unknown} error
653
+ */
654
+ function toToolError(error) {
655
+ const smithersError = toSmithersError(error);
656
+ return {
657
+ code: smithersError.code,
658
+ message: smithersError.summary,
659
+ details: smithersError.details ?? null,
660
+ docsUrl: smithersError.docsUrl ?? null,
661
+ };
662
+ }
663
+ /**
664
+ * @template Data
665
+ * @param {Data} data
666
+ */
667
+ function toolSuccess(data) {
668
+ const payload = { ok: true, data };
669
+ return {
670
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
671
+ structuredContent: payload,
672
+ };
673
+ }
674
+ /**
675
+ * @param {unknown} error
676
+ * @returns {SemanticToolCallResult}
677
+ */
678
+ function toolFailure(error) {
679
+ const payload = { ok: false, error: toToolError(error) };
680
+ return {
681
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
682
+ structuredContent: payload,
683
+ isError: true,
684
+ };
685
+ }
686
+ /**
687
+ * @template T
688
+ * @param {SemanticToolContext} context
689
+ * @param {(adapter: SmithersDb) => Promise<T>} run
690
+ */
691
+ async function withDb(context, run) {
692
+ const { adapter, cleanup } = await context.openDb(context.cwd());
693
+ try {
694
+ return await run(adapter);
695
+ }
696
+ finally {
697
+ cleanup();
698
+ }
699
+ }
700
+ /**
701
+ * @param {any} row
702
+ */
703
+ function parsePendingApproval(row) {
704
+ return {
705
+ runId: row.runId,
706
+ nodeId: row.nodeId,
707
+ iteration: row.iteration ?? 0,
708
+ status: row.status,
709
+ requestedAtMs: row.requestedAtMs ?? null,
710
+ decidedAtMs: row.decidedAtMs ?? null,
711
+ note: row.note ?? null,
712
+ decidedBy: row.decidedBy ?? null,
713
+ request: parseJsonValue(row.requestJson),
714
+ decision: parseJsonValue(row.decisionJson),
715
+ autoApproved: typeof row.autoApproved === "boolean" ? row.autoApproved : undefined,
716
+ workflowName: row.workflowName ?? null,
717
+ runStatus: row.runStatus ?? null,
718
+ nodeLabel: row.nodeLabel ?? null,
719
+ };
720
+ }
721
+ /**
722
+ * @param {any[]} approvals
723
+ * @param {{ runId?: string; workflowName?: string; nodeId?: string; iteration?: number; }} filters
724
+ */
725
+ function filterPendingApprovals(approvals, filters) {
726
+ return approvals.filter((approval) => {
727
+ if (filters.runId && approval.runId !== filters.runId)
728
+ return false;
729
+ if (filters.workflowName && approval.workflowName !== filters.workflowName)
730
+ return false;
731
+ if (filters.nodeId && approval.nodeId !== filters.nodeId)
732
+ return false;
733
+ if (typeof filters.iteration === "number" &&
734
+ Number(approval.iteration ?? 0) !== filters.iteration) {
735
+ return false;
736
+ }
737
+ return true;
738
+ });
739
+ }
740
+ /**
741
+ * @template Data
742
+ * @param {string} toolName
743
+ * @param {() => Promise<Data>} handler
744
+ * @returns {Promise<SemanticToolCallResult>}
745
+ */
746
+ async function executeSemanticTool(toolName, handler) {
747
+ try {
748
+ const data = await runPromise(Effect.tryPromise(() => handler()).pipe(Effect.annotateLogs({
749
+ toolName,
750
+ surface: "semantic",
751
+ }), Effect.withLogSpan("mcp:semantic")));
752
+ return toolSuccess(data);
753
+ }
754
+ catch (error) {
755
+ return toolFailure(error);
756
+ }
757
+ }
758
+ /**
759
+ * @param {Partial<SemanticToolContext>} [options]
760
+ * @returns {SemanticToolDefinition[]}
761
+ */
762
+ export function createSemanticToolDefinitions(options = {}) {
763
+ const context = {
764
+ cwd: options.cwd ?? (() => process.cwd()),
765
+ openDb: options.openDb ?? findAndOpenDb,
766
+ };
767
+ return [
768
+ {
769
+ name: "list_workflows",
770
+ description: "List discovered local Smithers workflows.",
771
+ inputSchema: listWorkflowsInputSchema,
772
+ outputSchema: resultSchema(listWorkflowsDataSchema),
773
+ annotations: { readOnlyHint: true },
774
+ handler: () => executeSemanticTool("list_workflows", async () => ({
775
+ workflows: discoverWorkflows(context.cwd()),
776
+ })),
777
+ },
778
+ {
779
+ name: "run_workflow",
780
+ description: "Start a discovered workflow directly through the engine. Defaults to background launch; set waitForTerminal=true to block until completion.",
781
+ inputSchema: runWorkflowInputSchema,
782
+ outputSchema: resultSchema(runWorkflowDataSchema),
783
+ annotations: { readOnlyHint: false, openWorldHint: true },
784
+ handler: (input) => executeSemanticTool("run_workflow", async () => {
785
+ const runId = input.runId ?? crypto.randomUUID();
786
+ const { workflow, summary } = await loadWorkflowById(input.workflowId, context.cwd());
787
+ const adapter = workflow.db
788
+ ? new SmithersDb(workflow.db)
789
+ : null;
790
+ const workflowInput = input.input ??
791
+ (typeof input.prompt === "string" ? { prompt: input.prompt } : {});
792
+ const launchState = {
793
+ settled: false,
794
+ result: null,
795
+ error: null,
796
+ };
797
+ const launchPromise = Effect.runPromise(runWorkflow(workflow, {
798
+ input: workflowInput,
799
+ runId,
800
+ resume: input.resume,
801
+ force: input.force,
802
+ workflowPath: summary.entryFile,
803
+ maxConcurrency: input.maxConcurrency,
804
+ rootDir: input.rootDir,
805
+ logDir: input.logDir,
806
+ allowNetwork: input.allowNetwork,
807
+ maxOutputBytes: input.maxOutputBytes,
808
+ toolTimeoutMs: input.toolTimeoutMs,
809
+ hot: input.hot,
810
+ })).then((result) => {
811
+ launchState.settled = true;
812
+ launchState.result = result;
813
+ return result;
814
+ }, (error) => {
815
+ launchState.settled = true;
816
+ launchState.error = error;
817
+ throw error;
818
+ });
819
+ if (input.waitForTerminal) {
820
+ const result = await launchPromise;
821
+ const observedRun = adapter != null ? await adapter.getRun(result.runId) : null;
822
+ return {
823
+ workflow: summary,
824
+ runId: result.runId,
825
+ launchMode: "waited",
826
+ requestedResume: input.resume,
827
+ status: result.status,
828
+ observedRun: observedRun != null
829
+ ? await buildRunSummary(adapter, observedRun)
830
+ : null,
831
+ result,
832
+ };
833
+ }
834
+ void launchPromise.catch((error) => {
835
+ const rendered = toToolError(error);
836
+ console.error(`[smithers:mcp] run_workflow background failure ${runId}: ${rendered.code} ${rendered.message}`);
837
+ });
838
+ const observedRun = adapter != null
839
+ ? await waitForObservedRun(adapter, runId, input.waitForStartMs)
840
+ : null;
841
+ if (observedRun == null && launchState.settled) {
842
+ if (launchState.error) {
843
+ throw launchState.error;
844
+ }
845
+ if (launchState.result) {
846
+ const finalRun = adapter != null
847
+ ? await adapter.getRun(launchState.result.runId)
848
+ : null;
849
+ return {
850
+ workflow: summary,
851
+ runId: launchState.result.runId,
852
+ launchMode: "background",
853
+ requestedResume: input.resume,
854
+ status: launchState.result.status,
855
+ observedRun: finalRun != null
856
+ ? await buildRunSummary(adapter, finalRun)
857
+ : null,
858
+ result: launchState.result,
859
+ };
860
+ }
861
+ }
862
+ return {
863
+ workflow: summary,
864
+ runId,
865
+ launchMode: "background",
866
+ requestedResume: input.resume,
867
+ status: observedRun?.status ?? "running",
868
+ observedRun,
869
+ result: null,
870
+ };
871
+ }),
872
+ },
873
+ {
874
+ name: "list_runs",
875
+ description: "List recent Smithers runs with stable structured summaries.",
876
+ inputSchema: listRunsInputSchema,
877
+ outputSchema: resultSchema(listRunsDataSchema),
878
+ annotations: { readOnlyHint: true },
879
+ handler: (input) => executeSemanticTool("list_runs", async () => withDb(context, async (adapter) => {
880
+ const runs = await adapter.listRuns(input.limit, input.status);
881
+ const summaries = await Promise.all(runs.map((run) => buildRunSummary(adapter, run)));
882
+ return { runs: summaries };
883
+ })),
884
+ },
885
+ {
886
+ name: "get_run",
887
+ description: "Get enriched structured state for a specific run, including steps, approvals, timers, lineage, and config.",
888
+ inputSchema: getRunInputSchema,
889
+ outputSchema: resultSchema(getRunDataSchema),
890
+ annotations: { readOnlyHint: true },
891
+ handler: (input) => executeSemanticTool("get_run", async () => withDb(context, async (adapter) => ({
892
+ run: await buildRunDetail(adapter, input.runId),
893
+ }))),
894
+ },
895
+ {
896
+ name: "watch_run",
897
+ description: "Poll a run until it reaches a terminal state or timeout. This is the explicit watch/poll semantic tool.",
898
+ inputSchema: watchRunInputSchema,
899
+ outputSchema: resultSchema(watchRunDataSchema),
900
+ annotations: { readOnlyHint: true },
901
+ handler: (input) => executeSemanticTool("watch_run", async () => withDb(context, async (adapter) => {
902
+ const intervalMs = Math.max(WATCH_MIN_INTERVAL_MS, input.intervalMs);
903
+ const deadline = Date.now() + input.timeoutMs;
904
+ const snapshots = [];
905
+ let pollCount = 0;
906
+ while (true) {
907
+ const run = await adapter.getRun(input.runId);
908
+ if (!run) {
909
+ throw new SmithersError("RUN_NOT_FOUND", `Run not found: ${input.runId}`, {
910
+ runId: input.runId,
911
+ });
912
+ }
913
+ const summary = await buildRunSummary(adapter, run);
914
+ snapshots.push({
915
+ observedAtMs: Date.now(),
916
+ run: summary,
917
+ });
918
+ if (run.status !== "running" &&
919
+ run.status !== "waiting-approval" &&
920
+ run.status !== "waiting-event" &&
921
+ run.status !== "waiting-timer") {
922
+ return {
923
+ runId: input.runId,
924
+ intervalMs,
925
+ pollCount,
926
+ reachedTerminal: true,
927
+ timedOut: false,
928
+ finalRun: summary,
929
+ snapshots,
930
+ };
931
+ }
932
+ if (Date.now() >= deadline) {
933
+ return {
934
+ runId: input.runId,
935
+ intervalMs,
936
+ pollCount,
937
+ reachedTerminal: false,
938
+ timedOut: true,
939
+ finalRun: summary,
940
+ snapshots,
941
+ };
942
+ }
943
+ pollCount += 1;
944
+ await sleep(intervalMs);
945
+ }
946
+ })),
947
+ },
948
+ {
949
+ name: "explain_run",
950
+ description: "Explain why a run is waiting, stale, or blocked by returning the diagnosis model directly.",
951
+ inputSchema: explainRunInputSchema,
952
+ outputSchema: resultSchema(explainRunDataSchema),
953
+ annotations: { readOnlyHint: true },
954
+ handler: (input) => executeSemanticTool("explain_run", async () => withDb(context, async (adapter) => ({
955
+ diagnosis: await runPromise(diagnoseRunEffect(adapter, input.runId)),
956
+ }))),
957
+ },
958
+ {
959
+ name: "list_pending_approvals",
960
+ description: "List pending approvals across all runs or a filtered subset.",
961
+ inputSchema: listPendingApprovalsInputSchema,
962
+ outputSchema: resultSchema(listPendingApprovalsDataSchema),
963
+ annotations: { readOnlyHint: true },
964
+ handler: (input) => executeSemanticTool("list_pending_approvals", async () => withDb(context, async (adapter) => {
965
+ const approvals = await adapter.listAllPendingApprovals();
966
+ return {
967
+ approvals: filterPendingApprovals(approvals, input).map(parsePendingApproval),
968
+ };
969
+ })),
970
+ },
971
+ {
972
+ name: "resolve_approval",
973
+ description: "Destructive: approve or deny a pending approval. If filters match more than one approval, this tool returns an ambiguity error instead of guessing.",
974
+ inputSchema: resolveApprovalInputSchema,
975
+ outputSchema: resultSchema(resolveApprovalDataSchema),
976
+ annotations: {
977
+ readOnlyHint: false,
978
+ destructiveHint: true,
979
+ idempotentHint: false,
980
+ },
981
+ handler: (input) => executeSemanticTool("resolve_approval", async () => withDb(context, async (adapter) => {
982
+ const approvals = await adapter.listAllPendingApprovals();
983
+ const matches = filterPendingApprovals(approvals, input);
984
+ if (matches.length === 0) {
985
+ throw new SmithersError("INVALID_INPUT", "No pending approval matched the provided filters.", {
986
+ filters: input,
987
+ });
988
+ }
989
+ if (matches.length > 1) {
990
+ throw new SmithersError("INVALID_INPUT", "Multiple pending approvals matched. Provide runId/nodeId/iteration to disambiguate.", {
991
+ matches: matches.map((approval) => ({
992
+ runId: approval.runId,
993
+ nodeId: approval.nodeId,
994
+ iteration: approval.iteration ?? 0,
995
+ workflowName: approval.workflowName ?? null,
996
+ })),
997
+ });
998
+ }
999
+ const approval = matches[0];
1000
+ if (input.action === "approve") {
1001
+ await Effect.runPromise(approveNode(adapter, approval.runId, approval.nodeId, approval.iteration ?? 0, input.note, input.decidedBy, input.decision));
1002
+ }
1003
+ else {
1004
+ await Effect.runPromise(denyNode(adapter, approval.runId, approval.nodeId, approval.iteration ?? 0, input.note, input.decidedBy, input.decision));
1005
+ }
1006
+ const run = await adapter.getRun(approval.runId);
1007
+ return {
1008
+ action: input.action,
1009
+ approval: {
1010
+ ...parsePendingApproval(approval),
1011
+ status: input.action === "approve" ? "approved" : "denied",
1012
+ requestedAtMs: approval.requestedAtMs ?? null,
1013
+ decidedAtMs: Date.now(),
1014
+ note: input.note ?? null,
1015
+ decidedBy: input.decidedBy ?? null,
1016
+ decision: input.decision ?? null,
1017
+ },
1018
+ run: run != null
1019
+ ? await buildRunSummary(adapter, run)
1020
+ : null,
1021
+ };
1022
+ })),
1023
+ },
1024
+ {
1025
+ name: "get_node_detail",
1026
+ description: "Get enriched node state including attempts, tool calls, token usage, scorers, and validated output.",
1027
+ inputSchema: getNodeDetailInputSchema,
1028
+ outputSchema: resultSchema(getNodeDetailDataSchema),
1029
+ annotations: { readOnlyHint: true },
1030
+ handler: (input) => executeSemanticTool("get_node_detail", async () => withDb(context, async (adapter) => ({
1031
+ detail: await runPromise(aggregateNodeDetailEffect(adapter, {
1032
+ runId: input.runId,
1033
+ nodeId: input.nodeId,
1034
+ iteration: input.iteration,
1035
+ })),
1036
+ }))),
1037
+ },
1038
+ {
1039
+ name: "revert_attempt",
1040
+ description: "Destructive: revert the workspace and frame history back to a recorded attempt.",
1041
+ inputSchema: revertAttemptInputSchema,
1042
+ outputSchema: resultSchema(revertAttemptDataSchema),
1043
+ annotations: {
1044
+ readOnlyHint: false,
1045
+ destructiveHint: true,
1046
+ idempotentHint: false,
1047
+ },
1048
+ handler: (input) => executeSemanticTool("revert_attempt", async () => withDb(context, async (adapter) => {
1049
+ const result = await revertToAttempt(adapter, input);
1050
+ const run = await adapter.getRun(input.runId);
1051
+ return {
1052
+ runId: input.runId,
1053
+ nodeId: input.nodeId,
1054
+ iteration: input.iteration,
1055
+ attempt: input.attempt,
1056
+ success: result.success,
1057
+ ...(result.error ? { error: result.error } : {}),
1058
+ ...(result.jjPointer ? { jjPointer: result.jjPointer } : {}),
1059
+ run: run != null
1060
+ ? await buildRunSummary(adapter, run)
1061
+ : null,
1062
+ };
1063
+ })),
1064
+ },
1065
+ {
1066
+ name: "list_artifacts",
1067
+ description: "List structured output artifacts produced by nodes in a run.",
1068
+ inputSchema: listArtifactsInputSchema,
1069
+ outputSchema: resultSchema(listArtifactsDataSchema),
1070
+ annotations: { readOnlyHint: true },
1071
+ handler: (input) => executeSemanticTool("list_artifacts", async () => withDb(context, async (adapter) => {
1072
+ await requireRun(adapter, input.runId);
1073
+ const nodes = await adapter.listNodes(input.runId);
1074
+ const selectedNodes = nodes.filter((node) => {
1075
+ if (input.nodeId && node.nodeId !== input.nodeId)
1076
+ return false;
1077
+ return Boolean(node.outputTable);
1078
+ });
1079
+ const artifacts = [];
1080
+ for (const node of selectedNodes) {
1081
+ const detail = await runPromise(aggregateNodeDetailEffect(adapter, {
1082
+ runId: input.runId,
1083
+ nodeId: node.nodeId,
1084
+ iteration: node.iteration ?? 0,
1085
+ }));
1086
+ if (detail.output.source === "none")
1087
+ continue;
1088
+ artifacts.push({
1089
+ artifactId: `${input.runId}:${node.nodeId}:${node.iteration ?? 0}`,
1090
+ kind: "node-output",
1091
+ runId: input.runId,
1092
+ nodeId: node.nodeId,
1093
+ iteration: node.iteration ?? 0,
1094
+ label: node.label ?? null,
1095
+ state: node.state,
1096
+ outputTable: node.outputTable ?? null,
1097
+ source: detail.output.source,
1098
+ cacheKey: detail.output.cacheKey,
1099
+ value: detail.output.validated,
1100
+ ...(input.includeRaw ? { rawValue: detail.output.raw } : {}),
1101
+ });
1102
+ }
1103
+ return { artifacts };
1104
+ })),
1105
+ },
1106
+ {
1107
+ name: "get_chat_transcript",
1108
+ description: "Return the structured agent chat transcript for a run, grouped by attempts and message role.",
1109
+ inputSchema: getChatTranscriptInputSchema,
1110
+ outputSchema: resultSchema(getChatTranscriptDataSchema),
1111
+ annotations: { readOnlyHint: true },
1112
+ handler: (input) => executeSemanticTool("get_chat_transcript", async () => withDb(context, async (adapter) => {
1113
+ await requireRun(adapter, input.runId);
1114
+ const attempts = await adapter.listAttemptsForRun(input.runId);
1115
+ const events = await listAllEvents(adapter, input.runId);
1116
+ const knownOutputAttemptKeys = new Set();
1117
+ const parsedOutputs = events
1118
+ .map((event) => parseNodeOutputEvent(event))
1119
+ .filter(Boolean);
1120
+ for (const event of parsedOutputs) {
1121
+ knownOutputAttemptKeys.add(chatAttemptKey(event));
1122
+ }
1123
+ const selectedAttempts = selectChatAttempts(attempts, knownOutputAttemptKeys, input.all);
1124
+ const selectedAttemptKeys = new Set(selectedAttempts.map((attempt) => chatAttemptKey(attempt)));
1125
+ const stdoutSeenAttempts = new Set();
1126
+ const messages = [];
1127
+ for (const attempt of selectedAttempts) {
1128
+ const attemptKey = chatAttemptKey(attempt);
1129
+ const meta = parseChatAttemptMeta(attempt.metaJson);
1130
+ const prompt = typeof meta.prompt === "string" ? meta.prompt.trim() : "";
1131
+ if (prompt) {
1132
+ messages.push({
1133
+ id: `prompt:${attemptKey}`,
1134
+ attemptKey,
1135
+ nodeId: attempt.nodeId,
1136
+ iteration: attempt.iteration ?? 0,
1137
+ attempt: attempt.attempt,
1138
+ role: "user",
1139
+ stream: null,
1140
+ timestampMs: attempt.startedAtMs,
1141
+ text: prompt,
1142
+ source: "prompt",
1143
+ });
1144
+ }
1145
+ }
1146
+ for (const event of parsedOutputs) {
1147
+ const attemptKey = chatAttemptKey(event);
1148
+ if (!selectedAttemptKeys.has(attemptKey))
1149
+ continue;
1150
+ if (event.stream === "stderr" && !input.includeStderr)
1151
+ continue;
1152
+ if (event.stream === "stdout") {
1153
+ stdoutSeenAttempts.add(attemptKey);
1154
+ }
1155
+ messages.push({
1156
+ id: `event:${event.seq}`,
1157
+ attemptKey,
1158
+ nodeId: event.nodeId,
1159
+ iteration: event.iteration,
1160
+ attempt: event.attempt,
1161
+ role: event.stream === "stderr" ? "stderr" : "assistant",
1162
+ stream: event.stream,
1163
+ timestampMs: event.timestampMs,
1164
+ text: event.text,
1165
+ source: "event",
1166
+ });
1167
+ }
1168
+ for (const attempt of selectedAttempts) {
1169
+ const attemptKey = chatAttemptKey(attempt);
1170
+ const responseText = typeof attempt.responseText === "string"
1171
+ ? attempt.responseText.trim()
1172
+ : "";
1173
+ if (!responseText || stdoutSeenAttempts.has(attemptKey))
1174
+ continue;
1175
+ messages.push({
1176
+ id: `response:${attemptKey}`,
1177
+ attemptKey,
1178
+ nodeId: attempt.nodeId,
1179
+ iteration: attempt.iteration ?? 0,
1180
+ attempt: attempt.attempt,
1181
+ role: "assistant",
1182
+ stream: null,
1183
+ timestampMs: attempt.finishedAtMs ?? attempt.startedAtMs ?? Date.now(),
1184
+ text: responseText,
1185
+ source: "responseText",
1186
+ });
1187
+ }
1188
+ messages.sort((left, right) => {
1189
+ if (left.timestampMs !== right.timestampMs) {
1190
+ return left.timestampMs - right.timestampMs;
1191
+ }
1192
+ return left.id.localeCompare(right.id);
1193
+ });
1194
+ const tailedMessages = typeof input.tail === "number"
1195
+ ? messages.slice(-input.tail)
1196
+ : messages;
1197
+ return {
1198
+ runId: input.runId,
1199
+ attempts: selectedAttempts.map((attempt) => ({
1200
+ attemptKey: chatAttemptKey(attempt),
1201
+ nodeId: attempt.nodeId,
1202
+ iteration: attempt.iteration ?? 0,
1203
+ attempt: attempt.attempt,
1204
+ state: attempt.state,
1205
+ startedAtMs: attempt.startedAtMs,
1206
+ finishedAtMs: attempt.finishedAtMs ?? null,
1207
+ cached: Boolean(attempt.cached),
1208
+ meta: parseJsonValue(attempt.metaJson),
1209
+ })),
1210
+ messages: tailedMessages,
1211
+ };
1212
+ })),
1213
+ },
1214
+ {
1215
+ name: "get_run_events",
1216
+ description: "Return structured event history for a run without relying on CLI table or NDJSON formatting.",
1217
+ inputSchema: getRunEventsInputSchema,
1218
+ outputSchema: resultSchema(getRunEventsDataSchema),
1219
+ annotations: { readOnlyHint: true },
1220
+ handler: (input) => executeSemanticTool("get_run_events", async () => withDb(context, async (adapter) => {
1221
+ await requireRun(adapter, input.runId);
1222
+ const events = await adapter.listEventHistory(input.runId, {
1223
+ afterSeq: input.afterSeq,
1224
+ limit: input.limit,
1225
+ nodeId: input.nodeId,
1226
+ types: input.types,
1227
+ sinceTimestampMs: input.sinceTimestampMs,
1228
+ });
1229
+ return {
1230
+ runId: input.runId,
1231
+ events: events.map((event) => ({
1232
+ runId: event.runId,
1233
+ seq: event.seq,
1234
+ timestampMs: event.timestampMs,
1235
+ type: event.type,
1236
+ payload: parseJsonValue(event.payloadJson),
1237
+ })),
1238
+ };
1239
+ })),
1240
+ },
1241
+ ];
1242
+ }