@smithers-orchestrator/cli 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +55 -0
- package/src/AgentAvailability.ts +13 -0
- package/src/AgentAvailabilityStatus.ts +5 -0
- package/src/AggregateNodeDetailParams.ts +5 -0
- package/src/AskOptions.ts +12 -0
- package/src/ChatAttemptMeta.ts +7 -0
- package/src/ChatAttemptRow.ts +12 -0
- package/src/ChatOutputEvent.ts +6 -0
- package/src/DiffBundleLike.ts +6 -0
- package/src/DiscoveredWorkflow.ts +9 -0
- package/src/EnrichedNodeDetail.ts +60 -0
- package/src/EventCategory.ts +18 -0
- package/src/FindDbWaitOptions.ts +4 -0
- package/src/FormatEventLineOptions.ts +4 -0
- package/src/HijackCandidate.ts +11 -0
- package/src/HijackLaunchSpec.ts +6 -0
- package/src/InitWorkflowPackOptions.ts +4 -0
- package/src/InitWorkflowPackResult.ts +6 -0
- package/src/NativeHijackEngine.ts +8 -0
- package/src/NodeDetailAttempt.ts +22 -0
- package/src/NodeDetailTokenUsage.ts +11 -0
- package/src/NodeDetailToolCall.ts +12 -0
- package/src/ParsedNodeOutputEvent.ts +9 -0
- package/src/RenderNodeDetailOptions.ts +4 -0
- package/src/RunAutoResumeSkipReason.ts +4 -0
- package/src/RunDiffCommandInput.ts +13 -0
- package/src/RunDiffCommandResult.ts +3 -0
- package/src/RunOutputCommandInput.ts +12 -0
- package/src/RunOutputCommandResult.ts +3 -0
- package/src/RunRewindCommandInput.ts +14 -0
- package/src/RunRewindCommandResult.ts +3 -0
- package/src/RunTreeCommandInput.ts +14 -0
- package/src/RunTreeCommandResult.ts +3 -0
- package/src/SmithersEventType.ts +3 -0
- package/src/SupervisorOptions.ts +33 -0
- package/src/SupervisorPollSummary.ts +6 -0
- package/src/TreeRenderOptions.ts +5 -0
- package/src/WatchLoopOptions.ts +9 -0
- package/src/WatchLoopResult.ts +8 -0
- package/src/WatchRenderContext.ts +4 -0
- package/src/WhyBlocker.ts +17 -0
- package/src/WhyBlockerKind.ts +9 -0
- package/src/WhyDiagnosis.ts +10 -0
- package/src/WorkflowCta.ts +4 -0
- package/src/WorkflowSourceType.ts +1 -0
- package/src/agent-detection.js +257 -0
- package/src/ask.js +491 -0
- package/src/chat.js +226 -0
- package/src/diff.js +221 -0
- package/src/event-categories.js +141 -0
- package/src/find-db.js +93 -0
- package/src/format.js +272 -0
- package/src/hijack-session.js +207 -0
- package/src/hijack.js +226 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +4868 -0
- package/src/mcp/SemanticMcpServerOptions.ts +4 -0
- package/src/mcp/SemanticToolCallResult.ts +14 -0
- package/src/mcp/SemanticToolContext.ts +6 -0
- package/src/mcp/SemanticToolDefinition.ts +13 -0
- package/src/mcp/SemanticToolError.ts +6 -0
- package/src/mcp/semantic-server.js +41 -0
- package/src/mcp/semantic-tools.js +1242 -0
- package/src/node-detail.js +682 -0
- package/src/output.js +111 -0
- package/src/resume-detached.js +37 -0
- package/src/rewind.js +88 -0
- package/src/scheduler.js +112 -0
- package/src/smithersRuntime.js +63 -0
- package/src/supervisor.js +418 -0
- package/src/tree.js +307 -0
- package/src/tui/app.jsx +139 -0
- package/src/tui/app.tsx +5 -0
- package/src/tui/components/AskModal.jsx +109 -0
- package/src/tui/components/AskModal.tsx +3 -0
- package/src/tui/components/AttentionPane.jsx +112 -0
- package/src/tui/components/AttentionPane.tsx +6 -0
- package/src/tui/components/ChatPane.jsx +57 -0
- package/src/tui/components/ChatPane.tsx +7 -0
- package/src/tui/components/CronList.jsx +87 -0
- package/src/tui/components/CronList.tsx +5 -0
- package/src/tui/components/DetailsPane.jsx +96 -0
- package/src/tui/components/DetailsPane.tsx +7 -0
- package/src/tui/components/FramesPane.jsx +147 -0
- package/src/tui/components/FramesPane.tsx +8 -0
- package/src/tui/components/LogsPane.jsx +46 -0
- package/src/tui/components/LogsPane.tsx +6 -0
- package/src/tui/components/MetricsPane.jsx +108 -0
- package/src/tui/components/MetricsPane.tsx +5 -0
- package/src/tui/components/NodeDetailView.jsx +284 -0
- package/src/tui/components/NodeDetailView.tsx +7 -0
- package/src/tui/components/NodeInspector.jsx +51 -0
- package/src/tui/components/NodeInspector.tsx +7 -0
- package/src/tui/components/RunDetailView.jsx +190 -0
- package/src/tui/components/RunDetailView.tsx +7 -0
- package/src/tui/components/RunsList.jsx +184 -0
- package/src/tui/components/RunsList.tsx +7 -0
- package/src/tui/components/SqliteBrowser.jsx +131 -0
- package/src/tui/components/SqliteBrowser.tsx +5 -0
- package/src/tui/components/WorkflowLauncher.jsx +63 -0
- package/src/tui/components/WorkflowLauncher.tsx +3 -0
- package/src/util/CliErrorMapping.ts +7 -0
- package/src/util/CliExitCode.ts +10 -0
- package/src/util/errorMessage.js +212 -0
- package/src/util/exitCodes.js +18 -0
- package/src/watch.js +128 -0
- package/src/why-diagnosis.js +1000 -0
- package/src/workflow-pack.js +2151 -0
- package/src/workflows.js +122 -0
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
|
+
}
|