@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
package/src/format.js ADDED
@@ -0,0 +1,272 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./FormatEventLineOptions.ts").FormatEventLineOptions} FormatEventLineOptions */
3
+ // @smithers-type-exports-end
4
+
5
+ import pc from "picocolors";
6
+ import { eventCategoryForType } from "./event-categories.js";
7
+ function cliColors() {
8
+ return pc.createColors(true);
9
+ }
10
+ /**
11
+ * Format a timestamp as relative age: "2m ago", "1h ago", "3d ago"
12
+ */
13
+ export function formatAge(ms) {
14
+ const now = Date.now();
15
+ const diff = now - ms;
16
+ if (diff < 0)
17
+ return "just now";
18
+ const seconds = Math.floor(diff / 1000);
19
+ if (seconds < 60)
20
+ return `${seconds}s ago`;
21
+ const minutes = Math.floor(seconds / 60);
22
+ if (minutes < 60)
23
+ return `${minutes}m ago`;
24
+ const hours = Math.floor(minutes / 60);
25
+ if (hours < 24)
26
+ return `${hours}h ago`;
27
+ const days = Math.floor(hours / 24);
28
+ return `${days}d ago`;
29
+ }
30
+ /**
31
+ * Format elapsed time compactly: "5m 23s", "1h 2m", "45s"
32
+ */
33
+ export function formatElapsedCompact(startMs, endMs) {
34
+ const elapsed = (endMs ?? Date.now()) - startMs;
35
+ const seconds = Math.floor(elapsed / 1000);
36
+ if (seconds < 60)
37
+ return `${seconds}s`;
38
+ const minutes = Math.floor(seconds / 60);
39
+ if (minutes < 60)
40
+ return `${minutes}m ${seconds % 60}s`;
41
+ const hours = Math.floor(minutes / 60);
42
+ return `${hours}h ${minutes % 60}m`;
43
+ }
44
+ /**
45
+ * Format an elapsed time as HH:MM:SS from a base timestamp.
46
+ */
47
+ export function formatTimestamp(baseMs, eventMs) {
48
+ const elapsed = eventMs - baseMs;
49
+ const secs = Math.floor(elapsed / 1000);
50
+ const mins = Math.floor(secs / 60);
51
+ const hrs = Math.floor(mins / 60);
52
+ /**
53
+ * @param {number} n
54
+ */
55
+ const pad = (n) => String(n).padStart(2, "0");
56
+ return `${pad(hrs)}:${pad(mins % 60)}:${pad(secs % 60)}`;
57
+ }
58
+ /**
59
+ * Format an elapsed time as a signed relative offset:
60
+ * +MM:SS.mmm (or +HH:MM:SS.mmm when hours > 0).
61
+ */
62
+ export function formatRelativeOffset(baseMs, eventMs) {
63
+ const elapsed = Math.max(0, eventMs - baseMs);
64
+ const totalSeconds = Math.floor(elapsed / 1000);
65
+ const millis = elapsed % 1000;
66
+ const seconds = totalSeconds % 60;
67
+ const totalMinutes = Math.floor(totalSeconds / 60);
68
+ const minutes = totalMinutes % 60;
69
+ const hours = Math.floor(totalMinutes / 60);
70
+ /**
71
+ * @param {number} n
72
+ */
73
+ const pad2 = (n) => String(n).padStart(2, "0");
74
+ /**
75
+ * @param {number} n
76
+ */
77
+ const pad3 = (n) => String(n).padStart(3, "0");
78
+ if (hours > 0) {
79
+ return `+${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}.${pad3(millis)}`;
80
+ }
81
+ return `+${pad2(minutes)}:${pad2(seconds)}.${pad3(millis)}`;
82
+ }
83
+ /**
84
+ * @param {string} type
85
+ * @param {string} text
86
+ * @returns {string}
87
+ */
88
+ export function colorizeEventText(type, text) {
89
+ const color = cliColors();
90
+ if (type.endsWith("Failed") ||
91
+ type.endsWith("Denied") ||
92
+ type.endsWith("Error") ||
93
+ type === "RunCancelled" ||
94
+ type === "NodeCancelled") {
95
+ return color.red(text);
96
+ }
97
+ if (type.endsWith("Finished") ||
98
+ type === "ApprovalGranted" ||
99
+ type === "RunAutoResumed") {
100
+ return color.green(text);
101
+ }
102
+ if (eventCategoryForType(type) === "approval") {
103
+ return color.yellow(text);
104
+ }
105
+ if (eventCategoryForType(type) === "tool-call" ||
106
+ eventCategoryForType(type) === "openapi") {
107
+ return color.blue(text);
108
+ }
109
+ if (type.endsWith("Started")) {
110
+ return color.cyan(text);
111
+ }
112
+ return text;
113
+ }
114
+ /**
115
+ * @param {string} value
116
+ * @param {number} maxLength
117
+ * @returns {string}
118
+ */
119
+ function truncateText(value, maxLength) {
120
+ if (value.length <= maxLength)
121
+ return value;
122
+ return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
123
+ }
124
+ /**
125
+ * @param {unknown} payload
126
+ * @param {string} rawPayloadJson
127
+ * @param {number} maxLength
128
+ * @returns {string}
129
+ */
130
+ function summarizePayload(payload, rawPayloadJson, maxLength) {
131
+ const value = payload === undefined
132
+ ? rawPayloadJson
133
+ : typeof payload === "string"
134
+ ? payload
135
+ : JSON.stringify(payload);
136
+ if (!value)
137
+ return "";
138
+ return truncateText(value, maxLength);
139
+ }
140
+ /**
141
+ * Format a single event from _smithers_events into a log line.
142
+ */
143
+ export function formatEventLine(event, baseMs, options) {
144
+ const ts = formatTimestamp(baseMs, event.timestampMs);
145
+ const prefix = options?.includeTimestamp === false ? "" : `[${ts}] `;
146
+ const truncatePayloadAt = options?.truncatePayloadAt ?? 240;
147
+ let payload;
148
+ try {
149
+ payload = JSON.parse(event.payloadJson);
150
+ }
151
+ catch {
152
+ payload = undefined;
153
+ }
154
+ switch (event.type) {
155
+ case "RunStarted":
156
+ return `${prefix}▶ Run started`;
157
+ case "RunStatusChanged":
158
+ return `${prefix}↺ Run status: ${payload?.status ?? "unknown"}`;
159
+ case "RunFinished":
160
+ return `${prefix}✓ Run finished`;
161
+ case "RunFailed":
162
+ return `${prefix}✗ Run failed: ${truncateText(String(payload?.error ?? "unknown"), truncatePayloadAt)}`;
163
+ case "RunCancelled":
164
+ return `${prefix}⊘ Run cancelled`;
165
+ case "RunContinuedAsNew":
166
+ return `${prefix}⇢ Continued as new: ${payload?.newRunId ?? "?"} (iteration ${payload?.iteration ?? 0})`;
167
+ case "RunHijackRequested":
168
+ return `${prefix}⇢ Hijack requested`;
169
+ case "RunHijacked":
170
+ return payload?.mode === "conversation"
171
+ ? `${prefix}⇢ Hijacked ${payload?.engine ?? "agent"} conversation`
172
+ : `${prefix}⇢ Hijacked ${payload?.engine ?? "agent"} session ${payload?.resume ?? ""}`.trim();
173
+ case "SandboxCreated":
174
+ return `${prefix}🧪 Sandbox created: ${payload?.sandboxId ?? "?"} (${payload?.runtime ?? "bubblewrap"})`;
175
+ case "SandboxShipped":
176
+ return `${prefix}📦 Sandbox shipped: ${payload?.sandboxId ?? "?"} (${payload?.bundleSizeBytes ?? 0} bytes)`;
177
+ case "SandboxHeartbeat":
178
+ return `${prefix}💓 Sandbox heartbeat: ${payload?.sandboxId ?? "?"}`;
179
+ case "SandboxBundleReceived":
180
+ return `${prefix}📥 Sandbox bundle received: ${payload?.sandboxId ?? "?"} (${payload?.patchCount ?? 0} patches)`;
181
+ case "SandboxCompleted":
182
+ return `${prefix}✅ Sandbox completed: ${payload?.sandboxId ?? "?"} (${payload?.status ?? "finished"})`;
183
+ case "SandboxFailed":
184
+ return `${prefix}❌ Sandbox failed: ${payload?.sandboxId ?? "?"}`;
185
+ case "SandboxDiffReviewRequested":
186
+ return `${prefix}📝 Sandbox diff review requested: ${payload?.sandboxId ?? "?"}`;
187
+ case "SandboxDiffAccepted":
188
+ return `${prefix}👍 Sandbox diffs accepted: ${payload?.sandboxId ?? "?"}`;
189
+ case "SandboxDiffRejected":
190
+ return `${prefix}👎 Sandbox diffs rejected: ${payload?.sandboxId ?? "?"}`;
191
+ case "NodePending":
192
+ return `${prefix}… ${payload?.nodeId ?? "?"} pending (iteration ${payload?.iteration ?? 0})`;
193
+ case "NodeStarted":
194
+ return `${prefix}→ ${payload?.nodeId ?? "?"} (attempt ${payload?.attempt ?? 1}, iteration ${payload?.iteration ?? 0})`;
195
+ case "TaskHeartbeat":
196
+ return `${prefix}💓 ${payload?.nodeId ?? "?"} heartbeat (${payload?.dataSizeBytes ?? 0} bytes)`;
197
+ case "TaskHeartbeatTimeout":
198
+ return `${prefix}⏲ ${payload?.nodeId ?? "?"} heartbeat timeout (${payload?.timeoutMs ?? 0}ms)`;
199
+ case "NodeFinished":
200
+ return `${prefix}✓ ${payload?.nodeId ?? "?"} (attempt ${payload?.attempt ?? 1})`;
201
+ case "NodeFailed":
202
+ return `${prefix}✗ ${payload?.nodeId ?? "?"} (attempt ${payload?.attempt ?? 1}): ${truncateText(String(payload?.error ?? "failed"), truncatePayloadAt)}`;
203
+ case "NodeCancelled":
204
+ return `${prefix}⊘ ${payload?.nodeId ?? "?"} cancelled`;
205
+ case "NodeSkipped":
206
+ return `${prefix}↷ ${payload?.nodeId ?? "?"} skipped`;
207
+ case "NodeRetrying":
208
+ return `${prefix}↻ ${payload?.nodeId ?? "?"} retrying (attempt ${payload?.attempt ?? 1})`;
209
+ case "NodeWaitingApproval":
210
+ return `${prefix}⏸ ${payload?.nodeId ?? "?"} waiting for approval`;
211
+ case "NodeWaitingTimer":
212
+ return `${prefix}⏱ Waiting for timer: ${payload?.nodeId ?? "?"}`;
213
+ case "ApprovalRequested":
214
+ return `${prefix}⏸ Approval requested: ${payload?.nodeId ?? "?"}`;
215
+ case "ApprovalGranted":
216
+ return `${prefix}✓ Approved: ${payload?.nodeId ?? "?"}`;
217
+ case "ApprovalAutoApproved":
218
+ return `${prefix}✓ Auto-approved: ${payload?.nodeId ?? "?"}`;
219
+ case "ApprovalDenied":
220
+ return `${prefix}✗ Denied: ${payload?.nodeId ?? "?"}`;
221
+ case "ToolCallStarted":
222
+ return `${prefix}🔧 ${payload?.nodeId ?? "?"} → ${payload?.toolName ?? "tool"} (attempt ${payload?.attempt ?? 1})`;
223
+ case "ToolCallFinished":
224
+ return `${prefix}🔧 ${payload?.nodeId ?? "?"} ← ${payload?.toolName ?? "tool"} (${payload?.status ?? "done"})`;
225
+ case "ScorerStarted":
226
+ return `${prefix}📊 ${payload?.nodeId ?? "?"} scorer ${payload?.scorerName ?? payload?.scorerId ?? "?"} started`;
227
+ case "ScorerFinished":
228
+ return `${prefix}📊 ${payload?.nodeId ?? "?"} scorer ${payload?.scorerName ?? payload?.scorerId ?? "?"} = ${payload?.score ?? "?"}`;
229
+ case "ScorerFailed":
230
+ return `${prefix}📊 ${payload?.nodeId ?? "?"} scorer ${payload?.scorerName ?? payload?.scorerId ?? "?"} failed`;
231
+ case "TokenUsageReported":
232
+ return `${prefix}🧮 ${payload?.nodeId ?? "?"} ${payload?.model ?? "model"} in=${payload?.inputTokens ?? 0} out=${payload?.outputTokens ?? 0}`;
233
+ case "TimerCreated":
234
+ return `${prefix}⏱ Timer created: ${payload?.timerId ?? "?"} (fires ${new Date(payload?.firesAtMs ?? 0).toISOString()})`;
235
+ case "TimerFired":
236
+ return `${prefix}🔔 Timer fired: ${payload?.timerId ?? "?"} (delay ${payload?.delayMs ?? 0}ms)`;
237
+ case "TimerCancelled":
238
+ return `${prefix}⊘ Timer cancelled: ${payload?.timerId ?? "?"}`;
239
+ case "WorkflowReloadDetected":
240
+ return `${prefix}⟳ File change detected`;
241
+ case "WorkflowReloaded":
242
+ return `${prefix}⟳ Workflow reloaded`;
243
+ case "WorkflowReloadFailed":
244
+ return `${prefix}⟳ Workflow reload failed`;
245
+ case "WorkflowReloadUnsafe":
246
+ return `${prefix}⟳ Workflow reload skipped: unsafe`;
247
+ case "AgentEvent":
248
+ return `${prefix}${payload?.engine ?? "agent"}: ${payload?.event?.type ?? "event"}`;
249
+ case "FrameCommitted":
250
+ return `${prefix}🖼 Frame ${payload?.frameNo ?? "?"} committed`;
251
+ case "SnapshotCaptured":
252
+ return `${prefix}📸 Snapshot ${payload?.frameNo ?? "?"} captured`;
253
+ case "RevertStarted":
254
+ return `${prefix}↩ Revert started on ${payload?.nodeId ?? "?"}`;
255
+ case "RevertFinished":
256
+ return `${prefix}${payload?.success ? "✓" : "✗"} Revert ${payload?.success ? "finished" : "failed"} on ${payload?.nodeId ?? "?"}`;
257
+ case "TimeTravelStarted":
258
+ return `${prefix}↺ Time travel started on ${payload?.nodeId ?? "?"}`;
259
+ case "TimeTravelFinished":
260
+ return `${prefix}${payload?.success ? "✓" : "✗"} Time travel ${payload?.success ? "finished" : "failed"} on ${payload?.nodeId ?? "?"}`;
261
+ case "OpenApiToolCalled":
262
+ return `${prefix}🌐 ${payload?.method ?? "?"} ${payload?.path ?? payload?.operationId ?? "?"} (${payload?.status ?? "unknown"})`;
263
+ case "MemoryFactSet":
264
+ return `${prefix}🧠 Memory set ${payload?.namespace ?? "default"}/${payload?.key ?? "?"}`;
265
+ case "MemoryRecalled":
266
+ return `${prefix}🧠 Memory recalled ${payload?.resultCount ?? 0} results`;
267
+ case "MemoryMessageSaved":
268
+ return `${prefix}🧠 Message saved to thread ${payload?.threadId ?? "?"}`;
269
+ default:
270
+ return `${prefix}${event.type} ${summarizePayload(payload, event.payloadJson, truncatePayloadAt)}`.trim();
271
+ }
272
+ }
@@ -0,0 +1,207 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin, stdout, stderr } from "node:process";
3
+ import { dirname, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { SmithersCtx } from "@smithers-orchestrator/driver";
6
+ import { loadInput, loadOutputs } from "@smithers-orchestrator/db/snapshot";
7
+ import { renderFrame, resolveSchema } from "@smithers-orchestrator/engine";
8
+ import { mdxPlugin } from "smithers-orchestrator/mdx-plugin";
9
+ import { SmithersError } from "@smithers-orchestrator/errors";
10
+ import { Effect } from "effect";
11
+ /** @typedef {import("./HijackCandidate.ts").HijackCandidate} HijackCandidate */
12
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
13
+
14
+ /**
15
+ * @template T
16
+ * @param {T} value
17
+ * @returns {T | undefined}
18
+ */
19
+ function cloneJsonValue(value) {
20
+ if (value === undefined)
21
+ return undefined;
22
+ try {
23
+ return JSON.parse(JSON.stringify(value));
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /**
30
+ * @param {string | null} [metaJson]
31
+ * @returns {Record<string, unknown>}
32
+ */
33
+ function parseAttemptMeta(metaJson) {
34
+ if (!metaJson)
35
+ return {};
36
+ try {
37
+ const parsed = JSON.parse(metaJson);
38
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
39
+ ? parsed
40
+ : {};
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
46
+ /**
47
+ * @param {string} workflowPath
48
+ * @returns {Promise<SmithersWorkflow<any>>}
49
+ */
50
+ async function loadWorkflow(workflowPath) {
51
+ const abs = resolve(process.cwd(), workflowPath);
52
+ mdxPlugin();
53
+ const mod = await import(pathToFileURL(abs).href);
54
+ if (!mod.default) {
55
+ throw new SmithersError("WORKFLOW_MISSING_DEFAULT", `Workflow ${workflowPath} must export default`);
56
+ }
57
+ return mod.default;
58
+ }
59
+ /**
60
+ * @param {SmithersDb} adapter
61
+ * @param {HijackCandidate & { mode: "conversation"; messages: unknown[] }} candidate
62
+ */
63
+ async function resolveConversationAgent(adapter, candidate) {
64
+ const run = await adapter.getRun(candidate.runId);
65
+ const workflowPath = run?.workflowPath;
66
+ if (!workflowPath) {
67
+ throw new SmithersError("HIJACK_WORKFLOW_PATH", `Run ${candidate.runId} does not have a workflowPath; cannot reconstruct agent`);
68
+ }
69
+ const workflow = await loadWorkflow(workflowPath);
70
+ const schema = resolveSchema(workflow.db);
71
+ const inputTable = schema.input;
72
+ const inputRow = inputTable
73
+ ? ((await loadInput(workflow.db, inputTable, candidate.runId)) ?? {})
74
+ : {};
75
+ const outputs = await loadOutputs(workflow.db, schema, candidate.runId);
76
+ const ctx = new SmithersCtx({
77
+ runId: candidate.runId,
78
+ iteration: candidate.iteration,
79
+ input: inputRow ?? {},
80
+ outputs,
81
+ zodToKeyName: workflow.zodToKeyName,
82
+ });
83
+ const baseRootDir = dirname(resolve(workflowPath));
84
+ const snap = await Effect.runPromise(renderFrame(workflow, ctx, {
85
+ baseRootDir,
86
+ workflowPath,
87
+ }));
88
+ const task = snap.tasks.find((entry) => entry.nodeId === candidate.nodeId &&
89
+ (entry.iteration ?? 0) === candidate.iteration) ?? snap.tasks.find((entry) => entry.nodeId === candidate.nodeId);
90
+ if (!task?.agent) {
91
+ throw new SmithersError("HIJACK_AGENT_NOT_FOUND", `Could not find agent-backed task ${candidate.nodeId} in ${workflowPath}`);
92
+ }
93
+ const allAgents = Array.isArray(task.agent) ? task.agent : [task.agent];
94
+ const effectiveAgent = allAgents[Math.min(candidate.attempt - 1, allAgents.length - 1)];
95
+ if (!effectiveAgent) {
96
+ throw new SmithersError("HIJACK_AGENT_EMPTY", `Task ${candidate.nodeId} does not have a usable agent to hijack`);
97
+ }
98
+ return {
99
+ workflowPath,
100
+ agent: effectiveAgent,
101
+ };
102
+ }
103
+ /**
104
+ * @param {SmithersDb} adapter
105
+ * @param {HijackCandidate} candidate
106
+ * @param {unknown[]} messages
107
+ */
108
+ export async function persistConversationHijackHandoff(adapter, candidate, messages) {
109
+ const attempt = await adapter.getAttempt(candidate.runId, candidate.nodeId, candidate.iteration, candidate.attempt);
110
+ if (!attempt) {
111
+ throw new SmithersError("HIJACK_ATTEMPT_NOT_FOUND", `Attempt ${candidate.nodeId}#${candidate.attempt} no longer exists`);
112
+ }
113
+ const clonedMessages = cloneJsonValue(messages) ?? messages;
114
+ const meta = parseAttemptMeta(attempt.metaJson);
115
+ meta.agentConversation = clonedMessages;
116
+ meta.hijackHandoff = {
117
+ engine: candidate.engine,
118
+ mode: "conversation",
119
+ messages: clonedMessages,
120
+ requestedAtMs: Date.now(),
121
+ cwd: candidate.cwd,
122
+ nodeId: candidate.nodeId,
123
+ iteration: candidate.iteration,
124
+ attempt: candidate.attempt,
125
+ };
126
+ await adapter.updateAttempt(candidate.runId, candidate.nodeId, candidate.iteration, candidate.attempt, {
127
+ metaJson: JSON.stringify(meta),
128
+ });
129
+ }
130
+ /**
131
+ * @param {SmithersDb} adapter
132
+ * @param {HijackCandidate & { mode: "conversation"; messages: unknown[] }} candidate
133
+ * @returns {Promise<{ code: number; messages: unknown[] }>}
134
+ */
135
+ export async function launchConversationHijackSession(adapter, candidate) {
136
+ const { agent } = await resolveConversationAgent(adapter, candidate);
137
+ const rl = createInterface({
138
+ input: stdin,
139
+ output: stdout,
140
+ terminal: true,
141
+ });
142
+ let messages = cloneJsonValue(candidate.messages) ?? candidate.messages;
143
+ stderr.write(`[smithers] hijacking ${candidate.engine} conversation from ${candidate.nodeId}#${candidate.attempt}\n`);
144
+ stderr.write("[smithers] enter /exit to return control to Smithers\n");
145
+ try {
146
+ while (true) {
147
+ const line = (await rl.question("> ")).trim();
148
+ if (!line)
149
+ continue;
150
+ if (line === "/exit" || line === "/quit") {
151
+ break;
152
+ }
153
+ if (line === "/help") {
154
+ stdout.write("/exit return control to Smithers\n/help show this message\n");
155
+ continue;
156
+ }
157
+ const nextMessages = [
158
+ ...messages,
159
+ { role: "user", content: line },
160
+ ];
161
+ try {
162
+ const stepMessages = [];
163
+ let streamedAny = false;
164
+ const result = await agent.generate({
165
+ options: undefined,
166
+ messages: nextMessages,
167
+ onStdout: (chunk) => {
168
+ streamedAny = true;
169
+ stdout.write(chunk);
170
+ },
171
+ onStderr: (chunk) => stderr.write(chunk),
172
+ onStepFinish: (step) => {
173
+ const responseMessages = Array.isArray(step?.response?.messages)
174
+ ? (cloneJsonValue(step.response.messages) ?? step.response.messages)
175
+ : [];
176
+ if (responseMessages.length > 0) {
177
+ stepMessages.push(...responseMessages);
178
+ }
179
+ },
180
+ });
181
+ if (!streamedAny && typeof result?.text === "string" && result.text) {
182
+ stdout.write(result.text);
183
+ }
184
+ stdout.write("\n");
185
+ const responseMessages = stepMessages.length > 0
186
+ ? stepMessages
187
+ : Array.isArray(result?.response?.messages)
188
+ ? (cloneJsonValue(result.response.messages) ?? result.response.messages)
189
+ : [{ role: "assistant", content: result?.text ?? "" }];
190
+ messages = [
191
+ ...nextMessages,
192
+ ...responseMessages,
193
+ ];
194
+ }
195
+ catch (err) {
196
+ stderr.write(`[smithers] hijack agent error: ${err instanceof Error ? err.message : String(err)}\n`);
197
+ }
198
+ }
199
+ }
200
+ finally {
201
+ rl.close();
202
+ }
203
+ return {
204
+ code: 0,
205
+ messages,
206
+ };
207
+ }