@karowanorg/orc-core 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/canonical.d.ts +11 -0
- package/dist/canonical.js +71 -0
- package/dist/compile.d.ts +12 -0
- package/dist/compile.js +27 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +42 -0
- package/dist/contracts.d.ts +405 -0
- package/dist/contracts.js +28 -0
- package/dist/cost.d.ts +34 -0
- package/dist/cost.js +51 -0
- package/dist/engine.d.ts +54 -0
- package/dist/engine.js +474 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/jsonschema.d.ts +3 -0
- package/dist/jsonschema.js +31 -0
- package/dist/rundir.d.ts +53 -0
- package/dist/rundir.js +299 -0
- package/dist/status.d.ts +10 -0
- package/dist/status.js +147 -0
- package/dist/supervisor.d.ts +34 -0
- package/dist/supervisor.js +1072 -0
- package/package.json +30 -0
package/dist/rundir.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { canonicalJson, sha256Hex } from "./canonical.js";
|
|
7
|
+
/** State home: ~/.orc or $ORC_HOME. One run = one directory under runs/. */
|
|
8
|
+
export function orcHome() {
|
|
9
|
+
return process.env.ORC_HOME ?? path.join(os.homedir(), ".orc");
|
|
10
|
+
}
|
|
11
|
+
export function runsDir() {
|
|
12
|
+
return path.join(orcHome(), "runs");
|
|
13
|
+
}
|
|
14
|
+
export function runDir(runId) {
|
|
15
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(runId))
|
|
16
|
+
throw new Error(`invalid run id: ${runId}`);
|
|
17
|
+
return path.join(runsDir(), runId);
|
|
18
|
+
}
|
|
19
|
+
export function newRunId(name) {
|
|
20
|
+
const slug = (name ?? "run").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "run";
|
|
21
|
+
const rand = randomBytes(8).toString("hex");
|
|
22
|
+
return `r_${slug}_${rand}`;
|
|
23
|
+
}
|
|
24
|
+
function writeAll(fd, data) {
|
|
25
|
+
let offset = 0;
|
|
26
|
+
while (offset < data.byteLength) {
|
|
27
|
+
const written = fs.writeSync(fd, data, offset, data.byteLength - offset);
|
|
28
|
+
if (written === 0)
|
|
29
|
+
throw new Error("failed to make progress writing record");
|
|
30
|
+
offset += written;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readFully(fd, data, length, position) {
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < length) {
|
|
36
|
+
const read = fs.readSync(fd, data, offset, length - offset, position + offset);
|
|
37
|
+
if (read === 0)
|
|
38
|
+
break;
|
|
39
|
+
offset += read;
|
|
40
|
+
}
|
|
41
|
+
return offset;
|
|
42
|
+
}
|
|
43
|
+
function truncateUnterminatedTail(filePath) {
|
|
44
|
+
if (!fs.existsSync(filePath))
|
|
45
|
+
return;
|
|
46
|
+
const fd = fs.openSync(filePath, "r+");
|
|
47
|
+
try {
|
|
48
|
+
const size = fs.fstatSync(fd).size;
|
|
49
|
+
if (size === 0)
|
|
50
|
+
return;
|
|
51
|
+
const last = Buffer.allocUnsafe(1);
|
|
52
|
+
if (readFully(fd, last, 1, size - 1) !== 1)
|
|
53
|
+
return;
|
|
54
|
+
if (last[0] === 0x0a)
|
|
55
|
+
return;
|
|
56
|
+
const chunk = Buffer.allocUnsafe(Math.min(size, 64 * 1024));
|
|
57
|
+
for (let end = size; end > 0;) {
|
|
58
|
+
const start = Math.max(0, end - chunk.byteLength);
|
|
59
|
+
const read = readFully(fd, chunk, end - start, start);
|
|
60
|
+
const newline = chunk.subarray(0, read).lastIndexOf(0x0a);
|
|
61
|
+
if (newline >= 0) {
|
|
62
|
+
fs.ftruncateSync(fd, start + newline + 1);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
end = start;
|
|
66
|
+
}
|
|
67
|
+
fs.ftruncateSync(fd, 0);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
fs.closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Append-only JSONL file with an explicit fsync mode.
|
|
75
|
+
* The journal uses fsync-per-append (the WAL); the trace sidecar fsyncs only
|
|
76
|
+
* on leaf close records.
|
|
77
|
+
*/
|
|
78
|
+
export class JsonlAppender {
|
|
79
|
+
filePath;
|
|
80
|
+
fd;
|
|
81
|
+
constructor(filePath) {
|
|
82
|
+
this.filePath = filePath;
|
|
83
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
+
// A record without its newline was never committed. Remove it before the
|
|
85
|
+
// next append so corruption cannot become an accepted interior line.
|
|
86
|
+
truncateUnterminatedTail(filePath);
|
|
87
|
+
this.fd = fs.openSync(filePath, "a");
|
|
88
|
+
}
|
|
89
|
+
append(record, opts) {
|
|
90
|
+
writeAll(this.fd, Buffer.from(JSON.stringify(record) + "\n"));
|
|
91
|
+
if (opts?.fsync !== false)
|
|
92
|
+
fs.fsyncSync(this.fd);
|
|
93
|
+
}
|
|
94
|
+
close() {
|
|
95
|
+
try {
|
|
96
|
+
fs.closeSync(this.fd);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* already closed */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Read a JSONL file tolerating a torn final line (crash-during-append). */
|
|
104
|
+
export function readJsonl(filePath) {
|
|
105
|
+
if (!fs.existsSync(filePath))
|
|
106
|
+
return [];
|
|
107
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
108
|
+
const out = [];
|
|
109
|
+
const lines = raw.split("\n");
|
|
110
|
+
// The final split element is either empty (committed newline) or the one
|
|
111
|
+
// uncommitted crash fragment readers are allowed to ignore.
|
|
112
|
+
lines.pop();
|
|
113
|
+
for (const [index, line] of lines.entries()) {
|
|
114
|
+
const trimmed = line.trim();
|
|
115
|
+
if (!trimmed)
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
out.push(JSON.parse(trimmed));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw new Error(`malformed JSONL record in ${filePath} at line ${index + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
export function runPaths(runId) {
|
|
127
|
+
const dir = runDir(runId);
|
|
128
|
+
return {
|
|
129
|
+
dir,
|
|
130
|
+
manifest: path.join(dir, "manifest.json"),
|
|
131
|
+
program: path.join(dir, "program.bundle.js"),
|
|
132
|
+
journal: path.join(dir, "journal.jsonl"),
|
|
133
|
+
traces: path.join(dir, "traces.jsonl"),
|
|
134
|
+
control: path.join(dir, "control.jsonl"),
|
|
135
|
+
results: path.join(dir, "results"),
|
|
136
|
+
report: path.join(dir, "report.html"),
|
|
137
|
+
lock: path.join(dir, "supervisor.lock"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function createRunDir(manifest, programBundle) {
|
|
141
|
+
const paths = runPaths(manifest.runId);
|
|
142
|
+
fs.mkdirSync(runsDir(), { recursive: true });
|
|
143
|
+
fs.mkdirSync(paths.dir);
|
|
144
|
+
fs.mkdirSync(paths.results);
|
|
145
|
+
fs.writeFileSync(paths.program, programBundle);
|
|
146
|
+
fs.writeFileSync(paths.manifest, JSON.stringify(manifest, null, 2));
|
|
147
|
+
return paths;
|
|
148
|
+
}
|
|
149
|
+
export function readManifest(runId) {
|
|
150
|
+
return JSON.parse(fs.readFileSync(runPaths(runId).manifest, "utf8"));
|
|
151
|
+
}
|
|
152
|
+
export function listRuns() {
|
|
153
|
+
const dir = runsDir();
|
|
154
|
+
if (!fs.existsSync(dir))
|
|
155
|
+
return [];
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
158
|
+
try {
|
|
159
|
+
out.push(readManifest(entry));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
/* skip malformed */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out.sort((a, b) => b.createdAtMs - a.createdAtMs);
|
|
166
|
+
}
|
|
167
|
+
/** Content-addressed result body storage. */
|
|
168
|
+
export function writeResult(paths, body) {
|
|
169
|
+
const canonical = canonicalJson(body);
|
|
170
|
+
const sha = sha256Hex(canonical);
|
|
171
|
+
const file = path.join(paths.results, `${sha}.json`);
|
|
172
|
+
if (!fs.existsSync(file)) {
|
|
173
|
+
const tmp = file + ".tmp";
|
|
174
|
+
fs.writeFileSync(tmp, canonical);
|
|
175
|
+
const fd = fs.openSync(tmp, "r+");
|
|
176
|
+
fs.fsyncSync(fd);
|
|
177
|
+
fs.closeSync(fd);
|
|
178
|
+
fs.renameSync(tmp, file);
|
|
179
|
+
}
|
|
180
|
+
return { sha, sizeBytes: Buffer.byteLength(canonical) };
|
|
181
|
+
}
|
|
182
|
+
export function readResult(paths, sha) {
|
|
183
|
+
const file = path.join(paths.results, `${sha}.json`);
|
|
184
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
185
|
+
if (sha256Hex(raw) !== sha)
|
|
186
|
+
throw new Error(`result body digest mismatch for ${sha}`);
|
|
187
|
+
return JSON.parse(raw);
|
|
188
|
+
}
|
|
189
|
+
export function readJournal(runId) {
|
|
190
|
+
return readJsonl(runPaths(runId).journal);
|
|
191
|
+
}
|
|
192
|
+
export function readTraces(runId) {
|
|
193
|
+
return readJsonl(runPaths(runId).traces);
|
|
194
|
+
}
|
|
195
|
+
export function readControl(runId) {
|
|
196
|
+
return readJsonl(runPaths(runId).control);
|
|
197
|
+
}
|
|
198
|
+
export function appendControl(runId, msg) {
|
|
199
|
+
const appender = new JsonlAppender(runPaths(runId).control);
|
|
200
|
+
appender.append(msg, { fsync: true });
|
|
201
|
+
appender.close();
|
|
202
|
+
}
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Supervisor lock: a platform advisory lock held by a tiny child process.
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
const LOCK_READY = "orc-lock-ready:";
|
|
207
|
+
const LOCK_HOLDER = `process.stdout.write(${JSON.stringify(LOCK_READY)}+process.pid+"\\n");process.stdin.resume()`;
|
|
208
|
+
export async function acquireLock(paths) {
|
|
209
|
+
fs.mkdirSync(path.dirname(paths.lock), { recursive: true });
|
|
210
|
+
const command = process.platform === "darwin"
|
|
211
|
+
? ["/usr/bin/lockf", "-k", "-s", "-t", "0", paths.lock]
|
|
212
|
+
: process.platform === "linux"
|
|
213
|
+
? ["flock", "-n", paths.lock]
|
|
214
|
+
: undefined;
|
|
215
|
+
if (!command) {
|
|
216
|
+
throw new Error(`supervisor locking is unsupported on ${process.platform}`);
|
|
217
|
+
}
|
|
218
|
+
command.push(process.execPath, "-e", LOCK_HOLDER);
|
|
219
|
+
const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
|
|
220
|
+
child.stdin.on("error", () => {
|
|
221
|
+
/* acquisition/exit paths report the child outcome */
|
|
222
|
+
});
|
|
223
|
+
let stdout = "";
|
|
224
|
+
let stderr = "";
|
|
225
|
+
child.stdout.setEncoding("utf8");
|
|
226
|
+
child.stderr.setEncoding("utf8");
|
|
227
|
+
child.stdout.on("data", (chunk) => {
|
|
228
|
+
stdout += chunk;
|
|
229
|
+
});
|
|
230
|
+
child.stderr.on("data", (chunk) => {
|
|
231
|
+
stderr += chunk;
|
|
232
|
+
});
|
|
233
|
+
const exited = new Promise((resolve) => {
|
|
234
|
+
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
235
|
+
});
|
|
236
|
+
const spawnFailed = new Promise((_, reject) => {
|
|
237
|
+
child.once("error", reject);
|
|
238
|
+
});
|
|
239
|
+
let holderPid;
|
|
240
|
+
let acquireTimer;
|
|
241
|
+
try {
|
|
242
|
+
await Promise.race([
|
|
243
|
+
new Promise((resolve) => {
|
|
244
|
+
const onData = () => {
|
|
245
|
+
const match = stdout.match(/orc-lock-ready:(\d+)\n/);
|
|
246
|
+
if (!match)
|
|
247
|
+
return;
|
|
248
|
+
holderPid = Number(match[1]);
|
|
249
|
+
child.stdout.removeListener("data", onData);
|
|
250
|
+
resolve();
|
|
251
|
+
};
|
|
252
|
+
child.stdout.on("data", onData);
|
|
253
|
+
}),
|
|
254
|
+
exited.then(({ code, signal }) => {
|
|
255
|
+
throw new Error(code === 75 || (process.platform === "linux" && code === 1)
|
|
256
|
+
? "run is owned by a live supervisor"
|
|
257
|
+
: `failed to acquire supervisor lock (${signal ?? code}): ${stderr.trim()}`);
|
|
258
|
+
}),
|
|
259
|
+
spawnFailed,
|
|
260
|
+
new Promise((_, reject) => {
|
|
261
|
+
acquireTimer = setTimeout(() => reject(new Error("timed out acquiring supervisor lock")), 5_000);
|
|
262
|
+
acquireTimer.unref();
|
|
263
|
+
}),
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
child.stdin.end();
|
|
268
|
+
child.kill();
|
|
269
|
+
await exited;
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
if (acquireTimer)
|
|
274
|
+
clearTimeout(acquireTimer);
|
|
275
|
+
}
|
|
276
|
+
let released = false;
|
|
277
|
+
const lost = exited.then(({ code, signal }) => {
|
|
278
|
+
if (!released) {
|
|
279
|
+
throw new Error(`supervisor lock holder exited unexpectedly (${signal ?? code})`);
|
|
280
|
+
}
|
|
281
|
+
return new Promise(() => undefined);
|
|
282
|
+
});
|
|
283
|
+
void lost.catch(() => undefined);
|
|
284
|
+
return {
|
|
285
|
+
holderPid: holderPid,
|
|
286
|
+
lost,
|
|
287
|
+
beat() {
|
|
288
|
+
if (child.exitCode !== null)
|
|
289
|
+
throw new Error("supervisor lock was lost");
|
|
290
|
+
},
|
|
291
|
+
async release() {
|
|
292
|
+
if (released)
|
|
293
|
+
return;
|
|
294
|
+
released = true;
|
|
295
|
+
child.stdin.end();
|
|
296
|
+
await exited;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ApprovalRequest, JournalRecord, LeafTraceRecord, RunManifest, RunStatus, TraceRecord } from "./contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure projection: journal (deterministic status) + trace sidecar (detail,
|
|
4
|
+
* running/failed leaves) -> RunStatus. Journal status wins where present.
|
|
5
|
+
*/
|
|
6
|
+
export declare function projectStatus(manifest: RunManifest, journal: JournalRecord[], traces: TraceRecord[]): RunStatus;
|
|
7
|
+
/** Supersession: highest attempt wins; within an attempt, highest rev wins. */
|
|
8
|
+
export declare function latestLeafTraces(traces: TraceRecord[]): Map<number, LeafTraceRecord>;
|
|
9
|
+
export declare function openApprovals(traces: TraceRecord[]): ApprovalRequest[];
|
|
10
|
+
export declare function statusForRun(runId: string): RunStatus;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readJournal, readManifest, readTraces } from "./rundir.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure projection: journal (deterministic status) + trace sidecar (detail,
|
|
4
|
+
* running/failed leaves) -> RunStatus. Journal status wins where present.
|
|
5
|
+
*/
|
|
6
|
+
export function projectStatus(manifest, journal, traces) {
|
|
7
|
+
const leaves = new Map();
|
|
8
|
+
const rearmed = new Set();
|
|
9
|
+
for (const rec of journal) {
|
|
10
|
+
if (rec.t === "call") {
|
|
11
|
+
leaves.set(rec.seq, {
|
|
12
|
+
seq: rec.seq,
|
|
13
|
+
id: rec.id,
|
|
14
|
+
phase: rec.phase,
|
|
15
|
+
kind: rec.kind,
|
|
16
|
+
readOnly: rec.readOnly,
|
|
17
|
+
status: "pending",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
else if (rec.t === "done") {
|
|
21
|
+
const leaf = leaves.get(rec.seq);
|
|
22
|
+
if (leaf) {
|
|
23
|
+
rearmed.delete(rec.seq);
|
|
24
|
+
leaf.status = rec.status;
|
|
25
|
+
leaf.error = rec.error;
|
|
26
|
+
leaf.resultSha = rec.resultSha;
|
|
27
|
+
leaf.attempt = rec.attempt;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (rec.t === "attempt") {
|
|
31
|
+
const leaf = leaves.get(rec.seq);
|
|
32
|
+
if (leaf) {
|
|
33
|
+
rearmed.delete(rec.seq);
|
|
34
|
+
leaf.status = "pending";
|
|
35
|
+
leaf.error = undefined;
|
|
36
|
+
leaf.resultSha = undefined;
|
|
37
|
+
leaf.endMs = undefined;
|
|
38
|
+
leaf.attempt = rec.attempt;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (rec.t === "retry") {
|
|
42
|
+
for (const seq of rec.seqs) {
|
|
43
|
+
const leaf = leaves.get(seq);
|
|
44
|
+
if (!leaf)
|
|
45
|
+
continue;
|
|
46
|
+
rearmed.add(seq);
|
|
47
|
+
leaf.status = "pending";
|
|
48
|
+
leaf.error = undefined;
|
|
49
|
+
leaf.resultSha = undefined;
|
|
50
|
+
leaf.endMs = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Overlay sidecar detail; sidecar supplies "running" and timing/harness info.
|
|
55
|
+
const bestTrace = latestLeafTraces(traces);
|
|
56
|
+
for (const [seq, tr] of bestTrace) {
|
|
57
|
+
const leaf = leaves.get(seq);
|
|
58
|
+
if (!leaf || rearmed.has(seq))
|
|
59
|
+
continue;
|
|
60
|
+
const currentAttempt = leaf.attempt ?? 0;
|
|
61
|
+
if (tr.attempt < currentAttempt)
|
|
62
|
+
continue;
|
|
63
|
+
leaf.harness = tr.harness;
|
|
64
|
+
leaf.host = tr.host;
|
|
65
|
+
leaf.startMs = tr.startMs;
|
|
66
|
+
leaf.endMs = tr.endMs;
|
|
67
|
+
if (tr.status === "running" && (tr.attempt > currentAttempt || leaf.status === "pending")) {
|
|
68
|
+
leaf.status = "running";
|
|
69
|
+
leaf.attempt = tr.attempt;
|
|
70
|
+
leaf.error = undefined;
|
|
71
|
+
leaf.resultSha = undefined;
|
|
72
|
+
}
|
|
73
|
+
if (leaf.status === "pending" && (tr.status === "ok" || tr.status === "error")) {
|
|
74
|
+
// journal completion lost/behind; sidecar close is still informative
|
|
75
|
+
leaf.status = tr.status;
|
|
76
|
+
leaf.error = tr.error;
|
|
77
|
+
leaf.attempt = tr.attempt;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Last finish wins; a retry record after it means a resume re-armed the run.
|
|
81
|
+
let finish;
|
|
82
|
+
let finishIdx = -1;
|
|
83
|
+
journal.forEach((r, i) => {
|
|
84
|
+
if (r.t === "finish") {
|
|
85
|
+
finish = r;
|
|
86
|
+
finishIdx = i;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const retriedAfterFinish = journal.some((r, i) => r.t === "retry" && i > finishIdx);
|
|
90
|
+
const state = !finish || retriedAfterFinish ? "running" : finish.status === "completed" ? "completed" : finish.status;
|
|
91
|
+
const all = [...leaves.values()].sort((a, b) => a.seq - b.seq);
|
|
92
|
+
const settled = state !== "running";
|
|
93
|
+
if (settled) {
|
|
94
|
+
for (const leaf of all) {
|
|
95
|
+
if (leaf.status === "running") {
|
|
96
|
+
leaf.status = "pending";
|
|
97
|
+
leaf.endMs = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const pendingApprovals = settled ? [] : openApprovals(traces);
|
|
102
|
+
const endMs = settled ? Math.max(...all.map((l) => l.endMs ?? 0), manifest.createdAtMs) : undefined;
|
|
103
|
+
return {
|
|
104
|
+
runId: manifest.runId,
|
|
105
|
+
state,
|
|
106
|
+
totalCalls: all.length,
|
|
107
|
+
ok: all.filter((l) => l.status === "ok").length,
|
|
108
|
+
failed: all.filter((l) => l.status === "error").length,
|
|
109
|
+
running: all.filter((l) => l.status === "running").length,
|
|
110
|
+
approvalsPending: pendingApprovals.length,
|
|
111
|
+
leaves: all,
|
|
112
|
+
resultSha: settled ? finish?.resultSha : undefined,
|
|
113
|
+
error: settled ? finish?.error : undefined,
|
|
114
|
+
startedAtMs: manifest.createdAtMs,
|
|
115
|
+
endedAtMs: endMs,
|
|
116
|
+
approvalMode: manifest.approvalMode,
|
|
117
|
+
allowWrites: manifest.allowWrites,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Supersession: highest attempt wins; within an attempt, highest rev wins. */
|
|
121
|
+
export function latestLeafTraces(traces) {
|
|
122
|
+
const best = new Map();
|
|
123
|
+
for (const rec of traces) {
|
|
124
|
+
if (rec.t !== "leaf")
|
|
125
|
+
continue;
|
|
126
|
+
const cur = best.get(rec.seq);
|
|
127
|
+
if (!cur || rec.attempt > cur.attempt || (rec.attempt === cur.attempt && rec.rev > cur.rev)) {
|
|
128
|
+
best.set(rec.seq, rec);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return best;
|
|
132
|
+
}
|
|
133
|
+
export function openApprovals(traces) {
|
|
134
|
+
const open = new Map();
|
|
135
|
+
for (const rec of traces) {
|
|
136
|
+
if (rec.t !== "event")
|
|
137
|
+
continue;
|
|
138
|
+
if (rec.event.kind === "approval-requested")
|
|
139
|
+
open.set(rec.event.approval.id, rec.event.approval);
|
|
140
|
+
if (rec.event.kind === "approval-resolved")
|
|
141
|
+
open.delete(rec.event.approvalId);
|
|
142
|
+
}
|
|
143
|
+
return [...open.values()];
|
|
144
|
+
}
|
|
145
|
+
export function statusForRun(runId) {
|
|
146
|
+
return projectStatus(readManifest(runId), readJournal(runId), readTraces(runId));
|
|
147
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Executor, type ExtensionLeaf, type Harness, type Policy, type RunManifest, type RunStatus } from "./contracts.js";
|
|
2
|
+
import { newRunId } from "./rundir.js";
|
|
3
|
+
export interface Registry {
|
|
4
|
+
harnesses: Map<string, Harness>;
|
|
5
|
+
extensions: Map<string, ExtensionLeaf>;
|
|
6
|
+
defaultHarness: string;
|
|
7
|
+
executorFor(host: string | undefined): Executor;
|
|
8
|
+
}
|
|
9
|
+
export interface LaunchOptions {
|
|
10
|
+
programPath: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
host?: string;
|
|
13
|
+
brief: string;
|
|
14
|
+
allowWrites?: boolean;
|
|
15
|
+
approvalMode?: RunManifest["approvalMode"];
|
|
16
|
+
sandbox?: boolean;
|
|
17
|
+
sandboxDirs?: string[];
|
|
18
|
+
maxParallel?: number;
|
|
19
|
+
idleTimeout?: number | false;
|
|
20
|
+
budgetUsd?: number;
|
|
21
|
+
name?: string;
|
|
22
|
+
defaultHarness?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SupervisorHooks {
|
|
25
|
+
/** Called (debounced by the caller if desired) after journal/trace appends. */
|
|
26
|
+
onUpdate?(runId: string): void;
|
|
27
|
+
}
|
|
28
|
+
export declare const ORC_VERSION = "0.1.0";
|
|
29
|
+
/** Static pre-scan: does the program source declare any write leaf? */
|
|
30
|
+
export declare function sourceRequestsWrite(source: string): boolean;
|
|
31
|
+
export declare function prepareRun(opts: LaunchOptions, registry: Registry): Promise<RunManifest>;
|
|
32
|
+
export declare function superviseRun(runId: string, registry: Registry, hooks?: SupervisorHooks, policy?: Policy): Promise<RunStatus>;
|
|
33
|
+
export declare function leafSystemPrompt(readOnly: boolean, cwd: string, brief: string): string;
|
|
34
|
+
export { newRunId };
|