@krapht/ork 1.0.0-alpha.10 → 1.0.0-alpha.12
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/dist/{__build_info__-DFF18K8y.mjs → __build_info__-BJbE3Zvp.mjs} +4 -72
- package/dist/attach-BmV5ckb9.mjs +89 -0
- package/dist/bin.mjs +467 -864
- package/dist/{decide-3l9q8UtX.mjs → builtin-files-w0YULJ4c.mjs} +2 -50
- package/dist/chips-CyeB_rhR.mjs +43 -0
- package/dist/{clean-DVaEH9e7.mjs → clean-D8GI_2K-.mjs} +1 -1
- package/dist/{completion-Dv5kf598.mjs → completion-CzElB8nS.mjs} +24 -16
- package/dist/{context-CEaKOVmG.mjs → context-BAwgVSX5.mjs} +43 -31
- package/dist/decide-DlbyctOE.mjs +50 -0
- package/dist/{find-DEjjOlTG.mjs → find-p05wlbmu.mjs} +1 -1
- package/dist/format-Dni98cbh.mjs +206 -0
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-DYP_zMRP.mjs} +4 -0
- package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-BI-hLIl_.mjs} +4 -2
- package/dist/{limits-BSwEjQST.mjs → limits-CElmhowV.mjs} +1 -1
- package/dist/{list-Qxr6rwsY.mjs → list-zW2xKuGr.mjs} +1 -1
- package/dist/{mcp-CslnG5wf.mjs → mcp-CM3b65AZ.mjs} +6 -6
- package/dist/output-CnRzrbUM.mjs +423 -0
- package/dist/{pause-D7OVB5Ac.mjs → pause-BuS83WAI.mjs} +4 -4
- package/dist/{relay-EceRIebO.mjs → relay-DAsJA8wF.mjs} +2 -2
- package/dist/{remove-0IQZVWmI.mjs → remove-Dh7D_hVn.mjs} +3 -3
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-C0uwfUYi.mjs → send-MFEQZH31.mjs} +3 -3
- package/dist/{src-cmmXKHn1.mjs → src-BfxkJOYm.mjs} +234 -150
- package/dist/{src-Df6KOIDC.mjs → src-CdDTQLdQ.mjs} +2081 -1035
- package/dist/{src-oaJYIddp.mjs → src-D1QedD5y.mjs} +523 -81
- package/dist/{src-i5k2Y-kf.mjs → src-S9yK2OHU.mjs} +79 -11
- package/dist/store-B2OaE4ID.mjs +1356 -0
- package/dist/ui/assets/{app-qs8Lofv6.js → app-CAoZtku5.js} +16 -16
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-DV1ZaOk-.js → index-BL7Ad0OA.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CBkHJdqk.mjs → ui-C4Ys26oO.mjs} +4 -4
- package/dist/{usage-Cd8pEYyK.mjs → usage-C2zx-X6q.mjs} +43 -32
- package/dist/verdict-CIy8tAyX.mjs +84 -0
- package/dist/{watch-DW5R2OPO.mjs → watch-DzCeqM05.mjs} +28 -16
- package/dist/{workflows-BT1gDRIV.mjs → workflows-Rr5w90A0.mjs} +3 -3
- package/dist/{worktrees-g1MQMGie.mjs → worktrees-f6SAbum5.mjs} +11 -10
- package/package.json +1 -1
- package/dist/format-Cb5_m-Qd.mjs +0 -191
- package/dist/output-Bqljtmp_.mjs +0 -40
- package/dist/ui/assets/app-C8uhHVYp.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ar as AGENT_DRIVERS, Bt as lowerChild, En as maskSecrets, Vt as startAt, an as FEEDBACK_SEVERITIES, en as outcomeEnd, ft as parseReport, in as FEEDBACK_KINDS, nn as END_KINDS, on as FEEDBACK_TARGETS, rn as endFor, wn as MASK } from "./src-CdDTQLdQ.mjs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -26,6 +26,113 @@ const holdAtTool = async (hold, signal, windowMs) => {
|
|
|
26
26
|
}
|
|
27
27
|
return hold.held() && !signal.aborted ? "defer" : "go";
|
|
28
28
|
};
|
|
29
|
+
// ../runtime/src/heavy-hook.ts
|
|
30
|
+
/**
|
|
31
|
+
* @file A session's slot for a heavy Bash call (the project's `heavy` rule),
|
|
32
|
+
* taken in the hook before a tool call, where a pause holds the call too
|
|
33
|
+
* (ADR-041): a slot that frees within the hold window lets the call go; past
|
|
34
|
+
* it the call is deferred, as a pause defers it, and the session resumes once
|
|
35
|
+
* the slot is taken. The slot is released after the call (the hook after it),
|
|
36
|
+
* at the session's next call, or at the session's end. The wait itself (its
|
|
37
|
+
* events, the clock standing still) is the node's machine port.
|
|
38
|
+
*/
|
|
39
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
|
+
/** The command of a Bash call's hook input, as Claude Code sends it. */
|
|
41
|
+
const bashCommand = (input) => {
|
|
42
|
+
if (!isRecord$1(input) || input["tool_name"] !== "Bash") return void 0;
|
|
43
|
+
const tool = input["tool_input"];
|
|
44
|
+
const command = isRecord$1(tool) ? tool["command"] : void 0;
|
|
45
|
+
return typeof command === "string" ? command : void 0;
|
|
46
|
+
};
|
|
47
|
+
/** The hook for one session; with no machine port, every call goes. */
|
|
48
|
+
const heavyHook = (machine) => {
|
|
49
|
+
let held;
|
|
50
|
+
let waiting;
|
|
51
|
+
const free = () => {
|
|
52
|
+
held?.release();
|
|
53
|
+
held = void 0;
|
|
54
|
+
};
|
|
55
|
+
/** Gives up the wait; a slot it already got is given back. */
|
|
56
|
+
const abandon = () => {
|
|
57
|
+
const pending = waiting;
|
|
58
|
+
waiting = void 0;
|
|
59
|
+
if (pending === void 0) return;
|
|
60
|
+
pending.stop.abort();
|
|
61
|
+
pending.slot.then((release) => {
|
|
62
|
+
release();
|
|
63
|
+
return null;
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
before: async (input, signal, windowMs) => {
|
|
68
|
+
const command = bashCommand(input);
|
|
69
|
+
if (held !== void 0 && !held.ran && held.command === command) {
|
|
70
|
+
held.ran = true;
|
|
71
|
+
return "go";
|
|
72
|
+
}
|
|
73
|
+
free();
|
|
74
|
+
if (waiting !== void 0 && waiting.command !== command) abandon();
|
|
75
|
+
if (machine === void 0 || command === void 0) return "go";
|
|
76
|
+
if (waiting === void 0) {
|
|
77
|
+
const stop = new AbortController();
|
|
78
|
+
waiting = {
|
|
79
|
+
command,
|
|
80
|
+
stop,
|
|
81
|
+
slot: machine.slot(command, stop.signal)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const pending = waiting;
|
|
85
|
+
const window = new AbortController();
|
|
86
|
+
const release = await Promise.race([pending.slot, setTimeout$1(windowMs, null, { signal: AbortSignal.any([signal, window.signal]) }).catch(() => null)]);
|
|
87
|
+
window.abort();
|
|
88
|
+
if (release === null) return signal.aborted ? "go" : "defer";
|
|
89
|
+
waiting = void 0;
|
|
90
|
+
held = {
|
|
91
|
+
command,
|
|
92
|
+
release,
|
|
93
|
+
ran: true
|
|
94
|
+
};
|
|
95
|
+
return "go";
|
|
96
|
+
},
|
|
97
|
+
after: () => {
|
|
98
|
+
if (held?.ran === true) free();
|
|
99
|
+
},
|
|
100
|
+
ready: async () => {
|
|
101
|
+
const pending = waiting;
|
|
102
|
+
if (pending === void 0) return;
|
|
103
|
+
const release = await pending.slot;
|
|
104
|
+
if (waiting !== pending) return;
|
|
105
|
+
waiting = void 0;
|
|
106
|
+
held = {
|
|
107
|
+
command: pending.command,
|
|
108
|
+
release,
|
|
109
|
+
ran: false
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
release: () => {
|
|
113
|
+
abandon();
|
|
114
|
+
free();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/** Whether a hook's input is from after a tool call. */
|
|
119
|
+
const afterCall = (input) => isRecord$1(input) && input["hook_event_name"] === "PostToolUse";
|
|
120
|
+
/**
|
|
121
|
+
* A session's hook on its tool calls. Before one, a pause holds it (ADR-041),
|
|
122
|
+
* then a heavy Bash call waits for its slot; past the window either defers it
|
|
123
|
+
* (`onDefer`). After a call, its slot is freed.
|
|
124
|
+
*/
|
|
125
|
+
const toolHook = (given) => async (gone, input) => {
|
|
126
|
+
if (afterCall(input)) {
|
|
127
|
+
given.heavy.after();
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
const signal = AbortSignal.any([gone, given.ended]);
|
|
131
|
+
let answer = await holdAtTool(given.hold, signal, given.windowMs);
|
|
132
|
+
if (answer === "go") answer = await given.heavy.before(input, signal, given.windowMs);
|
|
133
|
+
if (answer === "defer") given.onDefer();
|
|
134
|
+
return given.answer(answer);
|
|
135
|
+
};
|
|
29
136
|
/** The name Claude Code gives an Ork tool: `mcp__ork__<name>`. */
|
|
30
137
|
const toolId = (name) => `mcp__ork__${name}`;
|
|
31
138
|
const text = (value) => ({ content: [{
|
|
@@ -77,7 +184,8 @@ const defineTool = (definition) => ({
|
|
|
77
184
|
* `claude` CLI driver. It answers with plain JSON (no SSE stream), which the
|
|
78
185
|
* transport allows, and implements only what Ork needs: `initialize`, `ping`,
|
|
79
186
|
* `tools/list` and `tools/call`. Beside it, `/hold` answers the session's
|
|
80
|
-
* `PreToolUse` HTTP hook, which waits while the node is paused (ADR-041)
|
|
187
|
+
* `PreToolUse` HTTP hook, which waits while the node is paused (ADR-041) or
|
|
188
|
+
* for a heavy command's slot, and its `PostToolUse` hook, which frees it.
|
|
81
189
|
*/
|
|
82
190
|
const PATH = "/mcp";
|
|
83
191
|
const HOLD_PATH = "/hold";
|
|
@@ -169,15 +277,22 @@ const serveHold = async (req, res, hold) => {
|
|
|
169
277
|
respond(res, 405);
|
|
170
278
|
return;
|
|
171
279
|
}
|
|
172
|
-
|
|
280
|
+
const body = await readBody(req);
|
|
281
|
+
if (body === null) {
|
|
173
282
|
respond(res, 413);
|
|
174
283
|
return;
|
|
175
284
|
}
|
|
285
|
+
let input;
|
|
286
|
+
try {
|
|
287
|
+
input = JSON.parse(body);
|
|
288
|
+
} catch {
|
|
289
|
+
input = void 0;
|
|
290
|
+
}
|
|
176
291
|
const gone = new AbortController();
|
|
177
292
|
res.once("close", () => {
|
|
178
293
|
gone.abort();
|
|
179
294
|
});
|
|
180
|
-
const output = await hold(gone.signal);
|
|
295
|
+
const output = await hold(gone.signal, input);
|
|
181
296
|
if (!gone.signal.aborted) respond(res, 200, output);
|
|
182
297
|
};
|
|
183
298
|
const serve = async (req, res, tools, token, hold) => {
|
|
@@ -249,25 +364,139 @@ const startOrkMcpHttp = async (tools, hold) => {
|
|
|
249
364
|
})
|
|
250
365
|
};
|
|
251
366
|
};
|
|
252
|
-
// ../runtime/src/
|
|
367
|
+
// ../runtime/src/provider-errors.ts
|
|
253
368
|
/**
|
|
254
|
-
* @file
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
369
|
+
* @file The provider-error classifier every driver's adapter uses (specs/
|
|
370
|
+
* runtime/session-ends.md): an error a model provider sent back (Anthropic,
|
|
371
|
+
* OpenAI-style, or a proxy such as OpenCode's `commandcode`) to one end kind.
|
|
372
|
+
* It reads the error's type or code first, its HTTP status next, and its
|
|
373
|
+
* words last, so a changed sentence rarely changes the kind.
|
|
259
374
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
375
|
+
/** Error types and codes the providers document, by the kind they mean. */
|
|
376
|
+
const TYPES = {
|
|
377
|
+
rate_limit_error: "rate_limited",
|
|
378
|
+
overloaded_error: "provider_unavailable",
|
|
379
|
+
api_error: "provider_unavailable",
|
|
380
|
+
timeout_error: "provider_unavailable",
|
|
381
|
+
authentication_error: "auth",
|
|
382
|
+
permission_error: "auth",
|
|
383
|
+
billing_error: "out_of_credits",
|
|
384
|
+
not_found_error: "model_unavailable",
|
|
385
|
+
request_too_large: "context_too_long",
|
|
386
|
+
insufficient_quota: "out_of_credits",
|
|
387
|
+
invalid_api_key: "auth",
|
|
388
|
+
context_length_exceeded: "context_too_long",
|
|
389
|
+
model_not_found: "model_unavailable",
|
|
390
|
+
rate_limit_exceeded: "rate_limited",
|
|
391
|
+
server_error: "provider_unavailable",
|
|
392
|
+
ProviderAuthError: "auth",
|
|
393
|
+
MessageAbortedError: "interrupted"
|
|
394
|
+
};
|
|
395
|
+
const TYPE = new RegExp(`\\b(${Object.keys(TYPES).join("|")})\\b`, "u");
|
|
396
|
+
/** HTTP statuses that decide by themselves; 400 needs the words. */
|
|
397
|
+
const statusKind = (status) => {
|
|
398
|
+
if (status === 401 || status === 403) return "auth";
|
|
399
|
+
if (status === 402) return "out_of_credits";
|
|
400
|
+
if (status === 404) return "model_unavailable";
|
|
401
|
+
if (status === 413) return "context_too_long";
|
|
402
|
+
if (status === 429) return "rate_limited";
|
|
403
|
+
if (status === 408 || status === 529 || status >= 500 && status < 600) return "provider_unavailable";
|
|
404
|
+
return null;
|
|
405
|
+
};
|
|
406
|
+
/** The words, when neither type nor status decided; the first match wins. */
|
|
407
|
+
const WORDS = [
|
|
408
|
+
["plan_limit", /\busage limit reached\b|\bhit your (?:usage )?limit\b|\blimit will reset\b/iu],
|
|
409
|
+
["context_too_long", /\bprompt is too long\b|\bcontext (?:length|window)\b|\bmaximum context\b|\btoo many tokens\b/iu],
|
|
410
|
+
["out_of_credits", /\bcredit balance\b|\binsufficient (?:credits?|funds|balance|quota)\b|\bout of credits?\b|\bexceeded your current quota\b/iu],
|
|
411
|
+
["rate_limited", /\brate.?limit(?:ed)?\b|\btoo many requests\b/iu],
|
|
412
|
+
["auth", /\binvalid (?:x-)?api.?key\b|\bnot (?:logged|signed) in\b|\bplease run \/login\b|\bunauthori[sz]ed\b|\bOAuth token has expired\b/iu],
|
|
413
|
+
["provider_unavailable", /\boverloaded\b|\bservice unavailable\b|\bbad gateway\b|\bECONNRESET\b|\bECONNREFUSED\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bfetch failed\b|\bsocket hang up\b|\bnetwork error\b/iu],
|
|
414
|
+
["model_unavailable", /\bassistant message prefill\b|\bInvalid `signature` in `thinking` block\b|\bmodel(?: \S+)? (?:not found|does not exist|is not supported)\b|\bdoes not support\b|\bunknown model\b/iu]
|
|
415
|
+
];
|
|
416
|
+
/** Generic refusals: the provider would not serve the request as it was. */
|
|
417
|
+
const REFUSED$1 = /\binvalid_request_error\b|\binvalid_request\b/u;
|
|
418
|
+
/** A status written in the text, e.g. Claude Code's `API Error: 529 {…}`. */
|
|
419
|
+
const STATUS_IN_TEXT = /\bAPI Error:? (\d{3})\b|"(?:status|statusCode)"\s*:\s*(\d{3})\b|\b(?:status|HTTP)[ :]+(\d{3})\b/u;
|
|
420
|
+
const statusIn = (text) => {
|
|
421
|
+
const found = STATUS_IN_TEXT.exec(text);
|
|
422
|
+
const digits = found?.[1] ?? found?.[2] ?? found?.[3];
|
|
423
|
+
return digits === void 0 ? void 0 : Number(digits);
|
|
424
|
+
};
|
|
425
|
+
const classed = (kind) => ({
|
|
426
|
+
kind,
|
|
427
|
+
message: END_KINDS[kind].message({})
|
|
428
|
+
});
|
|
429
|
+
/**
|
|
430
|
+
* The end kind a provider's error means, with Ork's sentence for it; `null`
|
|
431
|
+
* when nothing in it is recognized. `status` is the HTTP status when the caller
|
|
432
|
+
* has it; otherwise one written in the text is read.
|
|
433
|
+
*/
|
|
434
|
+
const classifyProviderError = (text, status) => {
|
|
435
|
+
const type = TYPE.exec(text)?.[1];
|
|
436
|
+
if (type !== void 0) return classed(TYPES[type] ?? "unknown");
|
|
437
|
+
const code = status ?? statusIn(text);
|
|
438
|
+
const byStatus = code === void 0 ? null : statusKind(code);
|
|
439
|
+
if (byStatus !== null) return classed(byStatus);
|
|
440
|
+
const byWords = WORDS.find(([, pattern]) => pattern.test(text))?.[0];
|
|
441
|
+
if (byWords !== void 0) return classed(byWords);
|
|
442
|
+
return REFUSED$1.test(text) || code === 400 ? classed("model_unavailable") : null;
|
|
443
|
+
};
|
|
444
|
+
// ../runtime/src/session-end.ts
|
|
445
|
+
/**
|
|
446
|
+
* @file How a session ended, as its adapter's decoder and the session runner
|
|
447
|
+
* settle it (specs/runtime/session-ends.md): a decoder names the signal only
|
|
448
|
+
* it can see (a turn cap, an exit code, a provider's error), and the report
|
|
449
|
+
* the session handed in decides the rest. The runner then writes the end in
|
|
450
|
+
* the node's numbers, its detail scrubbed with the feedback redactor. A
|
|
451
|
+
* session the driver could not run ends as `driverFailure` says.
|
|
452
|
+
*/
|
|
453
|
+
/** The end a report gives: done, a reported failure, or none handed in. */
|
|
454
|
+
const reportEnd = (report) => {
|
|
455
|
+
if (report === null) return endFor("no_report");
|
|
456
|
+
return endFor(report.status === "fail" ? "reported_failure" : "done");
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* A session's end: a report it handed in decides it (the node's result is that
|
|
460
|
+
* report), then the decoder's `signal`, then `no_report`.
|
|
461
|
+
*/
|
|
462
|
+
const sessionEnd = (signal, report) => report !== null || signal === null ? reportEnd(report) : signal;
|
|
463
|
+
/**
|
|
464
|
+
* The end an error's text means: the provider classifier's kind, or `fallback`
|
|
465
|
+
* when it recognizes nothing. The text is the detail either way.
|
|
466
|
+
*/
|
|
467
|
+
const errorEnd = (text, fallback = "unknown", status) => endFor(classifyProviderError(text, status)?.kind ?? fallback, {}, text);
|
|
468
|
+
/**
|
|
469
|
+
* The outcome with its end settled: a report decides it, also one handed in
|
|
470
|
+
* through Ork's `report` tool after the decoder's end said none; the message is
|
|
471
|
+
* written with the node's numbers; the detail is scrubbed and capped. An
|
|
472
|
+
* adapter whose decoder sets no end gets one read from the outcome.
|
|
473
|
+
*/
|
|
474
|
+
const settleEnd = (outcome, facts, scrub) => {
|
|
475
|
+
const end = outcome.report === null ? outcome.end ?? outcomeEnd(outcome) : reportEnd(outcome.report);
|
|
476
|
+
return {
|
|
477
|
+
...outcome,
|
|
478
|
+
end: endFor(end.kind, facts, end.detail, scrub)
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* The kind of a failure to run the driver: its program missing, else what a
|
|
483
|
+
* provider's words in it say, else a crash.
|
|
484
|
+
*/
|
|
485
|
+
const failureKind = (error) => {
|
|
486
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return "tool_missing";
|
|
487
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
488
|
+
if (/\bENOENT\b/u.test(text)) return "tool_missing";
|
|
489
|
+
return classifyProviderError(text)?.kind ?? "driver_crashed";
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* A finished event for a session the driver could not run, ended as `kind` with
|
|
493
|
+
* `message` (Ork's words or the tool's, masked) as its detail.
|
|
494
|
+
*/
|
|
495
|
+
const driverFailure = (message, kind, scope) => ({
|
|
268
496
|
type: "session.finished",
|
|
269
497
|
outcome: {
|
|
270
498
|
subtype: "error_driver",
|
|
499
|
+
end: endFor(kind, scope.facts, kind === "interrupted" ? void 0 : message, scope.scrub),
|
|
271
500
|
sessionId: "",
|
|
272
501
|
report: null,
|
|
273
502
|
text: "",
|
|
@@ -283,6 +512,30 @@ const driverFailure = (message) => ({
|
|
|
283
512
|
errors: [message]
|
|
284
513
|
}
|
|
285
514
|
});
|
|
515
|
+
// ../runtime/src/session-leg.ts
|
|
516
|
+
/**
|
|
517
|
+
* @file One process (leg) of a session the runner drives: it writes the queued
|
|
518
|
+
* stdin lines, yields the adapter's decoded events, and says how it ended. A
|
|
519
|
+
* session is one leg, or several when a paused node's tool call was deferred
|
|
520
|
+
* or a message waits for a tool that takes none mid-session; a resumed leg
|
|
521
|
+
* carries what the legs before it spent, and its start is not news.
|
|
522
|
+
*/
|
|
523
|
+
const REASON_LIMIT = 500;
|
|
524
|
+
/** Marks a session's start with the driver that runs it. */
|
|
525
|
+
const withDriver = (event, driver) => event.type === "session.started" ? {
|
|
526
|
+
...event,
|
|
527
|
+
driver
|
|
528
|
+
} : event;
|
|
529
|
+
/**
|
|
530
|
+
* A session's outcome with its report (the one Ork's `report` tool accepted,
|
|
531
|
+
* when the session has that tool) and its end settled.
|
|
532
|
+
*/
|
|
533
|
+
const settled = (outcome, input) => settleEnd(input.reported === void 0 ? outcome : {
|
|
534
|
+
...outcome,
|
|
535
|
+
report: input.reported()
|
|
536
|
+
}, input.scope.facts, input.scope.scrub);
|
|
537
|
+
/** The kinds of a session that ended well, so a queued message continues it. */
|
|
538
|
+
const wentWell = (kind) => kind === "done" || kind === "reported_failure" || kind === "no_report";
|
|
286
539
|
/**
|
|
287
540
|
* What one decoded line yields, and how the leg ends if it does: a result is
|
|
288
541
|
* the session's end, unless the hold hook deferred a call or a message waits; a
|
|
@@ -295,12 +548,9 @@ const legStep = (decoded, input) => {
|
|
|
295
548
|
const events = [];
|
|
296
549
|
let ended;
|
|
297
550
|
for (const said of decoded.events) {
|
|
298
|
-
const event = said.type === "session.finished"
|
|
551
|
+
const event = said.type === "session.finished" ? {
|
|
299
552
|
...said,
|
|
300
|
-
outcome:
|
|
301
|
-
...said.outcome,
|
|
302
|
-
report: input.reported()
|
|
303
|
-
}
|
|
553
|
+
outcome: settled(said.outcome, input)
|
|
304
554
|
} : said;
|
|
305
555
|
if (event.type === "session.started" && carried !== void 0) continue;
|
|
306
556
|
if (event.type !== "session.finished") events.push(withDriver(event, adapter.driver));
|
|
@@ -319,7 +569,7 @@ const legStep = (decoded, input) => {
|
|
|
319
569
|
usage
|
|
320
570
|
}
|
|
321
571
|
};
|
|
322
|
-
if (input.withhold() && event.outcome.
|
|
572
|
+
if (input.withhold() && wentWell(event.outcome.end?.kind)) ended = {
|
|
323
573
|
kind: "continued",
|
|
324
574
|
sessionId: event.outcome.sessionId,
|
|
325
575
|
usage: event.outcome.usage,
|
|
@@ -348,7 +598,8 @@ const failureReason = (name, stderr, code) => {
|
|
|
348
598
|
};
|
|
349
599
|
/** Runs one process of a session, yielding its events; returns how it ended. */
|
|
350
600
|
const runLeg = async function* (input) {
|
|
351
|
-
const
|
|
601
|
+
const [bin, args] = startAt(input.priority, input.bin, input.args, input.env);
|
|
602
|
+
const child = spawn(bin, args, {
|
|
352
603
|
cwd: input.cwd,
|
|
353
604
|
env: { ...input.env },
|
|
354
605
|
stdio: [
|
|
@@ -357,6 +608,7 @@ const runLeg = async function* (input) {
|
|
|
357
608
|
"pipe"
|
|
358
609
|
]
|
|
359
610
|
});
|
|
611
|
+
lowerChild(input.priority, child.pid);
|
|
360
612
|
const exited = new Promise((resolve) => {
|
|
361
613
|
child.on("close", (code, signal) => {
|
|
362
614
|
resolve({
|
|
@@ -388,7 +640,7 @@ const runLeg = async function* (input) {
|
|
|
388
640
|
if (input.adapter.command.input === void 0) child.stdin.end();
|
|
389
641
|
let outcome;
|
|
390
642
|
const { decoder } = input;
|
|
391
|
-
const decode = decoder?.decode ?? input.adapter.
|
|
643
|
+
const decode = decoder?.decode ?? input.adapter.decode;
|
|
392
644
|
try {
|
|
393
645
|
for await (const line of createInterface({ input: child.stdout })) {
|
|
394
646
|
const { events, ended } = legStep(decode(line), input);
|
|
@@ -400,18 +652,26 @@ const runLeg = async function* (input) {
|
|
|
400
652
|
}
|
|
401
653
|
const end = await Promise.race([exited, spawnError.then((error) => error)]);
|
|
402
654
|
if (outcome !== void 0) return outcome;
|
|
403
|
-
if (decoder !== void 0 && !(end instanceof Error) && !stopped
|
|
655
|
+
if (decoder !== void 0 && !(end instanceof Error) && !stopped) {
|
|
404
656
|
const { events, ended } = legStep({ events: decoder.exit(end.code) }, input);
|
|
405
657
|
yield* events;
|
|
406
658
|
if (ended !== void 0) return ended;
|
|
407
659
|
}
|
|
408
660
|
if (end instanceof Error) return {
|
|
409
661
|
kind: "failed",
|
|
410
|
-
reason: `could not start ${input.bin}: ${end.message}
|
|
662
|
+
reason: `could not start ${input.bin}: ${end.message}`,
|
|
663
|
+
end: failureKind(end)
|
|
664
|
+
};
|
|
665
|
+
if (stopped || end.signal !== null) return {
|
|
666
|
+
kind: "failed",
|
|
667
|
+
reason: "interrupted",
|
|
668
|
+
end: "interrupted"
|
|
411
669
|
};
|
|
670
|
+
const reason = failureReason(input.adapter.command.bin, stderr, end.code);
|
|
412
671
|
return {
|
|
413
672
|
kind: "failed",
|
|
414
|
-
reason
|
|
673
|
+
reason,
|
|
674
|
+
end: classifyProviderError(reason)?.kind ?? "driver_crashed"
|
|
415
675
|
};
|
|
416
676
|
} finally {
|
|
417
677
|
input.attach();
|
|
@@ -774,6 +1034,33 @@ const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ??
|
|
|
774
1034
|
/** Their Claude Code names, for the session's allowed tools. */
|
|
775
1035
|
const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
|
|
776
1036
|
// ../runtime/src/session-runner.ts
|
|
1037
|
+
/**
|
|
1038
|
+
* @file The session runner: one agent CLI session, driven through an adapter's
|
|
1039
|
+
* strategies (`adapter.ts`), written once for every driver. It serves Ork's
|
|
1040
|
+
* tools on a local MCP endpoint, wires the session to it, and runs the tool
|
|
1041
|
+
* as one or more processes (legs). A paused node's tool call is held, and
|
|
1042
|
+
* past the hold window deferred; the next leg resumes the session once the
|
|
1043
|
+
* node continues (ADR-041). A tool that takes no message mid-session gets one
|
|
1044
|
+
* at the next leg, which resumes the session with it. A driver that cannot
|
|
1045
|
+
* hold the final answer to the report schema gets Ork's `report` tool, and
|
|
1046
|
+
* the session ends with the report it accepted. Interrupting stops the
|
|
1047
|
+
* running process.
|
|
1048
|
+
*/
|
|
1049
|
+
/**
|
|
1050
|
+
* What a session's end is written with: the node's limits and program, and its
|
|
1051
|
+
* folder and the home folder taken out of the detail.
|
|
1052
|
+
*/
|
|
1053
|
+
const scopeOf = (spec, bin, env) => ({
|
|
1054
|
+
facts: {
|
|
1055
|
+
...spec.maxTurns === void 0 ? {} : { maxTurns: spec.maxTurns },
|
|
1056
|
+
...spec.budgetUsd === void 0 ? {} : { budgetUsd: spec.budgetUsd },
|
|
1057
|
+
program: path.basename(bin)
|
|
1058
|
+
},
|
|
1059
|
+
scrub: {
|
|
1060
|
+
roots: [spec.cwd],
|
|
1061
|
+
home: env["HOME"] ?? homedir()
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
777
1064
|
/** A spec that resumes `sessionId`; a fork's later legs continue the fork. */
|
|
778
1065
|
const resumed = (spec, sessionId) => ({
|
|
779
1066
|
...spec,
|
|
@@ -830,6 +1117,12 @@ const createSessionRuntime = (adapter, options = {}) => ({
|
|
|
830
1117
|
let reported = null;
|
|
831
1118
|
const { hold } = ports;
|
|
832
1119
|
const hook = hold === void 0 ? void 0 : adapter.hold;
|
|
1120
|
+
const heavy = heavyHook(ports.machine);
|
|
1121
|
+
ended.signal.addEventListener("abort", () => {
|
|
1122
|
+
heavy.release();
|
|
1123
|
+
}, { once: true });
|
|
1124
|
+
const bin = options.bin ?? adapter.command.bin;
|
|
1125
|
+
const scope = scopeOf(spec, bin, options.env ?? process.env);
|
|
833
1126
|
const events = async function* () {
|
|
834
1127
|
const tools = orkTools({
|
|
835
1128
|
nodeId: spec.nodeId,
|
|
@@ -843,11 +1136,16 @@ const createSessionRuntime = (adapter, options = {}) => ({
|
|
|
843
1136
|
}
|
|
844
1137
|
});
|
|
845
1138
|
const byTool = tools.some((tool) => tool.name === REPORT_TOOL);
|
|
846
|
-
const mcp = await startOrkMcpHttp(tools, hold === void 0 || hook === void 0 ? void 0 :
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1139
|
+
const mcp = await startOrkMcpHttp(tools, hold === void 0 || hook === void 0 ? void 0 : toolHook({
|
|
1140
|
+
hold,
|
|
1141
|
+
heavy,
|
|
1142
|
+
answer: hook.answer,
|
|
1143
|
+
windowMs: options.holdWindowMs ?? hook.windowMs,
|
|
1144
|
+
ended: ended.signal,
|
|
1145
|
+
onDefer: () => {
|
|
1146
|
+
deferring = true;
|
|
1147
|
+
}
|
|
1148
|
+
}));
|
|
851
1149
|
const endpoint = hook === void 0 ? {
|
|
852
1150
|
url: mcp.url,
|
|
853
1151
|
token: mcp.token
|
|
@@ -858,19 +1156,22 @@ const createSessionRuntime = (adapter, options = {}) => ({
|
|
|
858
1156
|
try {
|
|
859
1157
|
wired = yield* wireSaying(adapter, spec, endpoint, ended.signal, options);
|
|
860
1158
|
} catch (error) {
|
|
861
|
-
yield driverFailure(`could not reach Ork's tools: ${error instanceof Error ? error.message : String(error)}
|
|
1159
|
+
yield driverFailure(`could not reach Ork's tools: ${error instanceof Error ? error.message : String(error)}`, failureKind(error), scope);
|
|
862
1160
|
return;
|
|
863
1161
|
}
|
|
864
1162
|
const env = {
|
|
865
1163
|
...options.env ?? process.env,
|
|
1164
|
+
PWD: spec.cwd,
|
|
866
1165
|
...adapter.context.env(spec),
|
|
867
1166
|
...wired.env
|
|
868
1167
|
};
|
|
869
1168
|
const legOf = (legSpec, carried) => ({
|
|
870
1169
|
adapter,
|
|
871
|
-
bin
|
|
1170
|
+
bin,
|
|
872
1171
|
env,
|
|
1172
|
+
scope,
|
|
873
1173
|
cwd: spec.cwd,
|
|
1174
|
+
...ports.machine === void 0 ? {} : { priority: ports.machine.priority },
|
|
874
1175
|
args: [...adapter.command.args(legSpec, {
|
|
875
1176
|
endpoint,
|
|
876
1177
|
allowed
|
|
@@ -889,18 +1190,21 @@ const createSessionRuntime = (adapter, options = {}) => ({
|
|
|
889
1190
|
let spent;
|
|
890
1191
|
for (;;) {
|
|
891
1192
|
if (ended.signal.aborted) {
|
|
892
|
-
yield driverFailure("interrupted");
|
|
1193
|
+
yield driverFailure("interrupted", "interrupted", scope);
|
|
893
1194
|
return;
|
|
894
1195
|
}
|
|
895
1196
|
const leg = yield* runLeg(legOf(legSpec, spent));
|
|
896
1197
|
if (leg.kind === "failed") {
|
|
897
|
-
yield driverFailure(leg.reason);
|
|
1198
|
+
yield driverFailure(leg.reason, leg.end, scope);
|
|
898
1199
|
return;
|
|
899
1200
|
}
|
|
900
1201
|
if (leg.kind === "finished") return;
|
|
901
1202
|
deferring = false;
|
|
902
1203
|
spent = spent === void 0 ? leg.usage : adapter.usage.combine(spent, leg.usage);
|
|
903
|
-
if (leg.kind === "deferred")
|
|
1204
|
+
if (leg.kind === "deferred") {
|
|
1205
|
+
await heavy.ready();
|
|
1206
|
+
await hold?.wait("tool", ended.signal);
|
|
1207
|
+
}
|
|
904
1208
|
if (ended.signal.aborted) {
|
|
905
1209
|
yield leg.last;
|
|
906
1210
|
return;
|
|
@@ -911,6 +1215,7 @@ const createSessionRuntime = (adapter, options = {}) => ({
|
|
|
911
1215
|
} : resumed(spec, leg.sessionId);
|
|
912
1216
|
}
|
|
913
1217
|
} finally {
|
|
1218
|
+
heavy.release();
|
|
914
1219
|
await wired?.release();
|
|
915
1220
|
await mcp.close();
|
|
916
1221
|
}
|
|
@@ -960,16 +1265,38 @@ const usageOf = (result) => {
|
|
|
960
1265
|
durationMs: num$2(result["duration_ms"])
|
|
961
1266
|
};
|
|
962
1267
|
};
|
|
1268
|
+
/** Claude Code's error subtypes that name their own kind. */
|
|
1269
|
+
const SUBTYPE_KINDS = {
|
|
1270
|
+
error_max_turns: "turn_limit",
|
|
1271
|
+
error_max_budget_usd: "budget_limit",
|
|
1272
|
+
error_max_structured_output_retries: "invalid_report"
|
|
1273
|
+
};
|
|
1274
|
+
/**
|
|
1275
|
+
* What a result says went wrong, if anything: a subtype that names its kind, or
|
|
1276
|
+
* an error (`is_error`, or an error subtype) read by the provider classifier,
|
|
1277
|
+
* `unknown` when it says nothing Ork knows.
|
|
1278
|
+
*/
|
|
1279
|
+
const signalOf$1 = (subtype, result, errors) => {
|
|
1280
|
+
const text = [...errors, str$2(result["result"])].join("\n").trim();
|
|
1281
|
+
const named = SUBTYPE_KINDS[subtype];
|
|
1282
|
+
if (named !== void 0) return endFor(named, {}, text);
|
|
1283
|
+
if (subtype === "success" && result["is_error"] !== true) return null;
|
|
1284
|
+
return errorEnd(text);
|
|
1285
|
+
};
|
|
963
1286
|
/** The outcome a `result` message describes. */
|
|
964
1287
|
const outcomeOf = (result) => {
|
|
965
|
-
const
|
|
1288
|
+
const said = result["errors"];
|
|
1289
|
+
const errors = Array.isArray(said) ? said.filter((e) => typeof e === "string") : [];
|
|
1290
|
+
const subtype = str$2(result["subtype"]) || "unknown";
|
|
1291
|
+
const report = parseReport(result["structured_output"]);
|
|
966
1292
|
return {
|
|
967
|
-
subtype
|
|
1293
|
+
subtype,
|
|
1294
|
+
end: sessionEnd(signalOf$1(subtype, result, errors), report),
|
|
968
1295
|
sessionId: str$2(result["session_id"]),
|
|
969
|
-
report
|
|
1296
|
+
report,
|
|
970
1297
|
text: str$2(result["result"]),
|
|
971
1298
|
usage: usageOf(result),
|
|
972
|
-
errors
|
|
1299
|
+
errors
|
|
973
1300
|
};
|
|
974
1301
|
};
|
|
975
1302
|
/** The year 2286: a reset beyond this is not in seconds. */
|
|
@@ -1113,12 +1440,14 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
1113
1440
|
/**
|
|
1114
1441
|
* The node's `--settings` with Ork's `PreToolUse` HTTP hook added: before every
|
|
1115
1442
|
* tool call, pre-approved ones included, the session asks whether its node may
|
|
1116
|
-
* go on.
|
|
1443
|
+
* go on. Its `PostToolUse` hook after a Bash call frees the call's slot.
|
|
1117
1444
|
*/
|
|
1118
1445
|
const withHoldHook = (settings, holdUrl, token) => {
|
|
1119
1446
|
const hooks = isRecord(settings?.["hooks"]) ? settings["hooks"] : {};
|
|
1120
|
-
const listed =
|
|
1121
|
-
|
|
1447
|
+
const listed = (event) => {
|
|
1448
|
+
const own = hooks[event];
|
|
1449
|
+
return Array.isArray(own) ? [...own] : [];
|
|
1450
|
+
};
|
|
1122
1451
|
const hook = {
|
|
1123
1452
|
type: "http",
|
|
1124
1453
|
url: holdUrl,
|
|
@@ -1129,9 +1458,13 @@ const withHoldHook = (settings, holdUrl, token) => {
|
|
|
1129
1458
|
...settings,
|
|
1130
1459
|
hooks: {
|
|
1131
1460
|
...hooks,
|
|
1132
|
-
PreToolUse: [...
|
|
1461
|
+
PreToolUse: [...listed("PreToolUse"), {
|
|
1133
1462
|
matcher: "*",
|
|
1134
1463
|
hooks: [hook]
|
|
1464
|
+
}],
|
|
1465
|
+
PostToolUse: [...listed("PostToolUse"), {
|
|
1466
|
+
matcher: "Bash",
|
|
1467
|
+
hooks: [hook]
|
|
1135
1468
|
}]
|
|
1136
1469
|
}
|
|
1137
1470
|
};
|
|
@@ -1273,9 +1606,11 @@ const runCmd = (options, args, cwd) => new Promise((resolve) => {
|
|
|
1273
1606
|
encoding: "utf8",
|
|
1274
1607
|
timeout: CMD_TIMEOUT_MS
|
|
1275
1608
|
}, (error, stdout, stderr) => {
|
|
1609
|
+
const said = `${stdout}${stderr}`.trim();
|
|
1610
|
+
const missing = error?.code === "ENOENT" ? `spawn ${options.bin} ENOENT` : "";
|
|
1276
1611
|
resolve({
|
|
1277
1612
|
ok: error === null,
|
|
1278
|
-
out:
|
|
1613
|
+
out: said || missing
|
|
1279
1614
|
});
|
|
1280
1615
|
});
|
|
1281
1616
|
});
|
|
@@ -1487,6 +1822,17 @@ const leaseWiring = (given) => ({
|
|
|
1487
1822
|
}
|
|
1488
1823
|
});
|
|
1489
1824
|
// ../runtime/src/adapters/command-code-events.ts
|
|
1825
|
+
/**
|
|
1826
|
+
* @file The `command-code` adapter's `LegDecoder` and `UsageReader`: maps
|
|
1827
|
+
* Command Code's `--output-format json` lines (`{"type":"event"}` frames,
|
|
1828
|
+
* then one `{"type":"result"}` line; measured on 1.88.5,
|
|
1829
|
+
* specs/architecture.md, "Command Code") to Ork's runtime events. Tool names
|
|
1830
|
+
* are given Ork's (Claude Code's) names, so logs and the node's rules read
|
|
1831
|
+
* the same on every driver. One decoder reads one leg: the session id comes
|
|
1832
|
+
* in an early `run_start` frame and the result line counts no turns, so it
|
|
1833
|
+
* keeps both. The result line is held until the process exits: its exit code
|
|
1834
|
+
* says how the session ended (specs/architecture/command-code.md).
|
|
1835
|
+
*/
|
|
1490
1836
|
const isObject$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1491
1837
|
const num$1 = (value) => typeof value === "number" ? value : 0;
|
|
1492
1838
|
const str$1 = (value) => typeof value === "string" ? value : "";
|
|
@@ -1513,6 +1859,28 @@ const SUBTYPES = {
|
|
|
1513
1859
|
max_turns: "error_max_turns",
|
|
1514
1860
|
error: "error_during_execution"
|
|
1515
1861
|
};
|
|
1862
|
+
/**
|
|
1863
|
+
* Command Code's exit codes, by the kind they mean; 0 is a session that ended
|
|
1864
|
+
* well and 1 an error its text explains.
|
|
1865
|
+
*/
|
|
1866
|
+
const COMMAND_CODE_EXITS = {
|
|
1867
|
+
3: "auth",
|
|
1868
|
+
4: "auth",
|
|
1869
|
+
5: "rate_limited",
|
|
1870
|
+
6: "provider_unavailable",
|
|
1871
|
+
7: "provider_unavailable",
|
|
1872
|
+
8: "turn_limit",
|
|
1873
|
+
9: "provider_unavailable",
|
|
1874
|
+
10: "out_of_credits",
|
|
1875
|
+
130: "interrupted"
|
|
1876
|
+
};
|
|
1877
|
+
/** What a leg's exit code and result line say went wrong, if anything. */
|
|
1878
|
+
const signalOf = (code, error) => {
|
|
1879
|
+
const kind = code === null ? void 0 : COMMAND_CODE_EXITS[code];
|
|
1880
|
+
if (kind !== void 0) return endFor(kind, {}, error);
|
|
1881
|
+
if ((code === 0 || code === null) && error === "") return null;
|
|
1882
|
+
return errorEnd(error);
|
|
1883
|
+
};
|
|
1516
1884
|
/** The text blocks of a `message_end` frame's content. */
|
|
1517
1885
|
const textOf = (content) => (Array.isArray(content) ? content : []).filter((item) => isObject$1(item)).filter((block) => block["type"] === "text").map((block) => str$1(block["text"])).join("");
|
|
1518
1886
|
/** The frames that end a tool call as an error: failed, denied, or blocked. */
|
|
@@ -1528,10 +1896,14 @@ const parseLine$1 = (line) => {
|
|
|
1528
1896
|
return;
|
|
1529
1897
|
}
|
|
1530
1898
|
};
|
|
1531
|
-
/**
|
|
1899
|
+
/**
|
|
1900
|
+
* A decoder for one leg of a Command Code session; its end comes at the exit,
|
|
1901
|
+
* from the held result line and the exit code.
|
|
1902
|
+
*/
|
|
1532
1903
|
const commandCodeDecoder = () => {
|
|
1533
1904
|
let sessionId = "";
|
|
1534
1905
|
let turns = 0;
|
|
1906
|
+
let held;
|
|
1535
1907
|
const frame = (event) => {
|
|
1536
1908
|
const type = event["type"];
|
|
1537
1909
|
const id = str$1(event["toolCallId"]);
|
|
@@ -1573,13 +1945,16 @@ const commandCodeDecoder = () => {
|
|
|
1573
1945
|
}];
|
|
1574
1946
|
return [];
|
|
1575
1947
|
};
|
|
1576
|
-
const result = (line) => {
|
|
1948
|
+
const result = (line, code) => {
|
|
1577
1949
|
const usage = isObject$1(line["usage"]) ? line["usage"] : {};
|
|
1578
1950
|
const error = str$1(line["error"]);
|
|
1951
|
+
const subtype = str$1(line["subtype"]);
|
|
1952
|
+
const signal = subtype === "max_turns" ? endFor("turn_limit", {}, error) : signalOf(code, error);
|
|
1579
1953
|
return {
|
|
1580
1954
|
type: "session.finished",
|
|
1581
1955
|
outcome: {
|
|
1582
|
-
subtype: SUBTYPES[
|
|
1956
|
+
subtype: SUBTYPES[subtype] ?? "error_during_execution",
|
|
1957
|
+
end: signal ?? endFor("no_report"),
|
|
1583
1958
|
sessionId: str$1(line["sessionId"]) || sessionId,
|
|
1584
1959
|
report: null,
|
|
1585
1960
|
text: str$1(line["finalText"]),
|
|
@@ -1596,12 +1971,21 @@ const commandCodeDecoder = () => {
|
|
|
1596
1971
|
}
|
|
1597
1972
|
};
|
|
1598
1973
|
};
|
|
1599
|
-
return
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1974
|
+
return {
|
|
1975
|
+
decode: (line) => {
|
|
1976
|
+
const message = parseLine$1(line);
|
|
1977
|
+
if (!isObject$1(message)) return { events: [] };
|
|
1978
|
+
if (message["type"] === "result") {
|
|
1979
|
+
held = message;
|
|
1980
|
+
return { events: [] };
|
|
1981
|
+
}
|
|
1982
|
+
const event = message["event"];
|
|
1983
|
+
return { events: message["type"] === "event" && isObject$1(event) ? frame(event) : [] };
|
|
1984
|
+
},
|
|
1985
|
+
exit: (code) => {
|
|
1986
|
+
if (held !== void 0) return [result(held, code)];
|
|
1987
|
+
return (code === null ? void 0 : COMMAND_CODE_EXITS[code]) === void 0 ? [] : [result({ error: "" }, code)];
|
|
1988
|
+
}
|
|
1605
1989
|
};
|
|
1606
1990
|
};
|
|
1607
1991
|
/** Command Code reports tokens, never dollars. */
|
|
@@ -1617,12 +2001,14 @@ const commandCodeUsage = {
|
|
|
1617
2001
|
* `--permission-mode yolo` and loads, with `--mod`, a mod written for the
|
|
1618
2002
|
* session outside the checkout: its `beforeToolCall` hook blocks every call
|
|
1619
2003
|
* the node did not allow. Ork's own tools (`mcp__ork__*`) always go through.
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
2004
|
+
* Command Code's tool loader passes, and reads inside the worktree need no
|
|
2005
|
+
* rule, as with Claude Code; one reaching outside it does. The rules are the
|
|
2006
|
+
* node's, in Claude Code's words (`Bash(git:*)`, `Read`, `mcp__github`); a
|
|
2007
|
+
* specifier Ork does not read never allows a call and always denies one.
|
|
1623
2008
|
*/
|
|
1624
2009
|
/** The rules of a session: an `acceptEdits` node may edit without a rule. */
|
|
1625
2010
|
const modRules = (spec) => ({
|
|
2011
|
+
cwd: spec.cwd,
|
|
1626
2012
|
allow: [...spec.tools.allow, ...spec.permissionMode === "acceptEdits" ? ["Edit"] : []],
|
|
1627
2013
|
deny: spec.tools.deny,
|
|
1628
2014
|
...spec.tools.only === void 0 ? {} : { only: spec.tools.only }
|
|
@@ -1632,7 +2018,20 @@ const modRules = (spec) => ({
|
|
|
1632
2018
|
* export taking the mod API), so it carries its own matcher.
|
|
1633
2019
|
*/
|
|
1634
2020
|
const modSource = (rules) => `// Ork's rules for one session; written by Ork, removed after it.
|
|
2021
|
+
import { relative, resolve } from "node:path";
|
|
1635
2022
|
const RULES = ${JSON.stringify(rules)};
|
|
2023
|
+
// Command Code's own: loading a deferred tool's schema, Ork's report included.
|
|
2024
|
+
const META = new Set(["search_tools", "tool_search"]);
|
|
2025
|
+
// Reads a session makes in its worktree without a rule, as Claude Code does.
|
|
2026
|
+
const READS = new Set(["read_file", "read_multiple_files", "read_directory", "glob", "grep"]);
|
|
2027
|
+
const pathsOf = input => Object.entries(input ?? {})
|
|
2028
|
+
.filter(([key]) => /path|file|dir/i.test(key))
|
|
2029
|
+
.flatMap(([, value]) => (Array.isArray(value) ? value : [value]))
|
|
2030
|
+
.filter(value => typeof value === "string");
|
|
2031
|
+
const inside = input => pathsOf(input).every(each => {
|
|
2032
|
+
const from = relative(RULES.cwd, resolve(RULES.cwd, each));
|
|
2033
|
+
return from === "" || (!from.startsWith("..") && !from.startsWith("/"));
|
|
2034
|
+
});
|
|
1636
2035
|
const TOOLS = ${JSON.stringify(COMMAND_CODE_TOOLS)};
|
|
1637
2036
|
// A rule's word, as Command Code reads it, covers these of Ork's tool names.
|
|
1638
2037
|
const COVERS = { read: ["Read", "Glob", "Grep"], edit: ["Edit", "Write"] };
|
|
@@ -1671,10 +2070,11 @@ const matches = (rule, name, input) => {
|
|
|
1671
2070
|
};
|
|
1672
2071
|
export const decide = (name, input) => {
|
|
1673
2072
|
if (RULES.deny.some(rule => matches(rule, name, input) !== false)) return "denied for this node";
|
|
1674
|
-
if (name.startsWith("mcp__ork__")) return undefined;
|
|
2073
|
+
if (name.startsWith("mcp__ork__") || META.has(name)) return undefined;
|
|
1675
2074
|
if (RULES.only !== undefined && !RULES.only.some(rule => matches(rule, name, input) === true)) {
|
|
1676
2075
|
return "not one of this node's tools";
|
|
1677
2076
|
}
|
|
2077
|
+
if (READS.has(name) && inside(input)) return undefined;
|
|
1678
2078
|
if (RULES.allow.some(rule => matches(rule, name, input) === true)) return undefined;
|
|
1679
2079
|
return "not allowed for this node";
|
|
1680
2080
|
};
|
|
@@ -1718,8 +2118,10 @@ const queryOf = (spec) => {
|
|
|
1718
2118
|
return text.startsWith("-") ? ` ${text}` : text;
|
|
1719
2119
|
};
|
|
1720
2120
|
/**
|
|
1721
|
-
* The `cmd` arguments for a session spec.
|
|
1722
|
-
*
|
|
2121
|
+
* The `cmd` arguments for a session spec. `--dangerously-skip-permissions`: in
|
|
2122
|
+
* `-p` mode only it lifts Command Code's own gate on edits and shell (the
|
|
2123
|
+
* permission mode alone does not), so the session's mod, which refuses what the
|
|
2124
|
+
* node did not allow, is the only gate.
|
|
1723
2125
|
*/
|
|
1724
2126
|
const commandCodeArgs = (spec) => {
|
|
1725
2127
|
const args = [
|
|
@@ -1729,6 +2131,7 @@ const commandCodeArgs = (spec) => {
|
|
|
1729
2131
|
"json",
|
|
1730
2132
|
"--permission-mode",
|
|
1731
2133
|
"yolo",
|
|
2134
|
+
"--dangerously-skip-permissions",
|
|
1732
2135
|
"--skip-onboarding",
|
|
1733
2136
|
"--no-auto-update"
|
|
1734
2137
|
];
|
|
@@ -1747,8 +2150,8 @@ const commandCodeAdapter = {
|
|
|
1747
2150
|
bin: "cmd",
|
|
1748
2151
|
args: (spec) => commandCodeArgs(spec)
|
|
1749
2152
|
},
|
|
1750
|
-
decode: (line) => commandCodeDecoder()(line),
|
|
1751
|
-
|
|
2153
|
+
decode: (line) => commandCodeDecoder().decode(line),
|
|
2154
|
+
legDecoder: () => commandCodeDecoder(),
|
|
1752
2155
|
mcp: {
|
|
1753
2156
|
kind: "lease",
|
|
1754
2157
|
wire: async (spec, endpoint, hooks) => {
|
|
@@ -1856,8 +2259,16 @@ const ACTIONS = {
|
|
|
1856
2259
|
TodoWrite: "todowrite"
|
|
1857
2260
|
};
|
|
1858
2261
|
const BUILT_IN = new Set(Object.values(ACTIONS));
|
|
1859
|
-
/**
|
|
2262
|
+
/**
|
|
2263
|
+
* Actions whose resource is a path. OpenCode matches it relative to the
|
|
2264
|
+
* worktree (an absolute path inside it too), `*` across folders, and asks
|
|
2265
|
+
* `external_directory`, denied first, for any path outside it.
|
|
2266
|
+
*/
|
|
1860
2267
|
const PATH_ACTIONS = /* @__PURE__ */ new Set(["read", "edit"]);
|
|
2268
|
+
/** A rule's path as OpenCode matches it: relative to the worktree. */
|
|
2269
|
+
const pathResource = (spec, cwd) => {
|
|
2270
|
+
return (path.isAbsolute(spec) ? path.relative(cwd, spec) : spec).replace(/^\.\//u, "") || "*";
|
|
2271
|
+
};
|
|
1861
2272
|
/** OpenCode's name for an MCP server's tool: other characters become `_`. */
|
|
1862
2273
|
const mcpAction = (server, tool) => `${server}_${tool}`.replaceAll(/[^A-Za-z0-9_*-]/gu, "_");
|
|
1863
2274
|
/** A shell rule's resource: Claude Code's `git status:*` is `git status *`. */
|
|
@@ -1883,16 +2294,22 @@ const ruleOf = (rule, effect, cwd) => {
|
|
|
1883
2294
|
if (action === void 0) return void 0;
|
|
1884
2295
|
if (spec !== void 0) return {
|
|
1885
2296
|
action,
|
|
1886
|
-
resource: action === "shell" ? shellResource(spec) : PATH_ACTIONS.has(action) ?
|
|
2297
|
+
resource: action === "shell" ? shellResource(spec) : PATH_ACTIONS.has(action) ? pathResource(spec, cwd) : spec,
|
|
1887
2298
|
effect
|
|
1888
2299
|
};
|
|
1889
2300
|
return {
|
|
1890
2301
|
action,
|
|
1891
|
-
resource:
|
|
2302
|
+
resource: "*",
|
|
1892
2303
|
effect
|
|
1893
2304
|
};
|
|
1894
2305
|
};
|
|
1895
2306
|
const rulesOf = (rules, effect, cwd) => rules.flatMap((rule) => ruleOf(rule, effect, cwd) ?? []);
|
|
2307
|
+
/** An action allowed anywhere in the worktree; outside it is denied. */
|
|
2308
|
+
const worktree = (action) => ({
|
|
2309
|
+
action,
|
|
2310
|
+
resource: "*",
|
|
2311
|
+
effect: "allow"
|
|
2312
|
+
});
|
|
1896
2313
|
/**
|
|
1897
2314
|
* The session's rules, in order: deny everything; read in the worktree (as
|
|
1898
2315
|
* Claude Code reads without asking); edits there under `acceptEdits`; the
|
|
@@ -1903,11 +2320,6 @@ const permissionRules = (spec) => {
|
|
|
1903
2320
|
const { cwd, tools } = spec;
|
|
1904
2321
|
const only = tools.only === void 0 ? void 0 : new Set(rulesOf(tools.only, "allow", cwd).map((r) => r.action));
|
|
1905
2322
|
const inOnly = (rule) => only === void 0 || only.has(rule.action) || !BUILT_IN.has(rule.action);
|
|
1906
|
-
const worktree = (action) => ({
|
|
1907
|
-
action,
|
|
1908
|
-
resource: `${cwd}/*`,
|
|
1909
|
-
effect: "allow"
|
|
1910
|
-
});
|
|
1911
2323
|
return [
|
|
1912
2324
|
{
|
|
1913
2325
|
action: "*",
|
|
@@ -1977,6 +2389,16 @@ const sessionConfig = (spec, endpoint, instructions) => ({
|
|
|
1977
2389
|
} }
|
|
1978
2390
|
});
|
|
1979
2391
|
// ../runtime/src/adapters/open-code-events.ts
|
|
2392
|
+
/**
|
|
2393
|
+
* @file The `open-code` adapter's decoder and usage reader. `opencode run
|
|
2394
|
+
* --format json` prints one JSON object per line, `{type, timestamp,
|
|
2395
|
+
* sessionID, part}` (2.1.0, read from its source): `step_start`, `text`,
|
|
2396
|
+
* `tool_use` (a finished call, `state.status` `completed` or `error`),
|
|
2397
|
+
* `step_finish` (`cost` in dollars, `tokens`) and `error`. There is no result
|
|
2398
|
+
* line: the last `text` is the final text and the process's exit ends the
|
|
2399
|
+
* session. Tool names come back in Claude Code's words (`shell` is `Bash`,
|
|
2400
|
+
* `ork_report` is `mcp__ork__report`), the words a node's rules use.
|
|
2401
|
+
*/
|
|
1980
2402
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1981
2403
|
const get = (value, key) => isObject(value) ? value[key] : void 0;
|
|
1982
2404
|
const num = (value) => typeof value === "number" ? value : 0;
|
|
@@ -2075,10 +2497,27 @@ const decodeOpenCodeLine = (line) => {
|
|
|
2075
2497
|
default: return { events: [] };
|
|
2076
2498
|
}
|
|
2077
2499
|
};
|
|
2500
|
+
/** An `error` line's words, for the session's errors. */
|
|
2501
|
+
const errorText = (error) => str(get(get(error, "data"), "message")) || str(get(error, "message")) || str(get(error, "name")) || "error";
|
|
2502
|
+
/** An `error` line's end: its name, status and body, through the classifier. */
|
|
2503
|
+
const errorLineEnd = (error) => {
|
|
2504
|
+
const data = get(error, "data");
|
|
2505
|
+
const status = get(data, "statusCode");
|
|
2506
|
+
const said = [
|
|
2507
|
+
str(get(error, "name")),
|
|
2508
|
+
errorText(error),
|
|
2509
|
+
str(get(data, "responseBody"))
|
|
2510
|
+
].filter((part) => part !== "").join("\n");
|
|
2511
|
+
return errorEnd(said, "unknown", typeof status === "number" ? status : void 0);
|
|
2512
|
+
};
|
|
2513
|
+
/** The exit code OpenCode gives an interrupted session. */
|
|
2514
|
+
const INTERRUPTED = 130;
|
|
2078
2515
|
/**
|
|
2079
2516
|
* A decoder for one leg of a session: it starts the session at the first line
|
|
2080
2517
|
* that names it, and keeps the last text, the steps' usage and the errors for
|
|
2081
|
-
* the end.
|
|
2518
|
+
* the end. OpenCode prints nothing at its step cap (`steps`, the node's
|
|
2519
|
+
* `maxTurns`): a session that used every step and handed in no report reached
|
|
2520
|
+
* its turn limit, which the runner settles once it knows the report.
|
|
2082
2521
|
*/
|
|
2083
2522
|
const openCodeDecoder = (spec) => {
|
|
2084
2523
|
const began = Date.now();
|
|
@@ -2086,6 +2525,7 @@ const openCodeDecoder = (spec) => {
|
|
|
2086
2525
|
let text = "";
|
|
2087
2526
|
let usage = NONE;
|
|
2088
2527
|
const errors = [];
|
|
2528
|
+
let failed = null;
|
|
2089
2529
|
return {
|
|
2090
2530
|
decode: (line) => {
|
|
2091
2531
|
const message = parseLine(line);
|
|
@@ -2108,20 +2548,22 @@ const openCodeDecoder = (spec) => {
|
|
|
2108
2548
|
case "step_finish":
|
|
2109
2549
|
usage = sumUsage(usage, stepUsage(message["part"]));
|
|
2110
2550
|
break;
|
|
2111
|
-
case "error":
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
break;
|
|
2115
|
-
}
|
|
2551
|
+
case "error":
|
|
2552
|
+
errors.push(errorText(message["error"]));
|
|
2553
|
+
failed ??= errorLineEnd(message["error"]);
|
|
2116
2554
|
}
|
|
2117
2555
|
return { events };
|
|
2118
2556
|
},
|
|
2119
2557
|
exit: (code) => {
|
|
2120
|
-
|
|
2558
|
+
const interrupted = code === INTERRUPTED;
|
|
2559
|
+
if (code !== 0 && errors.length === 0 && !interrupted) return [];
|
|
2560
|
+
const capped = spec.maxTurns !== void 0 && usage.turns >= spec.maxTurns;
|
|
2561
|
+
const end = interrupted ? endFor("interrupted", {}, errors.join("\n")) : failed ?? endFor(capped ? "turn_limit" : "no_report");
|
|
2121
2562
|
return [{
|
|
2122
2563
|
type: "session.finished",
|
|
2123
2564
|
outcome: {
|
|
2124
2565
|
subtype: code === 0 ? "success" : "error_during_execution",
|
|
2566
|
+
end,
|
|
2125
2567
|
sessionId,
|
|
2126
2568
|
report: null,
|
|
2127
2569
|
text,
|
|
@@ -2230,7 +2672,7 @@ const notBuilt = (driver) => ({
|
|
|
2230
2672
|
driver,
|
|
2231
2673
|
start: () => ({
|
|
2232
2674
|
events: (async function* () {
|
|
2233
|
-
yield driverFailure(notBuiltYet(driver));
|
|
2675
|
+
yield driverFailure(notBuiltYet(driver), "model_unavailable", { facts: {} });
|
|
2234
2676
|
})(),
|
|
2235
2677
|
send: () => {},
|
|
2236
2678
|
interrupt: () => Promise.resolve()
|