@karowanorg/orc-ops 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -0
- package/README.md +5 -0
- package/dist/exec-harness.d.ts +2 -0
- package/dist/exec-harness.js +78 -0
- package/dist/guide.d.ts +6 -0
- package/dist/guide.js +145 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/ops.d.ts +234 -0
- package/dist/ops.js +678 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +35 -0
- package/dist/supervisor-entry.d.ts +1 -0
- package/dist/supervisor-entry.js +15 -0
- package/dist/supervisor-run.d.ts +2 -0
- package/dist/supervisor-run.js +67 -0
- package/package.json +32 -0
package/dist/ops.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The zod-first operation registry — the single source of truth for the CLI
|
|
3
|
+
* command tree, the MCP tool schemas (passed to the MCP SDK natively), and the
|
|
4
|
+
* SDK types (z.infer). No codegen step; heads interpret this at runtime.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { DEFAULT_POLICY, ProgramVM, acquireLock, appendControl, compileProgram, listRuns, openApprovals, prepareRun, readJournal, readManifest, readResult, readTraces, runPaths, sourceRequestsWrite, statusForRun, superviseRun, } from "@karowanorg/orc-core";
|
|
12
|
+
import { MonitorServer, openInBrowser, portForHome, writeReport } from "@karowanorg/orc-ui";
|
|
13
|
+
import { orcHome } from "@karowanorg/orc-core";
|
|
14
|
+
import { GUIDE } from "./guide.js";
|
|
15
|
+
function defineOp(def) {
|
|
16
|
+
return def;
|
|
17
|
+
}
|
|
18
|
+
const ApprovalMode = z.enum(["manual", "accept-edits", "auto", "bypass"]);
|
|
19
|
+
const RunId = z.string().regex(/^[a-zA-Z0-9_-]+$/, "run id");
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const launch = defineOp({
|
|
22
|
+
name: "launch",
|
|
23
|
+
doc: "Compile, pin, and start a program as a new run. Returns immediately unless --wait.",
|
|
24
|
+
readOnly: false,
|
|
25
|
+
input: z.object({
|
|
26
|
+
programPath: z.string().describe("path to the program (.orc.ts/.ts/.js)"),
|
|
27
|
+
cwd: z.string().optional().describe("working directory (plain path); defaults to the caller's cwd"),
|
|
28
|
+
host: z.string().optional().describe("SSH destination (separate field; e.g. 'build@ci-box' or an ssh-config alias)"),
|
|
29
|
+
brief: z.string().describe("shared context injected into every leaf"),
|
|
30
|
+
allowWrites: z.boolean().default(false).describe("grant write-declared leaves permission to mutate files"),
|
|
31
|
+
approvalMode: ApprovalMode.default("auto").describe("manual | accept-edits | auto | bypass"),
|
|
32
|
+
sandbox: z.boolean().default(false).describe("confine write leaves to cwd + sandboxDirs (default: unconfined, like the caller)"),
|
|
33
|
+
sandboxDirs: z.array(z.string()).optional().describe("extra writable roots when sandboxed (e.g. cache dirs outside the workspace)"),
|
|
34
|
+
maxParallel: z.number().int().min(1).max(64).optional(),
|
|
35
|
+
idleTimeoutSeconds: z.number().int().optional().describe("run default for the per-call output-idle watchdog; 0 disables"),
|
|
36
|
+
budget: z.number().positive().optional().describe("reactive USD cap: fail the run once observed estimated cost exceeds this (may overshoot)"),
|
|
37
|
+
name: z.string().optional(),
|
|
38
|
+
harness: z.string().optional().describe("default harness override (else caller affinity → codex)"),
|
|
39
|
+
wait: z.boolean().default(false).describe("supervise in the foreground and return the final status"),
|
|
40
|
+
}),
|
|
41
|
+
async handler(input, ctx) {
|
|
42
|
+
const manifest = await prepareRun({
|
|
43
|
+
programPath: input.programPath,
|
|
44
|
+
cwd: input.cwd,
|
|
45
|
+
host: input.host,
|
|
46
|
+
brief: input.brief,
|
|
47
|
+
allowWrites: input.allowWrites,
|
|
48
|
+
approvalMode: input.approvalMode,
|
|
49
|
+
sandbox: input.sandbox,
|
|
50
|
+
sandboxDirs: input.sandboxDirs,
|
|
51
|
+
maxParallel: input.maxParallel,
|
|
52
|
+
idleTimeout: input.idleTimeoutSeconds === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: input.idleTimeoutSeconds === 0
|
|
55
|
+
? false
|
|
56
|
+
: input.idleTimeoutSeconds * 1000,
|
|
57
|
+
budgetUsd: input.budget,
|
|
58
|
+
name: input.name,
|
|
59
|
+
defaultHarness: input.harness,
|
|
60
|
+
}, ctx.registry);
|
|
61
|
+
const bundle = fs.readFileSync(runPaths(manifest.runId).program, "utf8");
|
|
62
|
+
const requestsWrite = sourceRequestsWrite(bundle);
|
|
63
|
+
writeReport(manifest.runId);
|
|
64
|
+
const monitorUrl = `http://127.0.0.1:${portForHome(orcHome())}/runs/${manifest.runId}`;
|
|
65
|
+
if (input.wait) {
|
|
66
|
+
const status = await superviseRun(manifest.runId, ctx.registry, { onUpdate: debouncedReport() });
|
|
67
|
+
return { runId: manifest.runId, requestsWrite, monitorUrl, reportPath: runPaths(manifest.runId).report, status };
|
|
68
|
+
}
|
|
69
|
+
await spawnDetachedSupervisor(manifest.runId, ctx.registryCwd);
|
|
70
|
+
return {
|
|
71
|
+
runId: manifest.runId,
|
|
72
|
+
requestsWrite,
|
|
73
|
+
allowWrites: manifest.allowWrites,
|
|
74
|
+
approvalMode: manifest.approvalMode,
|
|
75
|
+
monitorUrl,
|
|
76
|
+
reportPath: runPaths(manifest.runId).report,
|
|
77
|
+
wait: { op: "wait", input: { runId: manifest.runId, timeoutSeconds: 300 } },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
export const validate = defineOp({
|
|
82
|
+
name: "validate",
|
|
83
|
+
doc: "Compile a program and preview its first frontier without starting a run.",
|
|
84
|
+
readOnly: true,
|
|
85
|
+
input: z.object({
|
|
86
|
+
programPath: z.string(),
|
|
87
|
+
allowWrites: z.boolean().default(false),
|
|
88
|
+
approvalMode: ApprovalMode.default("auto").describe("check the first-frontier harnesses can honor this mode"),
|
|
89
|
+
harness: z.string().optional().describe("default harness override (same precedence as launch)"),
|
|
90
|
+
host: z.string().optional().describe("check capabilities on this remote host"),
|
|
91
|
+
checkCapabilities: z.boolean().default(true).describe("probe referenced harnesses for model/effort/mode support (slower)"),
|
|
92
|
+
}),
|
|
93
|
+
async handler(input, ctx) {
|
|
94
|
+
const { bundle, sha256 } = await compileProgram(input.programPath);
|
|
95
|
+
const requestsWrite = sourceRequestsWrite(bundle);
|
|
96
|
+
const firstCalls = [];
|
|
97
|
+
const problems = [];
|
|
98
|
+
let vm;
|
|
99
|
+
try {
|
|
100
|
+
vm = await ProgramVM.create(bundle, DEFAULT_POLICY, {
|
|
101
|
+
onCall: (seq, spec) => {
|
|
102
|
+
firstCalls.push({
|
|
103
|
+
seq,
|
|
104
|
+
kind: spec.kind,
|
|
105
|
+
id: spec.id,
|
|
106
|
+
readOnly: spec.readOnly,
|
|
107
|
+
harness: spec.harness,
|
|
108
|
+
host: spec.host,
|
|
109
|
+
model: spec.model,
|
|
110
|
+
reasoningEffort: spec.reasoningEffort,
|
|
111
|
+
schema: spec.schema,
|
|
112
|
+
promptPreview: spec.prompt?.slice(0, 120),
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
onLog: () => undefined,
|
|
116
|
+
onPhase: () => undefined,
|
|
117
|
+
});
|
|
118
|
+
const state = vm.state();
|
|
119
|
+
if (state.state === "error")
|
|
120
|
+
problems.push(`program failed before its first call: ${state.error}`);
|
|
121
|
+
if (state.state === "ok" && firstCalls.length === 0)
|
|
122
|
+
problems.push("program completes without dispatching any leaf");
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
problems.push(String(err instanceof Error ? err.message : err));
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
vm?.dispose();
|
|
129
|
+
}
|
|
130
|
+
// Host can change per leaf, so only share probes with the same effective
|
|
131
|
+
// harness and executor destination.
|
|
132
|
+
const capsCache = new Map();
|
|
133
|
+
const capsFor = (harnessName, host) => {
|
|
134
|
+
const key = JSON.stringify([harnessName, host]);
|
|
135
|
+
const cached = capsCache.get(key);
|
|
136
|
+
if (cached)
|
|
137
|
+
return cached;
|
|
138
|
+
const harness = ctx.registry.harnesses.get(harnessName);
|
|
139
|
+
if (!harness)
|
|
140
|
+
return Promise.reject(new Error(`unknown harness "${harnessName}"`));
|
|
141
|
+
const pending = Promise.resolve().then(() => harness.discover({ executor: ctx.registry.executorFor(host) }));
|
|
142
|
+
capsCache.set(key, pending);
|
|
143
|
+
return pending;
|
|
144
|
+
};
|
|
145
|
+
for (const call of firstCalls) {
|
|
146
|
+
if (call.kind.startsWith("ext:")) {
|
|
147
|
+
const extension = ctx.registry.extensions.get(call.kind.slice(4));
|
|
148
|
+
if (!extension) {
|
|
149
|
+
problems.push(`call ${call.seq} uses unregistered extension ${call.kind} (register it in orc.config)`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Runtime registration is authoritative; the program cannot make a
|
|
153
|
+
// write extension appear read-only.
|
|
154
|
+
call.readOnly = extension.readOnly;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!call.readOnly && !input.allowWrites) {
|
|
158
|
+
problems.push(`call ${call.seq} declares readOnly:false but allowWrites was not granted (fail-closed at dispatch)`);
|
|
159
|
+
}
|
|
160
|
+
if (call.kind === "agent") {
|
|
161
|
+
const harnessName = call.harness ?? input.harness ?? ctx.registry.defaultHarness;
|
|
162
|
+
const host = call.host ?? input.host;
|
|
163
|
+
const harness = ctx.registry.harnesses.get(harnessName);
|
|
164
|
+
if (!harness) {
|
|
165
|
+
problems.push(`call ${call.seq} uses unknown harness "${harnessName}" (available: ${[...ctx.registry.harnesses.keys()].join(", ")})`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Static structured-output check — no live probe, so it runs even
|
|
169
|
+
// with --no-check-capabilities: reject a schema the harness would
|
|
170
|
+
// fail on at invocation time (e.g. codex strict mode's open maps).
|
|
171
|
+
if (call.schema !== undefined && harness.lintOutputSchema) {
|
|
172
|
+
for (const p of harness.lintOutputSchema(call.schema)) {
|
|
173
|
+
problems.push(`call ${call.seq} output schema ${p}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (harness && input.checkCapabilities) {
|
|
178
|
+
let caps;
|
|
179
|
+
try {
|
|
180
|
+
caps = await capsFor(harnessName, host);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
problems.push(`call ${call.seq}: could not discover harness "${harnessName}"${host ? ` on ${host}` : ""} (${String(err instanceof Error ? err.message : err)})`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!caps.available) {
|
|
187
|
+
problems.push(`call ${call.seq}: harness "${harnessName}" is not available (${caps.detail ?? "not found"})`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
if (call.schema !== undefined && !caps.structuredOutput) {
|
|
191
|
+
problems.push(`call ${call.seq}: harness "${harnessName}" does not support structured output`);
|
|
192
|
+
}
|
|
193
|
+
const modelIds = caps.models.map((m) => m.id);
|
|
194
|
+
const matched = call.model ? caps.models.find((m) => m.id === call.model) : undefined;
|
|
195
|
+
if (call.model && modelIds.length > 0 && !matched) {
|
|
196
|
+
problems.push(`call ${call.seq}: model "${call.model}" not in ${harnessName}'s catalog (${modelIds.slice(0, 6).join(", ")}…)`);
|
|
197
|
+
}
|
|
198
|
+
if (call.reasoningEffort) {
|
|
199
|
+
if (matched) {
|
|
200
|
+
// The model is known: its ladder is authoritative. An empty
|
|
201
|
+
// ladder means the model takes no effort param at all, so any
|
|
202
|
+
// effort is invalid (e.g. Haiku vs. Fable).
|
|
203
|
+
if (matched.reasoningEfforts.length === 0) {
|
|
204
|
+
problems.push(`call ${call.seq}: model "${matched.id}" takes no reasoning effort (drop reasoningEffort "${call.reasoningEffort}")`);
|
|
205
|
+
}
|
|
206
|
+
else if (!matched.reasoningEfforts.includes(call.reasoningEffort)) {
|
|
207
|
+
problems.push(`call ${call.seq}: reasoning effort "${call.reasoningEffort}" not supported by model "${matched.id}" (${matched.reasoningEfforts.join(", ")})`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// No specific model pinned: gate against the union across the
|
|
212
|
+
// catalog, but only when we actually discovered efforts.
|
|
213
|
+
const union = [...new Set(caps.models.flatMap((m) => m.reasoningEfforts))];
|
|
214
|
+
if (union.length > 0 && !union.includes(call.reasoningEffort)) {
|
|
215
|
+
problems.push(`call ${call.seq}: reasoning effort "${call.reasoningEffort}" not supported by ${harnessName} (${union.join(", ")})`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const approvalMode = input.approvalMode ?? "auto";
|
|
220
|
+
if (caps.approvalModes.length > 0 && !caps.approvalModes.includes(approvalMode)) {
|
|
221
|
+
problems.push(`call ${call.seq}: harness "${harnessName}" cannot honor approval mode "${approvalMode}"${host ? " over ssh" : ""} (supports: ${caps.approvalModes.join(", ")})`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { ok: problems.length === 0, sha256, requestsWrite, firstCalls, problems };
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
export const status = defineOp({
|
|
231
|
+
name: "status",
|
|
232
|
+
doc: "Body-free status projection for a run.",
|
|
233
|
+
readOnly: true,
|
|
234
|
+
input: z.object({ runId: RunId }),
|
|
235
|
+
async handler(input) {
|
|
236
|
+
return statusForRun(input.runId);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
export const wait = defineOp({
|
|
240
|
+
name: "wait",
|
|
241
|
+
doc: "Block (bounded, ≤300s) until the run settles; returns status plus a retry handoff on timeout.",
|
|
242
|
+
readOnly: true,
|
|
243
|
+
input: z.object({
|
|
244
|
+
runId: RunId,
|
|
245
|
+
timeoutSeconds: z.number().int().min(1).max(300).default(120),
|
|
246
|
+
}),
|
|
247
|
+
async handler(input) {
|
|
248
|
+
const deadline = Date.now() + input.timeoutSeconds * 1000;
|
|
249
|
+
for (;;) {
|
|
250
|
+
const s = statusForRun(input.runId);
|
|
251
|
+
if (s.state !== "running")
|
|
252
|
+
return { outcome: s.state, timedOut: false, status: s };
|
|
253
|
+
if (Date.now() >= deadline) {
|
|
254
|
+
return {
|
|
255
|
+
outcome: "running",
|
|
256
|
+
timedOut: true,
|
|
257
|
+
status: s,
|
|
258
|
+
retry: { op: "wait", input: { runId: input.runId, timeoutSeconds: input.timeoutSeconds } },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
export const getResult = defineOp({
|
|
266
|
+
name: "get-result",
|
|
267
|
+
doc: "Hydrate one result body: the run's final result, or one leaf's by seq.",
|
|
268
|
+
readOnly: true,
|
|
269
|
+
input: z.object({
|
|
270
|
+
runId: RunId,
|
|
271
|
+
seq: z.number().int().optional().describe("leaf sequence number; omit for the final program result"),
|
|
272
|
+
}),
|
|
273
|
+
async handler(input) {
|
|
274
|
+
const paths = runPaths(input.runId);
|
|
275
|
+
const journal = readJournal(input.runId);
|
|
276
|
+
if (input.seq === undefined) {
|
|
277
|
+
const finish = [...journal].reverse().find((r) => r.t === "finish");
|
|
278
|
+
if (!finish)
|
|
279
|
+
throw new Error("run has no final result yet");
|
|
280
|
+
if (finish.status !== "completed" || !finish.resultSha) {
|
|
281
|
+
throw new Error(`run ${finish.status}: ${finish.error ?? "no result body"}`);
|
|
282
|
+
}
|
|
283
|
+
return { body: readResult(paths, finish.resultSha), sha256: finish.resultSha };
|
|
284
|
+
}
|
|
285
|
+
const done = [...journal]
|
|
286
|
+
.reverse()
|
|
287
|
+
.find((r) => r.t === "done" && r.seq === input.seq && r.status === "ok");
|
|
288
|
+
if (!done || done.t !== "done" || !done.resultSha)
|
|
289
|
+
throw new Error(`no ok completion for seq ${input.seq}`);
|
|
290
|
+
return { body: readResult(paths, done.resultSha), sha256: done.resultSha };
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
export const getTrace = defineOp({
|
|
294
|
+
name: "trace",
|
|
295
|
+
doc: "The run's trace: status projection plus bounded leaf/tool detail.",
|
|
296
|
+
readOnly: true,
|
|
297
|
+
input: z.object({ runId: RunId }),
|
|
298
|
+
async handler(input) {
|
|
299
|
+
const s = statusForRun(input.runId);
|
|
300
|
+
const traces = readTraces(input.runId).map((t) => {
|
|
301
|
+
if (t.t !== "leaf")
|
|
302
|
+
return t;
|
|
303
|
+
const bound = (v) => (v && v.length > 16_384 ? v.slice(0, 16_384) + `…[truncated]` : v);
|
|
304
|
+
return { ...t, prompt: bound(t.prompt), brief: bound(t.brief), error: bound(t.error) };
|
|
305
|
+
});
|
|
306
|
+
return { status: s, traces };
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
export const list = defineOp({
|
|
310
|
+
name: "list",
|
|
311
|
+
doc: "List runs, newest first.",
|
|
312
|
+
readOnly: true,
|
|
313
|
+
input: z.object({ limit: z.number().int().min(1).max(200).default(25) }),
|
|
314
|
+
async handler(input) {
|
|
315
|
+
return listRuns()
|
|
316
|
+
.slice(0, input.limit)
|
|
317
|
+
.map((m) => {
|
|
318
|
+
let state = "unknown";
|
|
319
|
+
try {
|
|
320
|
+
state = statusForRun(m.runId).state;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
/* partial run dir */
|
|
324
|
+
}
|
|
325
|
+
return { runId: m.runId, name: m.name, state, createdAtMs: m.createdAtMs, host: m.host, cwd: m.cwd };
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
export const cancel = defineOp({
|
|
330
|
+
name: "cancel",
|
|
331
|
+
doc: "Queue cancellation. Local leaves get TERM→KILL; plain SSH only confirms channel teardown, so remote process state may be unknown.",
|
|
332
|
+
readOnly: false,
|
|
333
|
+
input: z.object({ runId: RunId }),
|
|
334
|
+
async handler(input) {
|
|
335
|
+
requireRunningRun(input.runId);
|
|
336
|
+
appendControl(input.runId, { t: "cancel", atMs: Date.now() });
|
|
337
|
+
return { enqueued: true };
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
export const resume = defineOp({
|
|
341
|
+
name: "resume",
|
|
342
|
+
doc: "Resume a crashed or failed run. Write leaves re-dispatch as re-orienting attempts (never blind re-runs); fail-forward only.",
|
|
343
|
+
readOnly: false,
|
|
344
|
+
input: z.object({
|
|
345
|
+
runId: RunId,
|
|
346
|
+
wait: z.boolean().default(false),
|
|
347
|
+
}),
|
|
348
|
+
async handler(input, ctx) {
|
|
349
|
+
readManifest(input.runId); // throws if unknown
|
|
350
|
+
if (input.wait) {
|
|
351
|
+
const s = await superviseRun(input.runId, ctx.registry, { onUpdate: debouncedReport() });
|
|
352
|
+
return { runId: input.runId, status: s };
|
|
353
|
+
}
|
|
354
|
+
const current = statusForRun(input.runId);
|
|
355
|
+
if (current.state === "completed")
|
|
356
|
+
throw new Error(`run ${input.runId} already completed`);
|
|
357
|
+
const lock = await acquireLock(runPaths(input.runId));
|
|
358
|
+
await lock.release();
|
|
359
|
+
// tradeoff: releasing before spawn leaves a small ownership race; an
|
|
360
|
+
// atomic handoff needs a supervisor protocol rather than this one-shot CLI.
|
|
361
|
+
await spawnDetachedSupervisor(input.runId, ctx.registryCwd);
|
|
362
|
+
return { runId: input.runId, resumed: true };
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
export const listApprovals = defineOp({
|
|
366
|
+
name: "approvals",
|
|
367
|
+
doc: "List pending approval requests across a run.",
|
|
368
|
+
readOnly: true,
|
|
369
|
+
input: z.object({ runId: RunId }),
|
|
370
|
+
async handler(input) {
|
|
371
|
+
return openApprovals(readTraces(input.runId));
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
export const respondApproval = defineOp({
|
|
375
|
+
name: "respond",
|
|
376
|
+
doc: "Queue an answer for a pending approval request.",
|
|
377
|
+
readOnly: false,
|
|
378
|
+
input: z.object({
|
|
379
|
+
runId: RunId,
|
|
380
|
+
approvalId: z.string(),
|
|
381
|
+
behavior: z.enum(["allow", "deny"]),
|
|
382
|
+
message: z.string().optional(),
|
|
383
|
+
}),
|
|
384
|
+
async handler(input) {
|
|
385
|
+
requireRunningRun(input.runId);
|
|
386
|
+
if (!openApprovals(readTraces(input.runId)).some((approval) => approval.id === input.approvalId)) {
|
|
387
|
+
throw new Error(`approval ${input.approvalId} is not pending for run ${input.runId}`);
|
|
388
|
+
}
|
|
389
|
+
appendControl(input.runId, {
|
|
390
|
+
t: "approval",
|
|
391
|
+
approvalId: input.approvalId,
|
|
392
|
+
decision: { behavior: input.behavior, message: input.message },
|
|
393
|
+
by: "operator",
|
|
394
|
+
atMs: Date.now(),
|
|
395
|
+
});
|
|
396
|
+
return { enqueued: true };
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
export const capabilities = defineOp({
|
|
400
|
+
name: "capabilities",
|
|
401
|
+
doc: "List harnesses, models, and reasoning levels — each discovered through the harness's native mechanism.",
|
|
402
|
+
readOnly: true,
|
|
403
|
+
input: z.object({
|
|
404
|
+
host: z.string().optional().describe("probe a remote host instead of local"),
|
|
405
|
+
refresh: z.boolean().default(false),
|
|
406
|
+
}),
|
|
407
|
+
async handler(input, ctx) {
|
|
408
|
+
const executor = ctx.registry.executorFor(input.host);
|
|
409
|
+
const out = {};
|
|
410
|
+
for (const [name, harness] of ctx.registry.harnesses) {
|
|
411
|
+
out[name] = await harness.discover({ executor }).catch((err) => ({
|
|
412
|
+
available: false,
|
|
413
|
+
detail: String(err instanceof Error ? err.message : err),
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
host: input.host ?? "(local)",
|
|
418
|
+
defaultHarness: ctx.registry.defaultHarness,
|
|
419
|
+
harnesses: out,
|
|
420
|
+
extensions: [...ctx.registry.extensions.keys()],
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
export const openMonitor = defineOp({
|
|
425
|
+
name: "open",
|
|
426
|
+
doc: "Ensure the monitor UI server is up and return the live URL for a run.",
|
|
427
|
+
readOnly: true,
|
|
428
|
+
input: z.object({
|
|
429
|
+
runId: RunId.optional(),
|
|
430
|
+
browser: z.boolean().default(false).describe("also open the URL in the default browser"),
|
|
431
|
+
}),
|
|
432
|
+
async handler(input) {
|
|
433
|
+
const url = await ensureMonitor(input.runId);
|
|
434
|
+
if (input.browser)
|
|
435
|
+
openInBrowser(url);
|
|
436
|
+
return { url };
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
export const report = defineOp({
|
|
440
|
+
name: "report",
|
|
441
|
+
doc: "Write (or refresh) the self-contained report.html for a run and return its path.",
|
|
442
|
+
readOnly: true,
|
|
443
|
+
input: z.object({ runId: RunId }),
|
|
444
|
+
async handler(input) {
|
|
445
|
+
return { path: writeReport(input.runId) };
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
export const guide = defineOp({
|
|
449
|
+
name: "guide",
|
|
450
|
+
doc: "How to write and run an orc program — the authoring + usage guide, with this machine's live harness/model catalog appended.",
|
|
451
|
+
readOnly: true,
|
|
452
|
+
input: z.object({
|
|
453
|
+
host: z.string().optional().describe("probe this remote host's harnesses instead of local"),
|
|
454
|
+
probe: z.boolean().default(true).describe("append the live capability catalog (set false for the static doc only)"),
|
|
455
|
+
}),
|
|
456
|
+
async handler(input, ctx) {
|
|
457
|
+
if (!input.probe)
|
|
458
|
+
return { guide: GUIDE };
|
|
459
|
+
// Bake step-2 (capability discovery) into the guide response so a model
|
|
460
|
+
// goes straight from `guide` to writing, with valid harness/model/effort
|
|
461
|
+
// values in hand instead of guessing. Never let a slow or missing harness
|
|
462
|
+
// break the guide — degrade to the static doc, which already points at
|
|
463
|
+
// `orc capabilities`.
|
|
464
|
+
const caps = await withDeadline(capabilities.handler({ host: input.host, refresh: false }, ctx), 15_000).catch(() => null);
|
|
465
|
+
return { guide: GUIDE + (caps ? renderCapabilitiesForGuide(caps) : "") };
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
/** Race a promise against a deadline; reject if it doesn't settle in time. */
|
|
469
|
+
function withDeadline(p, ms) {
|
|
470
|
+
let timer;
|
|
471
|
+
return Promise.race([
|
|
472
|
+
p.finally(() => timer && clearTimeout(timer)),
|
|
473
|
+
new Promise((_, reject) => {
|
|
474
|
+
timer = setTimeout(() => reject(new Error("capability probe timed out")), ms);
|
|
475
|
+
}),
|
|
476
|
+
]);
|
|
477
|
+
}
|
|
478
|
+
/** Compact, model-readable rendering of the live catalog appended to the guide. */
|
|
479
|
+
function renderCapabilitiesForGuide(caps) {
|
|
480
|
+
const c = caps;
|
|
481
|
+
const lines = [
|
|
482
|
+
"",
|
|
483
|
+
"",
|
|
484
|
+
"## Available on this machine",
|
|
485
|
+
"",
|
|
486
|
+
`Discovered live${c.host && c.host !== "(local)" ? ` on ${c.host}` : ""} — these are the valid \`harness\`, \`model\`, and`,
|
|
487
|
+
`\`reasoningEffort\` values right now, so you don't have to guess. Default harness: **${c.defaultHarness}**.`,
|
|
488
|
+
"Omit any of them to take the default.",
|
|
489
|
+
];
|
|
490
|
+
for (const [name, h] of Object.entries(c.harnesses ?? {})) {
|
|
491
|
+
if (!h?.available) {
|
|
492
|
+
lines.push("", `- **${name}** — unavailable (${h?.detail ?? "not found"})`);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
lines.push("", `- **${name}**${h.version ? ` v${h.version}` : ""}`);
|
|
496
|
+
for (const m of h.models ?? []) {
|
|
497
|
+
const efforts = m.reasoningEfforts.length > 0
|
|
498
|
+
? `reasoningEffort: ${m.reasoningEfforts.join(", ")}`
|
|
499
|
+
: "no reasoningEffort";
|
|
500
|
+
lines.push(` - \`${m.id}\`${m.default ? " (default)" : ""} — ${efforts}`);
|
|
501
|
+
}
|
|
502
|
+
if (h.approvalModes?.length)
|
|
503
|
+
lines.push(` - approval modes: ${h.approvalModes.join(", ")}`);
|
|
504
|
+
}
|
|
505
|
+
lines.push("", "Re-run `orc capabilities` any time, or `orc capabilities --host <ssh>` for another machine.");
|
|
506
|
+
return lines.join("\n");
|
|
507
|
+
}
|
|
508
|
+
export const doctor = defineOp({
|
|
509
|
+
name: "doctor",
|
|
510
|
+
doc: "Preflight an executor: harness binaries, versions, cwd existence.",
|
|
511
|
+
readOnly: true,
|
|
512
|
+
input: z.object({
|
|
513
|
+
host: z.string().optional(),
|
|
514
|
+
cwd: z.string().optional(),
|
|
515
|
+
}),
|
|
516
|
+
async handler(input, ctx) {
|
|
517
|
+
const executor = ctx.registry.executorFor(input.host);
|
|
518
|
+
const checks = {};
|
|
519
|
+
for (const [name, harness] of ctx.registry.harnesses) {
|
|
520
|
+
const caps = await harness.discover({ executor }).catch(() => null);
|
|
521
|
+
checks[name] = caps ? { available: caps.available, version: caps.version, detail: caps.detail } : { available: false };
|
|
522
|
+
}
|
|
523
|
+
if (input.cwd) {
|
|
524
|
+
checks.cwd = { path: input.cwd, exists: await executor.exists(input.cwd) };
|
|
525
|
+
}
|
|
526
|
+
return { host: input.host ?? "(local)", checks };
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
export const ALL_OPS = [
|
|
531
|
+
guide,
|
|
532
|
+
launch,
|
|
533
|
+
validate,
|
|
534
|
+
status,
|
|
535
|
+
wait,
|
|
536
|
+
getResult,
|
|
537
|
+
getTrace,
|
|
538
|
+
list,
|
|
539
|
+
cancel,
|
|
540
|
+
resume,
|
|
541
|
+
listApprovals,
|
|
542
|
+
respondApproval,
|
|
543
|
+
capabilities,
|
|
544
|
+
openMonitor,
|
|
545
|
+
report,
|
|
546
|
+
doctor,
|
|
547
|
+
];
|
|
548
|
+
/** Machine-readable catalog for `orc commands --json` and agent discovery. */
|
|
549
|
+
export function catalog() {
|
|
550
|
+
return ALL_OPS.map((op) => ({
|
|
551
|
+
name: op.name,
|
|
552
|
+
doc: op.doc,
|
|
553
|
+
readOnly: op.readOnly,
|
|
554
|
+
inputSchema: z.toJSONSchema(op.input),
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
function debouncedReport() {
|
|
559
|
+
let last = 0;
|
|
560
|
+
let timer = null;
|
|
561
|
+
return (runId) => {
|
|
562
|
+
const now = Date.now();
|
|
563
|
+
if (now - last > 1000) {
|
|
564
|
+
last = now;
|
|
565
|
+
try {
|
|
566
|
+
writeReport(runId);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
/* best-effort */
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else if (!timer) {
|
|
573
|
+
timer = setTimeout(() => {
|
|
574
|
+
timer = null;
|
|
575
|
+
last = Date.now();
|
|
576
|
+
try {
|
|
577
|
+
writeReport(runId);
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
/* best-effort */
|
|
581
|
+
}
|
|
582
|
+
}, 1200);
|
|
583
|
+
timer.unref();
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function requireRunningRun(runId) {
|
|
588
|
+
const current = statusForRun(runId);
|
|
589
|
+
if (current.state !== "running")
|
|
590
|
+
throw new Error(`run ${runId} is ${current.state}, not running`);
|
|
591
|
+
return current;
|
|
592
|
+
}
|
|
593
|
+
function detachedSupervisorArgs(runId) {
|
|
594
|
+
const owner = fileURLToPath(import.meta.url);
|
|
595
|
+
const basename = path.basename(owner);
|
|
596
|
+
if (basename === "ops.ts") {
|
|
597
|
+
return ["--import", "tsx", fileURLToPath(new URL("./supervisor-entry.ts", import.meta.url)), runId];
|
|
598
|
+
}
|
|
599
|
+
if (basename === "ops.js") {
|
|
600
|
+
return [fileURLToPath(new URL("./supervisor-entry.js", import.meta.url)), runId];
|
|
601
|
+
}
|
|
602
|
+
if (basename === "orc.mjs")
|
|
603
|
+
return [owner, "_supervise", runId];
|
|
604
|
+
throw new Error(`detached supervisor entry is unavailable from bundled @karowanorg/orc-ops (${basename}); keep @karowanorg/orc-ops external`);
|
|
605
|
+
}
|
|
606
|
+
/** Start the package-owned detached supervisor and wait for its startup signal. */
|
|
607
|
+
export async function spawnDetachedSupervisor(runId, registryCwd) {
|
|
608
|
+
const args = detachedSupervisorArgs(runId);
|
|
609
|
+
const env = { ...process.env };
|
|
610
|
+
if (registryCwd === undefined)
|
|
611
|
+
delete env.ORC_SUPERVISOR_REGISTRY_CWD;
|
|
612
|
+
else
|
|
613
|
+
env.ORC_SUPERVISOR_REGISTRY_CWD = registryCwd;
|
|
614
|
+
const child = spawn(process.execPath, args, {
|
|
615
|
+
detached: true,
|
|
616
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
617
|
+
env,
|
|
618
|
+
});
|
|
619
|
+
await new Promise((resolve, reject) => {
|
|
620
|
+
const timer = setTimeout(() => finish(new Error("detached supervisor did not start within 10 seconds")), 10_000);
|
|
621
|
+
const cleanup = () => {
|
|
622
|
+
clearTimeout(timer);
|
|
623
|
+
child.removeListener("message", onMessage);
|
|
624
|
+
child.removeListener("error", onError);
|
|
625
|
+
child.removeListener("exit", onExit);
|
|
626
|
+
};
|
|
627
|
+
const finish = (err) => {
|
|
628
|
+
cleanup();
|
|
629
|
+
if (err) {
|
|
630
|
+
if (child.connected)
|
|
631
|
+
child.disconnect();
|
|
632
|
+
child.kill();
|
|
633
|
+
child.unref();
|
|
634
|
+
reject(err);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
child.unref();
|
|
638
|
+
resolve();
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const onMessage = (message) => {
|
|
642
|
+
if (!message || typeof message !== "object")
|
|
643
|
+
return;
|
|
644
|
+
const startup = message;
|
|
645
|
+
if (startup.type === "orc-supervisor-ready")
|
|
646
|
+
finish();
|
|
647
|
+
if (startup.type === "orc-supervisor-error") {
|
|
648
|
+
finish(new Error(typeof startup.message === "string" ? startup.message : "detached supervisor failed to start"));
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const onError = (err) => finish(err);
|
|
652
|
+
const onExit = (code) => finish(new Error(`detached supervisor exited during startup (code ${code ?? "signal"})`));
|
|
653
|
+
child.on("message", onMessage);
|
|
654
|
+
child.once("error", onError);
|
|
655
|
+
child.once("exit", onExit);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
let monitorSingleton = null;
|
|
659
|
+
async function ensureMonitor(runId) {
|
|
660
|
+
const home = orcHome();
|
|
661
|
+
const firstPort = portForHome(home);
|
|
662
|
+
let base;
|
|
663
|
+
for (let offset = 0; offset <= 20; offset++) {
|
|
664
|
+
const candidate = `http://127.0.0.1:${firstPort + offset}`;
|
|
665
|
+
const health = await fetch(`${candidate}/health.json`, { signal: AbortSignal.timeout(500) })
|
|
666
|
+
.then(async (response) => response.ok ? await response.json() : undefined)
|
|
667
|
+
.catch(() => undefined);
|
|
668
|
+
if (health?.service === "orc-monitor" && health.home === home) {
|
|
669
|
+
base = candidate;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!base) {
|
|
674
|
+
monitorSingleton ??= new MonitorServer();
|
|
675
|
+
base = (await monitorSingleton.start()).url;
|
|
676
|
+
}
|
|
677
|
+
return runId ? `${base}/runs/${runId}` : base;
|
|
678
|
+
}
|