@krapht/ork 0.2.0 → 0.3.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/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/__build_info__-CXXMLvjt.mjs +104 -0
- package/dist/bin.mjs +859 -3037
- package/dist/context-BFGC_C02.mjs +435 -0
- package/dist/find-BcvhT6UN.mjs +47 -0
- package/dist/format-DqpObyMl.mjs +553 -0
- package/dist/in-a-run-BbLhZOFW.mjs +22 -0
- package/dist/limits-BKYAmqCR.mjs +44 -0
- package/dist/mcp-BWsdQyri.mjs +91 -0
- package/dist/output-Q2EdeVUt.mjs +856 -0
- package/dist/pause-DKACDfV1.mjs +65 -0
- package/dist/relay-CbK-Wme9.mjs +122 -0
- package/dist/report-DK4LGqwF.mjs +129 -0
- package/dist/send-C6yr04Fe.mjs +163 -0
- package/dist/{src-DAsOYtoR.mjs → src-CZOVF2yU.mjs} +496 -193
- package/dist/src-Cz1yOUTg.mjs +11841 -0
- package/dist/src-D5ruZFgD.mjs +1973 -0
- package/dist/src-DtN1k-B3.mjs +1899 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
- package/dist/ui/assets/app-BlDdP396.js +61 -0
- package/dist/ui/assets/app-C722xNZ3.css +1 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/ui/assets/index-BiO54L90.css +1 -0
- package/dist/ui/assets/index-BoG6O-Lp.js +10 -0
- package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +30 -0
- package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
- package/dist/ui/ork-wordmark-small.svg +4 -0
- package/dist/ui-CZS1XKla.mjs +160 -0
- package/dist/usage-BIjf9Xdt.mjs +418 -0
- package/dist/watch-B8o4Szd_.mjs +394 -0
- package/package.json +4 -2
- package/dist/src-CJ_zF0Zj.mjs +0 -5433
|
@@ -1,10 +1,312 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ct as FEEDBACK_SEVERITIES, D as parseReport, St as FEEDBACK_KINDS, wt as FEEDBACK_TARGETS } from "./src-Cz1yOUTg.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
+
// ../runtime-claude/src/normalize.ts
|
|
9
|
+
/**
|
|
10
|
+
* @file Both drivers emit the same message JSON (the Agent SDK's messages are
|
|
11
|
+
* the CLI's `stream-json` lines), so one normalizer serves both.
|
|
12
|
+
*/
|
|
13
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
const get = (value, key) => isObject(value) ? value[key] : void 0;
|
|
15
|
+
const num = (value) => typeof value === "number" ? value : 0;
|
|
16
|
+
const str = (value) => typeof value === "string" ? value : "";
|
|
17
|
+
const blocks = (message) => {
|
|
18
|
+
const content = get(get(message, "message"), "content");
|
|
19
|
+
return Array.isArray(content) ? content.filter((item) => isObject(item)) : [];
|
|
20
|
+
};
|
|
21
|
+
const usageOf = (result) => {
|
|
22
|
+
const usage = get(result, "usage");
|
|
23
|
+
return {
|
|
24
|
+
costUsd: num(result["total_cost_usd"]),
|
|
25
|
+
inputTokens: num(get(usage, "input_tokens")),
|
|
26
|
+
outputTokens: num(get(usage, "output_tokens")),
|
|
27
|
+
cacheReadTokens: num(get(usage, "cache_read_input_tokens")),
|
|
28
|
+
cacheWriteTokens: num(get(usage, "cache_creation_input_tokens")),
|
|
29
|
+
turns: num(result["num_turns"]),
|
|
30
|
+
durationMs: num(result["duration_ms"])
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** The outcome a `result` message describes. */
|
|
34
|
+
const outcomeOf = (result) => {
|
|
35
|
+
const errors = result["errors"];
|
|
36
|
+
return {
|
|
37
|
+
subtype: str(result["subtype"]) || "unknown",
|
|
38
|
+
sessionId: str(result["session_id"]),
|
|
39
|
+
report: parseReport(result["structured_output"]),
|
|
40
|
+
text: str(result["result"]),
|
|
41
|
+
usage: usageOf(result),
|
|
42
|
+
errors: Array.isArray(errors) ? errors.filter((e) => typeof e === "string") : []
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** The year 2286: a reset beyond this is not in seconds. */
|
|
46
|
+
const MAX_RESET_SECONDS = 1e10;
|
|
47
|
+
/**
|
|
48
|
+
* What a subscription's limit said (RFC-007). Only the fields Claude Code
|
|
49
|
+
* documents are read, so `utilization`, often absent, plays no part.
|
|
50
|
+
*/
|
|
51
|
+
const planLimitOf = (info) => {
|
|
52
|
+
const status = get(info, "status");
|
|
53
|
+
if (status !== "allowed" && status !== "allowed_warning" && status !== "rejected") return [];
|
|
54
|
+
const window = get(info, "rateLimitType");
|
|
55
|
+
const resets = get(info, "resetsAt");
|
|
56
|
+
const overage = get(info, "overageStatus");
|
|
57
|
+
return [{
|
|
58
|
+
type: "plan.limit",
|
|
59
|
+
status: status === "rejected" && !(status === "rejected" && overage !== "allowed" && overage !== "allowed_warning") ? "allowed_warning" : status,
|
|
60
|
+
window: typeof window === "string" ? window : "plan",
|
|
61
|
+
...typeof resets === "number" && resets > 0 && resets < MAX_RESET_SECONDS ? { resetsAt: (/* @__PURE__ */ new Date(resets * 1e3)).toISOString() } : {}
|
|
62
|
+
}];
|
|
63
|
+
};
|
|
64
|
+
/** Maps one driver message to zero or more normalized events. */
|
|
65
|
+
const normalize = (message) => {
|
|
66
|
+
if (!isObject(message)) return [];
|
|
67
|
+
switch (message["type"]) {
|
|
68
|
+
case "system": {
|
|
69
|
+
if (message["subtype"] !== "init") return [];
|
|
70
|
+
const started = {
|
|
71
|
+
type: "session.started",
|
|
72
|
+
sessionId: str(message["session_id"]),
|
|
73
|
+
model: str(message["model"])
|
|
74
|
+
};
|
|
75
|
+
if (str(message["cwd"]) !== "") started.cwd = str(message["cwd"]);
|
|
76
|
+
return [started];
|
|
77
|
+
}
|
|
78
|
+
case "assistant": return blocks(message).flatMap((block) => {
|
|
79
|
+
if (block["type"] === "text" && str(block["text"]).trim() !== "") return [{
|
|
80
|
+
type: "assistant.text",
|
|
81
|
+
text: str(block["text"])
|
|
82
|
+
}];
|
|
83
|
+
if (block["type"] === "tool_use") {
|
|
84
|
+
const input = block["input"];
|
|
85
|
+
return [{
|
|
86
|
+
type: "tool.called",
|
|
87
|
+
id: str(block["id"]),
|
|
88
|
+
name: str(block["name"]),
|
|
89
|
+
input: isObject(input) ? input : {}
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
});
|
|
94
|
+
case "user": return blocks(message).filter((block) => block["type"] === "tool_result").map((block) => ({
|
|
95
|
+
type: "tool.result",
|
|
96
|
+
id: str(block["tool_use_id"]),
|
|
97
|
+
isError: block["is_error"] === true
|
|
98
|
+
}));
|
|
99
|
+
case "rate_limit_event": return planLimitOf(message["rate_limit_info"]);
|
|
100
|
+
case "result": return [{
|
|
101
|
+
type: "session.finished",
|
|
102
|
+
outcome: outcomeOf(message)
|
|
103
|
+
}];
|
|
104
|
+
default: return [];
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/** Marks a session's start with the driver that runs it. */
|
|
108
|
+
const withDriver = (event, driver) => event.type === "session.started" ? {
|
|
109
|
+
...event,
|
|
110
|
+
driver
|
|
111
|
+
} : event;
|
|
112
|
+
/** A finished event for a session the driver could not run. */
|
|
113
|
+
const driverFailure = (message) => ({
|
|
114
|
+
type: "session.finished",
|
|
115
|
+
outcome: {
|
|
116
|
+
subtype: "error_driver",
|
|
117
|
+
sessionId: "",
|
|
118
|
+
report: null,
|
|
119
|
+
text: "",
|
|
120
|
+
usage: {
|
|
121
|
+
costUsd: 0,
|
|
122
|
+
inputTokens: 0,
|
|
123
|
+
outputTokens: 0,
|
|
124
|
+
cacheReadTokens: 0,
|
|
125
|
+
cacheWriteTokens: 0,
|
|
126
|
+
turns: 0,
|
|
127
|
+
durationMs: 0
|
|
128
|
+
},
|
|
129
|
+
errors: [message]
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
/** How long a hook holds a call before it defers it instead. */
|
|
133
|
+
const HOLD_WINDOW_MS = 54e4;
|
|
134
|
+
/**
|
|
135
|
+
* Waits at a tool call while `hold` is held: "go" once it is released within
|
|
136
|
+
* the window (or at once when it is not held), "defer" after the window.
|
|
137
|
+
*/
|
|
138
|
+
const holdAtTool = async (hold, signal, windowMs = HOLD_WINDOW_MS) => {
|
|
139
|
+
if (!hold.held()) return "go";
|
|
140
|
+
const window = new AbortController();
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
window.abort();
|
|
143
|
+
}, windowMs);
|
|
144
|
+
try {
|
|
145
|
+
await hold.wait("tool", AbortSignal.any([signal, window.signal]));
|
|
146
|
+
} finally {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
}
|
|
149
|
+
return hold.held() && !signal.aborted ? "defer" : "go";
|
|
150
|
+
};
|
|
151
|
+
/** The hook's answer: nothing to say for "go", the deferral otherwise. */
|
|
152
|
+
const hookOutput = (answer) => answer === "go" ? {} : { hookSpecificOutput: {
|
|
153
|
+
hookEventName: "PreToolUse",
|
|
154
|
+
permissionDecision: "defer",
|
|
155
|
+
permissionDecisionReason: "Ork paused this node; it goes on from this call when it continues."
|
|
156
|
+
} };
|
|
157
|
+
/** Whether a stream-json message is the result of a deferred tool call. */
|
|
158
|
+
const isDeferredResult = (message) => typeof message === "object" && message !== null && "type" in message && message.type === "result" && "stop_reason" in message && message.stop_reason === "tool_deferred";
|
|
159
|
+
/**
|
|
160
|
+
* Two legs of one session, as one: the tokens of both, and the later leg's
|
|
161
|
+
* cost, which is already the session's total (ADR-046).
|
|
162
|
+
*/
|
|
163
|
+
const addUsage = (a, b) => ({
|
|
164
|
+
costUsd: Math.max(a.costUsd, b.costUsd),
|
|
165
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
166
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
167
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
168
|
+
cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
|
|
169
|
+
turns: a.turns + b.turns,
|
|
170
|
+
durationMs: a.durationMs + b.durationMs
|
|
171
|
+
});
|
|
172
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
|
+
/**
|
|
174
|
+
* The node's `--settings` with Ork's `PreToolUse` HTTP hook added: before every
|
|
175
|
+
* tool call, pre-approved ones included, the session asks whether its node may
|
|
176
|
+
* go on.
|
|
177
|
+
*/
|
|
178
|
+
const withHoldHook = (settings, holdUrl, token) => {
|
|
179
|
+
const hooks = isRecord(settings?.["hooks"]) ? settings["hooks"] : {};
|
|
180
|
+
const listed = hooks["PreToolUse"];
|
|
181
|
+
const before = Array.isArray(listed) ? [...listed] : [];
|
|
182
|
+
const hook = {
|
|
183
|
+
type: "http",
|
|
184
|
+
url: holdUrl,
|
|
185
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
186
|
+
timeout: 600
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
...settings,
|
|
190
|
+
hooks: {
|
|
191
|
+
...hooks,
|
|
192
|
+
PreToolUse: [...before, {
|
|
193
|
+
matcher: "*",
|
|
194
|
+
hooks: [hook]
|
|
195
|
+
}]
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* What one stream-json message of a leg yields, and how the leg ends if it
|
|
201
|
+
* does: a result is the session's end, unless the hold hook deferred a call in
|
|
202
|
+
* this leg; a resumed leg's start is not news, and its end carries what the
|
|
203
|
+
* legs before it spent.
|
|
204
|
+
*/
|
|
205
|
+
const legStep = (message, input) => {
|
|
206
|
+
const deferred = input.deferred && isDeferredResult(message);
|
|
207
|
+
const events = [];
|
|
208
|
+
let ended;
|
|
209
|
+
for (const event of normalize(message)) {
|
|
210
|
+
if (event.type === "session.started" && input.carried !== void 0) continue;
|
|
211
|
+
if (event.type !== "session.finished") events.push(withDriver(event, input.driver));
|
|
212
|
+
else if (deferred) ended = {
|
|
213
|
+
kind: "deferred",
|
|
214
|
+
sessionId: event.outcome.sessionId,
|
|
215
|
+
usage: event.outcome.usage,
|
|
216
|
+
last: withDriver(event, input.driver)
|
|
217
|
+
};
|
|
218
|
+
else {
|
|
219
|
+
const usage = input.carried === void 0 ? event.outcome.usage : addUsage(input.carried, event.outcome.usage);
|
|
220
|
+
events.push(withDriver({
|
|
221
|
+
...event,
|
|
222
|
+
outcome: {
|
|
223
|
+
...event.outcome,
|
|
224
|
+
usage
|
|
225
|
+
}
|
|
226
|
+
}, input.driver));
|
|
227
|
+
ended = { kind: "finished" };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return ended === void 0 ? { events } : {
|
|
231
|
+
events,
|
|
232
|
+
ended
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
// ../runtime-claude/src/cli-leg.ts
|
|
236
|
+
/**
|
|
237
|
+
* @file One `claude -p` process of a CLI-driven session: it writes the queued
|
|
238
|
+
* stream-json lines, yields the normalized events, and says how it ended. A
|
|
239
|
+
* session is one leg, or several when a paused node's tool call was deferred
|
|
240
|
+
* and the session resumed (ADR-041); a resumed leg carries what the legs
|
|
241
|
+
* before it spent, and its start is not news.
|
|
242
|
+
*/
|
|
243
|
+
const STDERR_LINES = 20;
|
|
244
|
+
const parseLine = (line) => {
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(line);
|
|
247
|
+
} catch {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
/** Runs one `claude` process, yielding its events; returns how it ended. */
|
|
252
|
+
const runLeg = async function* (input) {
|
|
253
|
+
const child = spawn(input.bin, input.args, {
|
|
254
|
+
cwd: input.cwd,
|
|
255
|
+
env: { ...input.env },
|
|
256
|
+
stdio: [
|
|
257
|
+
"pipe",
|
|
258
|
+
"pipe",
|
|
259
|
+
"pipe"
|
|
260
|
+
]
|
|
261
|
+
});
|
|
262
|
+
const exited = new Promise((resolve) => {
|
|
263
|
+
child.on("close", (code) => {
|
|
264
|
+
resolve(code);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
const spawnError = new Promise((resolve) => {
|
|
268
|
+
child.on("error", resolve);
|
|
269
|
+
});
|
|
270
|
+
const stderr = [];
|
|
271
|
+
child.stderr.setEncoding("utf8");
|
|
272
|
+
child.stderr.on("data", (chunk) => {
|
|
273
|
+
stderr.push(...chunk.split("\n").filter((l) => l.trim() !== ""));
|
|
274
|
+
stderr.splice(0, Math.max(0, stderr.length - STDERR_LINES));
|
|
275
|
+
});
|
|
276
|
+
const write = (line) => {
|
|
277
|
+
if (child.stdin.writable) child.stdin.write(line);
|
|
278
|
+
};
|
|
279
|
+
input.attach({
|
|
280
|
+
write,
|
|
281
|
+
stop: () => {
|
|
282
|
+
child.kill("SIGINT");
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
for (const line of input.pending.splice(0)) write(line);
|
|
286
|
+
let outcome;
|
|
287
|
+
try {
|
|
288
|
+
for await (const line of createInterface({ input: child.stdout })) {
|
|
289
|
+
const { events, ended } = legStep(parseLine(line), {
|
|
290
|
+
carried: input.carried,
|
|
291
|
+
deferred: input.deferred(),
|
|
292
|
+
driver: "cli"
|
|
293
|
+
});
|
|
294
|
+
yield* events;
|
|
295
|
+
if (ended !== void 0) {
|
|
296
|
+
child.stdin.end();
|
|
297
|
+
outcome = ended;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const code = await Promise.race([exited, spawnError.then((error) => error)]);
|
|
301
|
+
return outcome ?? {
|
|
302
|
+
kind: "failed",
|
|
303
|
+
reason: code instanceof Error ? `could not start ${input.bin}: ${code.message}` : stderr.at(-1) ?? `claude exited with code ${String(code)}`
|
|
304
|
+
};
|
|
305
|
+
} finally {
|
|
306
|
+
input.attach();
|
|
307
|
+
if (child.exitCode === null) child.kill();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
8
310
|
/** The name Claude Code gives an Ork tool: `mcp__ork__<name>`. */
|
|
9
311
|
const toolId = (name) => `mcp__ork__${name}`;
|
|
10
312
|
const text = (value) => ({ content: [{
|
|
@@ -30,9 +332,11 @@ const defineTool = (definition) => {
|
|
|
30
332
|
* @file A minimal MCP server over the streamable HTTP transport, for the
|
|
31
333
|
* `claude` CLI driver. It answers with plain JSON (no SSE stream), which the
|
|
32
334
|
* transport allows, and implements only what Ork needs: `initialize`, `ping`,
|
|
33
|
-
* `tools/list` and `tools/call`.
|
|
335
|
+
* `tools/list` and `tools/call`. Beside it, `/hold` answers the session's
|
|
336
|
+
* `PreToolUse` HTTP hook, which waits while the node is paused (ADR-041).
|
|
34
337
|
*/
|
|
35
338
|
const PATH = "/mcp";
|
|
339
|
+
const HOLD_PATH = "/hold";
|
|
36
340
|
const MAX_BODY = 1048576;
|
|
37
341
|
const MCP_PROTOCOL_FALLBACK = "2025-06-18";
|
|
38
342
|
const JsonRpcSchema = z.object({
|
|
@@ -115,11 +419,32 @@ const respond = (res, status, body) => {
|
|
|
115
419
|
}
|
|
116
420
|
res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
117
421
|
};
|
|
118
|
-
|
|
422
|
+
/** The hook's POST: waits for `hold`; a closed connection gives up. */
|
|
423
|
+
const serveHold = async (req, res, hold) => {
|
|
424
|
+
if (req.method !== "POST") {
|
|
425
|
+
respond(res, 405);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (await readBody(req) === null) {
|
|
429
|
+
respond(res, 413);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const gone = new AbortController();
|
|
433
|
+
res.once("close", () => {
|
|
434
|
+
gone.abort();
|
|
435
|
+
});
|
|
436
|
+
const output = await hold(gone.signal);
|
|
437
|
+
if (!gone.signal.aborted) respond(res, 200, output);
|
|
438
|
+
};
|
|
439
|
+
const serve = async (req, res, tools, token, hold) => {
|
|
119
440
|
if (!authorized(req, token)) {
|
|
120
441
|
respond(res, 401);
|
|
121
442
|
return;
|
|
122
443
|
}
|
|
444
|
+
if (req.url === HOLD_PATH && hold !== void 0) {
|
|
445
|
+
await serveHold(req, res, hold);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
123
448
|
if (req.url !== PATH) {
|
|
124
449
|
respond(res, 404);
|
|
125
450
|
return;
|
|
@@ -156,10 +481,10 @@ const serve = async (req, res, tools, token) => {
|
|
|
156
481
|
* Serves Ork's tools to one `claude` CLI session: on 127.0.0.1 only, on a
|
|
157
482
|
* random port, behind a per-session bearer token.
|
|
158
483
|
*/
|
|
159
|
-
const startOrkMcpHttp = async (tools) => {
|
|
484
|
+
const startOrkMcpHttp = async (tools, hold) => {
|
|
160
485
|
const token = randomBytes(24).toString("hex");
|
|
161
486
|
const http = createServer((req, res) => {
|
|
162
|
-
serve(req, res, tools, token).catch(() => {
|
|
487
|
+
serve(req, res, tools, token, hold).catch(() => {
|
|
163
488
|
if (!res.headersSent) respond(res, 500);
|
|
164
489
|
});
|
|
165
490
|
});
|
|
@@ -167,8 +492,10 @@ const startOrkMcpHttp = async (tools) => {
|
|
|
167
492
|
http.listen(0, "127.0.0.1", resolve);
|
|
168
493
|
});
|
|
169
494
|
const address = http.address();
|
|
495
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
170
496
|
return {
|
|
171
|
-
url: `http://127.0.0.1:${
|
|
497
|
+
url: `http://127.0.0.1:${port}${PATH}`,
|
|
498
|
+
holdUrl: `http://127.0.0.1:${port}${HOLD_PATH}`,
|
|
172
499
|
token,
|
|
173
500
|
close: () => new Promise((resolve) => {
|
|
174
501
|
http.closeAllConnections();
|
|
@@ -178,110 +505,6 @@ const startOrkMcpHttp = async (tools) => {
|
|
|
178
505
|
})
|
|
179
506
|
};
|
|
180
507
|
};
|
|
181
|
-
// ../runtime-claude/src/normalize.ts
|
|
182
|
-
/**
|
|
183
|
-
* @file Both drivers emit the same message JSON (the Agent SDK's messages are
|
|
184
|
-
* the CLI's `stream-json` lines), so one normalizer serves both.
|
|
185
|
-
*/
|
|
186
|
-
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
187
|
-
const get = (value, key) => isObject(value) ? value[key] : void 0;
|
|
188
|
-
const num = (value) => typeof value === "number" ? value : 0;
|
|
189
|
-
const str = (value) => typeof value === "string" ? value : "";
|
|
190
|
-
const blocks = (message) => {
|
|
191
|
-
const content = get(get(message, "message"), "content");
|
|
192
|
-
return Array.isArray(content) ? content.filter((item) => isObject(item)) : [];
|
|
193
|
-
};
|
|
194
|
-
const usageOf = (result) => {
|
|
195
|
-
const usage = get(result, "usage");
|
|
196
|
-
return {
|
|
197
|
-
costUsd: num(result["total_cost_usd"]),
|
|
198
|
-
inputTokens: num(get(usage, "input_tokens")),
|
|
199
|
-
outputTokens: num(get(usage, "output_tokens")),
|
|
200
|
-
cacheReadTokens: num(get(usage, "cache_read_input_tokens")),
|
|
201
|
-
cacheWriteTokens: num(get(usage, "cache_creation_input_tokens")),
|
|
202
|
-
turns: num(result["num_turns"]),
|
|
203
|
-
durationMs: num(result["duration_ms"])
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
/** The outcome a `result` message describes. */
|
|
207
|
-
const outcomeOf = (result) => {
|
|
208
|
-
const errors = result["errors"];
|
|
209
|
-
return {
|
|
210
|
-
subtype: str(result["subtype"]) || "unknown",
|
|
211
|
-
sessionId: str(result["session_id"]),
|
|
212
|
-
report: parseReport(result["structured_output"]),
|
|
213
|
-
text: str(result["result"]),
|
|
214
|
-
usage: usageOf(result),
|
|
215
|
-
errors: Array.isArray(errors) ? errors.filter((e) => typeof e === "string") : []
|
|
216
|
-
};
|
|
217
|
-
};
|
|
218
|
-
/** Maps one driver message to zero or more normalized events. */
|
|
219
|
-
const normalize = (message) => {
|
|
220
|
-
if (!isObject(message)) return [];
|
|
221
|
-
switch (message["type"]) {
|
|
222
|
-
case "system": {
|
|
223
|
-
if (message["subtype"] !== "init") return [];
|
|
224
|
-
const started = {
|
|
225
|
-
type: "session.started",
|
|
226
|
-
sessionId: str(message["session_id"]),
|
|
227
|
-
model: str(message["model"])
|
|
228
|
-
};
|
|
229
|
-
if (str(message["cwd"]) !== "") started.cwd = str(message["cwd"]);
|
|
230
|
-
return [started];
|
|
231
|
-
}
|
|
232
|
-
case "assistant": return blocks(message).flatMap((block) => {
|
|
233
|
-
if (block["type"] === "text" && str(block["text"]).trim() !== "") return [{
|
|
234
|
-
type: "assistant.text",
|
|
235
|
-
text: str(block["text"])
|
|
236
|
-
}];
|
|
237
|
-
if (block["type"] === "tool_use") {
|
|
238
|
-
const input = block["input"];
|
|
239
|
-
return [{
|
|
240
|
-
type: "tool.called",
|
|
241
|
-
id: str(block["id"]),
|
|
242
|
-
name: str(block["name"]),
|
|
243
|
-
input: isObject(input) ? input : {}
|
|
244
|
-
}];
|
|
245
|
-
}
|
|
246
|
-
return [];
|
|
247
|
-
});
|
|
248
|
-
case "user": return blocks(message).filter((block) => block["type"] === "tool_result").map((block) => ({
|
|
249
|
-
type: "tool.result",
|
|
250
|
-
id: str(block["tool_use_id"]),
|
|
251
|
-
isError: block["is_error"] === true
|
|
252
|
-
}));
|
|
253
|
-
case "result": return [{
|
|
254
|
-
type: "session.finished",
|
|
255
|
-
outcome: outcomeOf(message)
|
|
256
|
-
}];
|
|
257
|
-
default: return [];
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
/** Marks a session's start with the driver that runs it. */
|
|
261
|
-
const withDriver = (event, driver) => event.type === "session.started" ? {
|
|
262
|
-
...event,
|
|
263
|
-
driver
|
|
264
|
-
} : event;
|
|
265
|
-
/** A finished event for a session the driver could not run. */
|
|
266
|
-
const driverFailure = (message) => ({
|
|
267
|
-
type: "session.finished",
|
|
268
|
-
outcome: {
|
|
269
|
-
subtype: "error_driver",
|
|
270
|
-
sessionId: "",
|
|
271
|
-
report: null,
|
|
272
|
-
text: "",
|
|
273
|
-
usage: {
|
|
274
|
-
costUsd: 0,
|
|
275
|
-
inputTokens: 0,
|
|
276
|
-
outputTokens: 0,
|
|
277
|
-
cacheReadTokens: 0,
|
|
278
|
-
cacheWriteTokens: 0,
|
|
279
|
-
turns: 0,
|
|
280
|
-
durationMs: 0
|
|
281
|
-
},
|
|
282
|
-
errors: [message]
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
508
|
// ../runtime-claude/src/tools/ask-user.ts
|
|
286
509
|
const askUserTool = defineTool({
|
|
287
510
|
name: "ask_user",
|
|
@@ -428,13 +651,25 @@ const feedbackTool = defineTool({
|
|
|
428
651
|
* session gets a one-line notice; `answer` replies to a question Ork asked an
|
|
429
652
|
* owner about a node it owns. Only sessions inside a run get them.
|
|
430
653
|
*/
|
|
431
|
-
/**
|
|
432
|
-
|
|
654
|
+
/**
|
|
655
|
+
* Peers are mutual: a node no one may write to has no inbox to read, unless it
|
|
656
|
+
* may be linked later, since a session's tools are set when it starts.
|
|
657
|
+
*/
|
|
658
|
+
const hasPeers = ({ ports }) => (ports.mailbox?.peers().length ?? 0) > 0 || ports.mailbox?.linkable() === true;
|
|
659
|
+
/** A program or a rule may write to a node with no one to write to itself. */
|
|
660
|
+
const hasInbox = (context) => hasPeers(context) || context.ports.mailbox?.hears() === true;
|
|
661
|
+
const MAX_WAIT_SECONDS = 3600;
|
|
662
|
+
const DEFAULT_WAIT_SECONDS = 300;
|
|
663
|
+
const inboxText = (unread) => JSON.stringify({ messages: unread.map((m) => ({
|
|
664
|
+
from: m.from,
|
|
665
|
+
...m.via === void 0 ? {} : { via: m.via },
|
|
666
|
+
body: m.body
|
|
667
|
+
})) });
|
|
433
668
|
const sendMessageTool = defineTool({
|
|
434
669
|
name: "send_message",
|
|
435
|
-
description: "Send a short message to
|
|
670
|
+
description: "Send a short message to a node you can reach: your owner, a node you own, one you share a declared channel with, or a node of another run the user linked you to. It sees a notice and reads the message with read_inbox. Use it for what they need to know now; your report still goes to your owner at the end.",
|
|
436
671
|
shape: {
|
|
437
|
-
to: z.string().describe("The node's id."),
|
|
672
|
+
to: z.string().describe("The node's id, or <run>/<node> for a linked node."),
|
|
438
673
|
body: z.string().min(1).describe("Self-contained: the recipient does not see your work.")
|
|
439
674
|
},
|
|
440
675
|
available: hasPeers,
|
|
@@ -442,20 +677,29 @@ const sendMessageTool = defineTool({
|
|
|
442
677
|
const sent = ports.mailbox?.send(args.to, args.body);
|
|
443
678
|
if (sent === void 0) return Promise.resolve(text("No message bus."));
|
|
444
679
|
if (!sent.ok) return Promise.resolve(text(`Not sent: ${sent.error}.`));
|
|
445
|
-
return Promise.resolve(text(sent.delivered === "now" ? `Sent to ${args.to}; it sees a notice at its next step.` : `Sent to ${args.to}; it is not running now and sees the message
|
|
680
|
+
return Promise.resolve(text(sent.delivered === "now" ? `Sent to ${args.to}; it sees a notice at its next step.` : `Sent to ${args.to}; it is not running now and sees the message if it starts again.`));
|
|
446
681
|
}
|
|
447
682
|
});
|
|
448
683
|
const readInboxTool = defineTool({
|
|
449
684
|
name: "read_inbox",
|
|
450
|
-
description: "Read your unread messages from other nodes
|
|
685
|
+
description: "Read your unread messages from other nodes, oldest first. Ork tells you when one arrives.",
|
|
451
686
|
shape: {},
|
|
452
|
-
available:
|
|
687
|
+
available: hasInbox,
|
|
453
688
|
run: ({ ports }) => {
|
|
454
689
|
const unread = ports.mailbox?.read() ?? [];
|
|
455
|
-
return Promise.resolve(text(unread.length === 0 ? "No unread messages." :
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
690
|
+
return Promise.resolve(text(unread.length === 0 ? "No unread messages." : inboxText(unread)));
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
const waitForMessagesTool = defineTool({
|
|
694
|
+
name: "wait_for_messages",
|
|
695
|
+
description: "Wait for a message from another node when you cannot go on without its reply, instead of ending your session. Returns what arrived, or says none did in time. You hold no session slot while you wait.",
|
|
696
|
+
shape: { timeoutSeconds: z.number().int().positive().max(MAX_WAIT_SECONDS).optional().describe(`How long to wait. Default: ${String(DEFAULT_WAIT_SECONDS)}.`) },
|
|
697
|
+
available: hasInbox,
|
|
698
|
+
run: async ({ ports }, args) => {
|
|
699
|
+
const seconds = args.timeoutSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
700
|
+
await ports.mailbox?.wait(seconds * 1e3);
|
|
701
|
+
const unread = ports.mailbox?.read() ?? [];
|
|
702
|
+
return text(unread.length === 0 ? `No message yet, after waiting up to ${String(seconds)} seconds. Ork may have told you something; if not, wait again or go on.` : inboxText(unread));
|
|
459
703
|
}
|
|
460
704
|
});
|
|
461
705
|
const answerTool = defineTool({
|
|
@@ -508,6 +752,7 @@ const ORK_TOOLS = [
|
|
|
508
752
|
spawnWorkerTool,
|
|
509
753
|
sendMessageTool,
|
|
510
754
|
readInboxTool,
|
|
755
|
+
waitForMessagesTool,
|
|
511
756
|
answerTool,
|
|
512
757
|
claimTool,
|
|
513
758
|
releaseTool,
|
|
@@ -519,11 +764,6 @@ const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ??
|
|
|
519
764
|
const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
|
|
520
765
|
// ../runtime-claude/src/cli.ts
|
|
521
766
|
/**
|
|
522
|
-
* @file The CLI driver: runs a session through the user's own `claude -p` in
|
|
523
|
-
* stream-json mode, with Ork's tools served by a local MCP server.
|
|
524
|
-
*/
|
|
525
|
-
const STDERR_LINES = 20;
|
|
526
|
-
/**
|
|
527
767
|
* Per-request timeout for Ork's MCP server, in ms. The CLI otherwise gives an
|
|
528
768
|
* HTTP server's request 60 seconds and drops a call silent for 5 minutes, and
|
|
529
769
|
* `delegate` waits for whole child sessions (the per-server `timeout` raises
|
|
@@ -582,16 +822,10 @@ const buildCliArgs = (spec, mcp, allowed) => {
|
|
|
582
822
|
if (spec.budgetUsd !== void 0) args.push("--max-budget-usd", String(spec.budgetUsd));
|
|
583
823
|
if (spec.resume !== void 0) args.push("--resume", spec.resume);
|
|
584
824
|
for (const dir of spec.plugins ?? []) args.push("--plugin-dir", dir);
|
|
585
|
-
|
|
825
|
+
const settings = mcp.holdUrl === void 0 ? spec.settings : withHoldHook(spec.settings, mcp.holdUrl, mcp.token);
|
|
826
|
+
if (settings !== void 0) args.push("--settings", JSON.stringify(settings));
|
|
586
827
|
return args;
|
|
587
828
|
};
|
|
588
|
-
const parseLine = (line) => {
|
|
589
|
-
try {
|
|
590
|
-
return JSON.parse(line);
|
|
591
|
-
} catch {
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
829
|
/**
|
|
596
830
|
* Drives a session with the user's own `claude` CLI in print mode, logged in by
|
|
597
831
|
* the user. Ork never reads or passes its credentials.
|
|
@@ -600,63 +834,62 @@ const createCliRuntime = (options = {}) => ({
|
|
|
600
834
|
driver: "cli",
|
|
601
835
|
start: (spec, ports) => {
|
|
602
836
|
const pending = [userLine(spec.prompt)];
|
|
603
|
-
let
|
|
604
|
-
|
|
837
|
+
let io;
|
|
838
|
+
const ended = new AbortController();
|
|
839
|
+
/** The hold hook deferred a call in the running process. */
|
|
840
|
+
let deferring = false;
|
|
841
|
+
const { hold } = ports;
|
|
605
842
|
const events = async function* () {
|
|
606
843
|
const tools = orkTools({
|
|
607
844
|
nodeId: spec.nodeId,
|
|
608
845
|
driver: "cli",
|
|
609
846
|
ports
|
|
610
847
|
});
|
|
611
|
-
const mcp = await startOrkMcpHttp(tools)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
env: { ...options.env ?? process.env },
|
|
616
|
-
stdio: [
|
|
617
|
-
"pipe",
|
|
618
|
-
"pipe",
|
|
619
|
-
"pipe"
|
|
620
|
-
]
|
|
621
|
-
});
|
|
622
|
-
const exited = new Promise((resolve) => {
|
|
623
|
-
child.on("close", (code) => {
|
|
624
|
-
resolve(code);
|
|
625
|
-
});
|
|
626
|
-
});
|
|
627
|
-
const spawnError = new Promise((resolve) => {
|
|
628
|
-
child.on("error", resolve);
|
|
848
|
+
const mcp = await startOrkMcpHttp(tools, hold === void 0 ? void 0 : async (gone) => {
|
|
849
|
+
const answer = await holdAtTool(hold, AbortSignal.any([gone, ended.signal]), options.holdWindowMs);
|
|
850
|
+
if (answer === "defer") deferring = true;
|
|
851
|
+
return hookOutput(answer);
|
|
629
852
|
});
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
853
|
+
const allowed = preapprovedTools(tools);
|
|
854
|
+
const endpoint = hold === void 0 ? {
|
|
855
|
+
url: mcp.url,
|
|
856
|
+
token: mcp.token
|
|
857
|
+
} : mcp;
|
|
858
|
+
const legOf = (args, carried) => runLeg({
|
|
859
|
+
bin: options.bin ?? "claude",
|
|
860
|
+
env: options.env ?? process.env,
|
|
861
|
+
cwd: spec.cwd,
|
|
862
|
+
args,
|
|
863
|
+
carried,
|
|
864
|
+
pending,
|
|
865
|
+
deferred: () => deferring,
|
|
866
|
+
attach: (attached) => {
|
|
867
|
+
io = attached;
|
|
868
|
+
}
|
|
635
869
|
});
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
};
|
|
639
|
-
stop = () => {
|
|
640
|
-
child.kill("SIGINT");
|
|
641
|
-
};
|
|
642
|
-
for (const line of pending.splice(0)) write(line);
|
|
643
|
-
let finished = false;
|
|
870
|
+
let args = buildCliArgs(spec, endpoint, allowed);
|
|
871
|
+
let spent;
|
|
644
872
|
try {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
873
|
+
for (;;) {
|
|
874
|
+
const leg = yield* legOf(args, spent);
|
|
875
|
+
if (leg.kind === "failed") {
|
|
876
|
+
yield driverFailure(leg.reason);
|
|
877
|
+
return;
|
|
650
878
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
879
|
+
if (leg.kind === "finished") return;
|
|
880
|
+
deferring = false;
|
|
881
|
+
spent = spent === void 0 ? leg.usage : addUsage(spent, leg.usage);
|
|
882
|
+
await hold?.wait("tool", ended.signal);
|
|
883
|
+
if (ended.signal.aborted) {
|
|
884
|
+
yield leg.last;
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
args = buildCliArgs({
|
|
888
|
+
...spec,
|
|
889
|
+
resume: leg.sessionId
|
|
890
|
+
}, endpoint, allowed);
|
|
657
891
|
}
|
|
658
892
|
} finally {
|
|
659
|
-
if (child.exitCode === null) child.kill();
|
|
660
893
|
await mcp.close();
|
|
661
894
|
}
|
|
662
895
|
};
|
|
@@ -664,11 +897,12 @@ const createCliRuntime = (options = {}) => ({
|
|
|
664
897
|
events: events(),
|
|
665
898
|
send: (text) => {
|
|
666
899
|
const line = userLine(text, true);
|
|
667
|
-
if (
|
|
668
|
-
else write(line);
|
|
900
|
+
if (io === void 0) pending.push(line);
|
|
901
|
+
else io.write(line);
|
|
669
902
|
},
|
|
670
903
|
interrupt: () => {
|
|
671
|
-
|
|
904
|
+
ended.abort();
|
|
905
|
+
io?.stop();
|
|
672
906
|
return Promise.resolve();
|
|
673
907
|
}
|
|
674
908
|
};
|
|
@@ -721,7 +955,10 @@ const createQueue = () => {
|
|
|
721
955
|
// ../runtime-claude/src/sdk.ts
|
|
722
956
|
/**
|
|
723
957
|
* @file The SDK driver: runs a session in-process with the Claude Agent SDK,
|
|
724
|
-
* only with an API key or a cloud provider, never a claude.ai login.
|
|
958
|
+
* only with an API key or a cloud provider, never a claude.ai login. A node
|
|
959
|
+
* that can be paused gets a `PreToolUse` callback hook: a short pause holds
|
|
960
|
+
* the call, a long one defers it and resumes the session when the node
|
|
961
|
+
* continues (ADR-041).
|
|
725
962
|
*/
|
|
726
963
|
/**
|
|
727
964
|
* The SDK driver runs only with an API key or a cloud provider. It must never
|
|
@@ -747,8 +984,24 @@ const userMessage = (text, later = false) => ({
|
|
|
747
984
|
...later ? { priority: "next" } : {}
|
|
748
985
|
});
|
|
749
986
|
const definedEnv = (env) => Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
|
|
987
|
+
/**
|
|
988
|
+
* The hold hook: waits before each tool call while the node is paused, and
|
|
989
|
+
* defers the call (`onDefer`) when the pause outlasts the window.
|
|
990
|
+
*/
|
|
991
|
+
const holdHook = (hold, onDefer, windowMs) => ({
|
|
992
|
+
timeout: 600,
|
|
993
|
+
hooks: [async (_input, _toolUseId, { signal }) => {
|
|
994
|
+
if (await holdAtTool(hold, signal, windowMs) === "go") return {};
|
|
995
|
+
onDefer();
|
|
996
|
+
return { hookSpecificOutput: {
|
|
997
|
+
hookEventName: "PreToolUse",
|
|
998
|
+
permissionDecision: "defer",
|
|
999
|
+
permissionDecisionReason: "Ork paused this node; it goes on from this call when it continues."
|
|
1000
|
+
} };
|
|
1001
|
+
}]
|
|
1002
|
+
});
|
|
750
1003
|
/** The Agent SDK options for a session spec. */
|
|
751
|
-
const buildSdkOptions = (spec, ports, env) => {
|
|
1004
|
+
const buildSdkOptions = (spec, ports, env, held) => {
|
|
752
1005
|
const { human } = ports;
|
|
753
1006
|
const tools = orkTools({
|
|
754
1007
|
nodeId: spec.nodeId,
|
|
@@ -804,28 +1057,77 @@ const buildSdkOptions = (spec, ports, env) => {
|
|
|
804
1057
|
path: dir
|
|
805
1058
|
}));
|
|
806
1059
|
if (spec.settings !== void 0) options.settings = spec.settings;
|
|
1060
|
+
if (ports.hold !== void 0 && held !== void 0) options.hooks = { PreToolUse: [holdHook(ports.hold, held.onDefer, held.windowMs)] };
|
|
807
1061
|
return options;
|
|
808
1062
|
};
|
|
809
1063
|
/** Drives a session with the Claude Agent SDK, in this process. */
|
|
810
|
-
const createSdkRuntime = (env = process.env) => ({
|
|
1064
|
+
const createSdkRuntime = (env = process.env, holdWindowMs) => ({
|
|
811
1065
|
driver: "sdk",
|
|
812
1066
|
start: (spec, ports) => {
|
|
813
|
-
|
|
1067
|
+
let input = createQueue();
|
|
814
1068
|
input.push(userMessage(spec.prompt));
|
|
815
1069
|
let current;
|
|
1070
|
+
const ended = new AbortController();
|
|
1071
|
+
/** The hold hook deferred a call in the current query. */
|
|
1072
|
+
let deferring = false;
|
|
1073
|
+
const held = {
|
|
1074
|
+
onDefer: () => {
|
|
1075
|
+
deferring = true;
|
|
1076
|
+
},
|
|
1077
|
+
...holdWindowMs === void 0 ? {} : { windowMs: holdWindowMs }
|
|
1078
|
+
};
|
|
1079
|
+
/** One query: a leg of the session, until its result. */
|
|
1080
|
+
const leg = async function* (resume, carried) {
|
|
1081
|
+
deferring = false;
|
|
1082
|
+
const legSpec = resume === void 0 ? spec : {
|
|
1083
|
+
...spec,
|
|
1084
|
+
resume
|
|
1085
|
+
};
|
|
1086
|
+
current = query({
|
|
1087
|
+
prompt: input,
|
|
1088
|
+
options: buildSdkOptions(legSpec, ports, env, held)
|
|
1089
|
+
});
|
|
1090
|
+
let outcome = {
|
|
1091
|
+
kind: "failed",
|
|
1092
|
+
reason: "the session ended early"
|
|
1093
|
+
};
|
|
1094
|
+
for await (const message of current) {
|
|
1095
|
+
const { events, ended: end } = legStep(message, {
|
|
1096
|
+
carried,
|
|
1097
|
+
deferred: deferring,
|
|
1098
|
+
driver: "sdk"
|
|
1099
|
+
});
|
|
1100
|
+
yield* events;
|
|
1101
|
+
if (end !== void 0) {
|
|
1102
|
+
input.close();
|
|
1103
|
+
outcome = end;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return outcome;
|
|
1107
|
+
};
|
|
816
1108
|
const events = async function* () {
|
|
817
1109
|
if (sdkCredentials(env) === null) {
|
|
818
1110
|
yield driverFailure(NO_SDK_CREDENTIALS);
|
|
819
1111
|
return;
|
|
820
1112
|
}
|
|
1113
|
+
let resume = spec.resume;
|
|
1114
|
+
let spent;
|
|
821
1115
|
try {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1116
|
+
for (;;) {
|
|
1117
|
+
const result = yield* leg(resume, spent);
|
|
1118
|
+
if (result.kind === "failed") {
|
|
1119
|
+
yield driverFailure(result.reason);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (result.kind === "finished") return;
|
|
1123
|
+
input = createQueue();
|
|
1124
|
+
spent = spent === void 0 ? result.usage : addUsage(spent, result.usage);
|
|
1125
|
+
await ports.hold?.wait("tool", ended.signal);
|
|
1126
|
+
if (ended.signal.aborted) {
|
|
1127
|
+
yield result.last;
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
resume = result.sessionId;
|
|
829
1131
|
}
|
|
830
1132
|
} catch (error) {
|
|
831
1133
|
input.close();
|
|
@@ -838,6 +1140,7 @@ const createSdkRuntime = (env = process.env) => ({
|
|
|
838
1140
|
input.push(userMessage(text, true));
|
|
839
1141
|
},
|
|
840
1142
|
interrupt: async () => {
|
|
1143
|
+
ended.abort();
|
|
841
1144
|
await current?.interrupt();
|
|
842
1145
|
}
|
|
843
1146
|
};
|