@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/cost.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default list-price ESTIMATES for the gpt-5.x families, keyed by longest
|
|
3
|
+
* matching prefix. These are editable placeholders — real billing depends on
|
|
4
|
+
* your account/agreement. Override with config `costRates` or ORC_COST_RATES.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_COST_RATES = {
|
|
7
|
+
"gpt-5.6": { inputPer1M: 1.25, outputPer1M: 10, cachedInputPer1M: 0.125 },
|
|
8
|
+
"gpt-5.5": { inputPer1M: 1.25, outputPer1M: 10, cachedInputPer1M: 0.125 },
|
|
9
|
+
"gpt-5.3": { inputPer1M: 1.25, outputPer1M: 10, cachedInputPer1M: 0.125 },
|
|
10
|
+
"gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cachedInputPer1M: 0.125 },
|
|
11
|
+
};
|
|
12
|
+
export function loadCostRates(override) {
|
|
13
|
+
let fromEnv = {};
|
|
14
|
+
if (process.env.ORC_COST_RATES) {
|
|
15
|
+
try {
|
|
16
|
+
fromEnv = JSON.parse(process.env.ORC_COST_RATES);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* ignore malformed env */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { ...DEFAULT_COST_RATES, ...fromEnv, ...(override ?? {}) };
|
|
23
|
+
}
|
|
24
|
+
/** Longest-prefix match so "gpt-5.6-sol" resolves to the "gpt-5.6" rate. */
|
|
25
|
+
export function rateForModel(rates, model) {
|
|
26
|
+
if (!model)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (rates[model])
|
|
29
|
+
return rates[model];
|
|
30
|
+
let best;
|
|
31
|
+
let bestLen = -1;
|
|
32
|
+
for (const [key, rate] of Object.entries(rates)) {
|
|
33
|
+
if (model.startsWith(key) && key.length > bestLen) {
|
|
34
|
+
best = rate;
|
|
35
|
+
bestLen = key.length;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return best;
|
|
39
|
+
}
|
|
40
|
+
export function estimateCostUsd(rates, model, tokens) {
|
|
41
|
+
const rate = rateForModel(rates, model);
|
|
42
|
+
if (!rate)
|
|
43
|
+
return undefined;
|
|
44
|
+
const cached = tokens.cachedInputTokens ?? 0;
|
|
45
|
+
const freshInput = Math.max((tokens.inputTokens ?? 0) - cached, 0);
|
|
46
|
+
const cachedRate = rate.cachedInputPer1M ?? rate.inputPer1M;
|
|
47
|
+
const cost = (freshInput / 1_000_000) * rate.inputPer1M +
|
|
48
|
+
(cached / 1_000_000) * cachedRate +
|
|
49
|
+
((tokens.outputTokens ?? 0) / 1_000_000) * rate.outputPer1M;
|
|
50
|
+
return cost;
|
|
51
|
+
}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Json, type Policy, type ThunkSpec } from "./contracts.js";
|
|
2
|
+
export interface VmHooks {
|
|
3
|
+
/**
|
|
4
|
+
* Called synchronously when the program makes a call. MUST NOT re-enter the
|
|
5
|
+
* VM (no evalCode / executePendingJobs from inside). Record and return.
|
|
6
|
+
*/
|
|
7
|
+
onCall(seq: number, spec: ThunkSpec): void;
|
|
8
|
+
onLog(message: string): void;
|
|
9
|
+
onPhase(name: string): void;
|
|
10
|
+
}
|
|
11
|
+
export type ProgramState = {
|
|
12
|
+
state: "pending";
|
|
13
|
+
} | {
|
|
14
|
+
state: "ok";
|
|
15
|
+
result: Json;
|
|
16
|
+
} | {
|
|
17
|
+
state: "error";
|
|
18
|
+
error: string;
|
|
19
|
+
};
|
|
20
|
+
export declare class ProgramVM {
|
|
21
|
+
private readonly policy;
|
|
22
|
+
private readonly hooks;
|
|
23
|
+
private runtime;
|
|
24
|
+
private ctx;
|
|
25
|
+
private deferreds;
|
|
26
|
+
private nextSeq;
|
|
27
|
+
private stepsRemaining;
|
|
28
|
+
private interrupted;
|
|
29
|
+
private disposed;
|
|
30
|
+
private jsonParse?;
|
|
31
|
+
private jsonSerialize?;
|
|
32
|
+
private constructor();
|
|
33
|
+
static create(bundle: string, policy: Policy, hooks: VmHooks, startSeq?: number): Promise<ProgramVM>;
|
|
34
|
+
private installHostFunctions;
|
|
35
|
+
private evalOrThrow;
|
|
36
|
+
private resetBudget;
|
|
37
|
+
/** Drain the VM job queue to quiescence. One turn = one step budget. */
|
|
38
|
+
drain(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Deliver exactly one completion, then drain to quiescence.
|
|
41
|
+
* This one-per-drain policy is replay-visible and frozen (§5.3).
|
|
42
|
+
*/
|
|
43
|
+
deliver(seq: number, outcome: {
|
|
44
|
+
status: "ok";
|
|
45
|
+
value: Json;
|
|
46
|
+
} | {
|
|
47
|
+
status: "error";
|
|
48
|
+
error: string;
|
|
49
|
+
}): void;
|
|
50
|
+
/** Sequence numbers of calls made but not yet delivered. */
|
|
51
|
+
pendingSeqs(): number[];
|
|
52
|
+
state(): ProgramState;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deterministic program VM: quickjs-emscripten (sync build) with a
|
|
3
|
+
* host-controlled event loop.
|
|
4
|
+
*
|
|
5
|
+
* Determinism contract (design doc §5.3):
|
|
6
|
+
* - The bootstrap actively strips ambient authority (Date, Math.random,
|
|
7
|
+
* WeakRef, FinalizationRegistry) — a fresh QuickJS context ships all of them.
|
|
8
|
+
* - orc drives the event loop itself; jobs run only inside drain().
|
|
9
|
+
* - Sequence identity: each agent()/ext.* call gets the next sequence number in
|
|
10
|
+
* call order. Scheduling is replayed deterministically, so call order is
|
|
11
|
+
* stable across live execution and replay.
|
|
12
|
+
* - Delivery granularity is part of the replay contract: exactly ONE completion
|
|
13
|
+
* is resolved per quiescent drain.
|
|
14
|
+
* - The step budget is the interrupt handler's invocation count per turn —
|
|
15
|
+
* never wall-clock (which would abort replay at a different point than live).
|
|
16
|
+
* - Values cross the boundary as VM-parsed canonical JSON literals; the journal
|
|
17
|
+
* digest is computed host-side over the exact canonical bytes.
|
|
18
|
+
*/
|
|
19
|
+
import { getQuickJS, } from "quickjs-emscripten";
|
|
20
|
+
import { canonicalJson } from "./canonical.js";
|
|
21
|
+
import { PolicyError } from "./contracts.js";
|
|
22
|
+
const BOOTSTRAP = String.raw `
|
|
23
|
+
"use strict";
|
|
24
|
+
(function () {
|
|
25
|
+
var deny = function (name) {
|
|
26
|
+
var f = function () { throw new Error(name + " is not available in orc programs (deterministic sandbox)"); };
|
|
27
|
+
return f;
|
|
28
|
+
};
|
|
29
|
+
var deniedDate = deny("Date");
|
|
30
|
+
deniedDate.now = deny("Date.now");
|
|
31
|
+
deniedDate.parse = deny("Date.parse");
|
|
32
|
+
deniedDate.UTC = deny("Date.UTC");
|
|
33
|
+
globalThis.Date = deniedDate;
|
|
34
|
+
Math.random = deny("Math.random");
|
|
35
|
+
globalThis.WeakRef = undefined;
|
|
36
|
+
globalThis.FinalizationRegistry = undefined;
|
|
37
|
+
|
|
38
|
+
var groupCounter = 0;
|
|
39
|
+
var currentPhase = "";
|
|
40
|
+
|
|
41
|
+
function dispatch(spec) {
|
|
42
|
+
if (spec.phase === undefined && currentPhase) spec.phase = currentPhase;
|
|
43
|
+
return __orc_dispatch(JSON.stringify(spec));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normTimeout(v) {
|
|
47
|
+
if (v === false) return false;
|
|
48
|
+
if (typeof v === "number") return v;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Dispatch a leaf and, when its completion resolves, restore the phase that
|
|
53
|
+
// was active at CALL time. Correct under concurrency because orc delivers
|
|
54
|
+
// exactly one completion per quiescent drain: the restore microtask runs
|
|
55
|
+
// (and any synchronous agent() calls in the await continuation capture the
|
|
56
|
+
// right phase) before the next completion is ever delivered.
|
|
57
|
+
function dispatchLeaf(spec) {
|
|
58
|
+
var capturedPhase = currentPhase;
|
|
59
|
+
return dispatch(spec).then(
|
|
60
|
+
function (v) { currentPhase = capturedPhase; return v; },
|
|
61
|
+
function (e) { currentPhase = capturedPhase; throw e; }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
globalThis.agent = function (prompt, opts) {
|
|
66
|
+
var o = opts || {};
|
|
67
|
+
return dispatchLeaf({
|
|
68
|
+
kind: "agent",
|
|
69
|
+
prompt: String(prompt),
|
|
70
|
+
id: o.id,
|
|
71
|
+
harness: o.harness,
|
|
72
|
+
model: o.model,
|
|
73
|
+
reasoningEffort: o.reasoningEffort,
|
|
74
|
+
schema: o.schema,
|
|
75
|
+
readOnly: o.readOnly === false ? false : true,
|
|
76
|
+
cwd: o.cwd,
|
|
77
|
+
host: o.host,
|
|
78
|
+
phase: o.phase,
|
|
79
|
+
idleTimeoutMs: normTimeout(o.idleTimeout),
|
|
80
|
+
groupId: o.__groupId,
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Independent fan-out: every lane runs to completion and comes back as a
|
|
85
|
+
// settled outcome ({status:"ok",value}|{status:"error",error}), so one lane's
|
|
86
|
+
// failure never cancels or wastes its siblings. The caller decides how to
|
|
87
|
+
// handle partial failure. (For fail-fast, use Promise.all over agent().)
|
|
88
|
+
globalThis.parallel = function (specs) {
|
|
89
|
+
var gid = "g" + ++groupCounter;
|
|
90
|
+
return Promise.all(
|
|
91
|
+
specs.map(function (s) {
|
|
92
|
+
var o = Object.assign({}, s);
|
|
93
|
+
var prompt = o.prompt;
|
|
94
|
+
delete o.prompt;
|
|
95
|
+
o.__groupId = gid; // lane-grouping metadata for the monitor
|
|
96
|
+
return globalThis.settle(globalThis.agent(prompt, o));
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
globalThis.settle = function (p) {
|
|
102
|
+
return Promise.resolve(p).then(
|
|
103
|
+
function (v) { return { status: "ok", value: v === undefined ? null : v }; },
|
|
104
|
+
function (e) { return { status: "error", error: String((e && e.message) || e) }; }
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Lexically scoped ONLY (the rejected persistent-global marker is not offered):
|
|
109
|
+
// phase(name, fn) labels every call made inside fn, and — thanks to the
|
|
110
|
+
// call-time capture + one-per-drain restore above — that holds across awaits
|
|
111
|
+
// and concurrent sibling phases without mislabeling.
|
|
112
|
+
globalThis.phase = function (name, fn) {
|
|
113
|
+
var label = String(name);
|
|
114
|
+
if (typeof fn !== "function") {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'phase(name, fn) requires a function, e.g. phase("' + label + '", () => {...}). ' +
|
|
117
|
+
'For a single call, use the per-call { phase: "' + label + '" } option instead.'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
__orc_phase(label);
|
|
121
|
+
var prev = currentPhase;
|
|
122
|
+
currentPhase = label;
|
|
123
|
+
try {
|
|
124
|
+
var r = fn();
|
|
125
|
+
if (r && typeof r.then === "function") {
|
|
126
|
+
return r.then(
|
|
127
|
+
function (v) { currentPhase = prev; return v; },
|
|
128
|
+
function (e) { currentPhase = prev; throw e; }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
currentPhase = prev;
|
|
132
|
+
return r;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
currentPhase = prev;
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
globalThis.log = function (msg) { __orc_log(String(msg)); };
|
|
140
|
+
|
|
141
|
+
globalThis.ext = new Proxy({}, {
|
|
142
|
+
get: function (_t, name) {
|
|
143
|
+
return function (payload) {
|
|
144
|
+
return dispatchLeaf({
|
|
145
|
+
kind: "ext:" + String(name),
|
|
146
|
+
payload: payload === undefined ? null : payload,
|
|
147
|
+
readOnly: true, // host overrides from the extension registration
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
globalThis.__orc_api = {
|
|
154
|
+
agent: globalThis.agent,
|
|
155
|
+
parallel: globalThis.parallel,
|
|
156
|
+
phase: globalThis.phase,
|
|
157
|
+
log: globalThis.log,
|
|
158
|
+
settle: globalThis.settle,
|
|
159
|
+
ext: globalThis.ext,
|
|
160
|
+
};
|
|
161
|
+
})();
|
|
162
|
+
`;
|
|
163
|
+
const INVOKE = String.raw `
|
|
164
|
+
"use strict";
|
|
165
|
+
(function () {
|
|
166
|
+
var mod = typeof __orc_mod !== "undefined" ? __orc_mod : undefined;
|
|
167
|
+
var fn = mod && (typeof mod.default === "function" ? mod.default : typeof mod === "function" ? mod : null);
|
|
168
|
+
if (!fn) throw new Error("program has no default-exported function");
|
|
169
|
+
globalThis.__orc_state = "pending";
|
|
170
|
+
globalThis.__orc_result = null;
|
|
171
|
+
Promise.resolve(fn(globalThis.__orc_api)).then(
|
|
172
|
+
function (v) { globalThis.__orc_state = "ok"; globalThis.__orc_result = v === undefined ? null : v; },
|
|
173
|
+
function (e) {
|
|
174
|
+
globalThis.__orc_state = "error";
|
|
175
|
+
// QuickJS Error.stack does NOT include the message line (unlike V8).
|
|
176
|
+
var msg = e && e.message ? String(e.message) : String(e);
|
|
177
|
+
var stack = e && e.stack ? String(e.stack) : "";
|
|
178
|
+
globalThis.__orc_result = stack ? msg + "\n" + stack : msg;
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
})();
|
|
182
|
+
`;
|
|
183
|
+
const STRICT_JSON_SERIALIZER = String.raw `
|
|
184
|
+
(function () {
|
|
185
|
+
var isArray = Array.isArray;
|
|
186
|
+
var getPrototypeOf = Object.getPrototypeOf;
|
|
187
|
+
var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
188
|
+
var keys = Object.keys;
|
|
189
|
+
var objectPrototype = Object.prototype;
|
|
190
|
+
var stringify = JSON.stringify;
|
|
191
|
+
var isFiniteNumber = Number.isFinite;
|
|
192
|
+
|
|
193
|
+
function encode(value, depth, ancestors) {
|
|
194
|
+
if (depth > 256) throw new TypeError("JSON nesting exceeds 256");
|
|
195
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
196
|
+
return stringify(value);
|
|
197
|
+
}
|
|
198
|
+
if (typeof value === "number") {
|
|
199
|
+
if (!isFiniteNumber(value)) throw new TypeError("JSON numbers must be finite");
|
|
200
|
+
return stringify(value);
|
|
201
|
+
}
|
|
202
|
+
if (typeof value !== "object") {
|
|
203
|
+
throw new TypeError("unsupported JSON value: " + typeof value);
|
|
204
|
+
}
|
|
205
|
+
for (var a = 0; a < ancestors.length; a++) {
|
|
206
|
+
if (ancestors[a] === value) throw new TypeError("JSON value contains a cycle");
|
|
207
|
+
}
|
|
208
|
+
ancestors[ancestors.length] = value;
|
|
209
|
+
try {
|
|
210
|
+
var out = "";
|
|
211
|
+
var i;
|
|
212
|
+
if (isArray(value)) {
|
|
213
|
+
out = "[";
|
|
214
|
+
for (i = 0; i < value.length; i++) {
|
|
215
|
+
var item = getOwnPropertyDescriptor(value, i);
|
|
216
|
+
if (!item) throw new TypeError("JSON arrays cannot be sparse");
|
|
217
|
+
if (!("value" in item)) throw new TypeError("JSON properties cannot be accessors");
|
|
218
|
+
if (i) out += ",";
|
|
219
|
+
out += encode(item.value, depth + 1, ancestors);
|
|
220
|
+
}
|
|
221
|
+
return out + "]";
|
|
222
|
+
}
|
|
223
|
+
var prototype = getPrototypeOf(value);
|
|
224
|
+
if (prototype !== objectPrototype && prototype !== null) {
|
|
225
|
+
throw new TypeError("JSON objects must be plain objects");
|
|
226
|
+
}
|
|
227
|
+
var names = keys(value);
|
|
228
|
+
out = "{";
|
|
229
|
+
for (i = 0; i < names.length; i++) {
|
|
230
|
+
var name = names[i];
|
|
231
|
+
var property = getOwnPropertyDescriptor(value, name);
|
|
232
|
+
if (!("value" in property)) throw new TypeError("JSON properties cannot be accessors");
|
|
233
|
+
if (i) out += ",";
|
|
234
|
+
out += stringify(name) + ":" + encode(property.value, depth + 1, ancestors);
|
|
235
|
+
}
|
|
236
|
+
return out + "}";
|
|
237
|
+
} finally {
|
|
238
|
+
ancestors.length -= 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return function (value) { return encode(value, 0, []); };
|
|
243
|
+
})()
|
|
244
|
+
`;
|
|
245
|
+
export class ProgramVM {
|
|
246
|
+
policy;
|
|
247
|
+
hooks;
|
|
248
|
+
runtime;
|
|
249
|
+
ctx;
|
|
250
|
+
deferreds = new Map();
|
|
251
|
+
nextSeq = 0;
|
|
252
|
+
stepsRemaining = 0;
|
|
253
|
+
interrupted = false;
|
|
254
|
+
disposed = false;
|
|
255
|
+
jsonParse;
|
|
256
|
+
jsonSerialize;
|
|
257
|
+
constructor(runtime, ctx, policy, hooks) {
|
|
258
|
+
this.policy = policy;
|
|
259
|
+
this.hooks = hooks;
|
|
260
|
+
this.runtime = runtime;
|
|
261
|
+
this.ctx = ctx;
|
|
262
|
+
}
|
|
263
|
+
static async create(bundle, policy, hooks, startSeq = 0) {
|
|
264
|
+
const QuickJS = await getQuickJS();
|
|
265
|
+
const runtime = QuickJS.newRuntime();
|
|
266
|
+
let vm;
|
|
267
|
+
try {
|
|
268
|
+
runtime.setMemoryLimit(policy.memoryLimitBytes);
|
|
269
|
+
runtime.setMaxStackSize(policy.maxStackBytes);
|
|
270
|
+
const ctx = runtime.newContext();
|
|
271
|
+
vm = new ProgramVM(runtime, ctx, policy, hooks);
|
|
272
|
+
vm.nextSeq = startSeq;
|
|
273
|
+
const json = ctx.getProp(ctx.global, "JSON");
|
|
274
|
+
try {
|
|
275
|
+
// Keep the intrinsic handle outside the program's reach. Sandbox code
|
|
276
|
+
// may replace globalThis.JSON.parse, but replay materialization must
|
|
277
|
+
// still use the original parser.
|
|
278
|
+
vm.jsonParse = ctx.getProp(json, "parse");
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
json.dispose();
|
|
282
|
+
}
|
|
283
|
+
const serializer = ctx.evalCode(STRICT_JSON_SERIALIZER, "orc-json.js");
|
|
284
|
+
if ("error" in serializer && serializer.error) {
|
|
285
|
+
const err = ctx.dump(serializer.error);
|
|
286
|
+
serializer.error.dispose();
|
|
287
|
+
throw new Error(`failed to install JSON serializer: ${String(err)}`);
|
|
288
|
+
}
|
|
289
|
+
vm.jsonSerialize = serializer.value;
|
|
290
|
+
runtime.setInterruptHandler(() => {
|
|
291
|
+
vm.stepsRemaining -= 1;
|
|
292
|
+
if (vm.stepsRemaining < 0) {
|
|
293
|
+
vm.interrupted = true;
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
});
|
|
298
|
+
vm.installHostFunctions();
|
|
299
|
+
vm.evalOrThrow(BOOTSTRAP, "orc-bootstrap.js");
|
|
300
|
+
vm.evalOrThrow(bundle, "program.bundle.js");
|
|
301
|
+
vm.evalOrThrow(INVOKE, "orc-invoke.js");
|
|
302
|
+
vm.drain(); // initial turn: run the program to its first quiescent point
|
|
303
|
+
return vm;
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
if (vm)
|
|
307
|
+
vm.dispose();
|
|
308
|
+
else
|
|
309
|
+
runtime.dispose();
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
installHostFunctions() {
|
|
314
|
+
const { ctx } = this;
|
|
315
|
+
const dispatchFn = ctx.newFunction("__orc_dispatch", (specHandle) => {
|
|
316
|
+
const specJson = ctx.getString(specHandle);
|
|
317
|
+
const spec = JSON.parse(specJson);
|
|
318
|
+
const seq = this.nextSeq++;
|
|
319
|
+
// Record only — never re-enter the VM from a host callback.
|
|
320
|
+
this.hooks.onCall(seq, spec);
|
|
321
|
+
const deferred = ctx.newPromise();
|
|
322
|
+
this.deferreds.set(seq, deferred);
|
|
323
|
+
// Returned to the program as its awaited promise; the handle is owned by
|
|
324
|
+
// the deferred and freed via deferred.dispose() (see deliver/dispose).
|
|
325
|
+
return deferred.handle;
|
|
326
|
+
});
|
|
327
|
+
ctx.setProp(ctx.global, "__orc_dispatch", dispatchFn);
|
|
328
|
+
dispatchFn.dispose();
|
|
329
|
+
const logFn = ctx.newFunction("__orc_log", (msgHandle) => {
|
|
330
|
+
this.hooks.onLog(ctx.getString(msgHandle));
|
|
331
|
+
});
|
|
332
|
+
ctx.setProp(ctx.global, "__orc_log", logFn);
|
|
333
|
+
logFn.dispose();
|
|
334
|
+
const phaseFn = ctx.newFunction("__orc_phase", (nameHandle) => {
|
|
335
|
+
this.hooks.onPhase(ctx.getString(nameHandle));
|
|
336
|
+
});
|
|
337
|
+
ctx.setProp(ctx.global, "__orc_phase", phaseFn);
|
|
338
|
+
phaseFn.dispose();
|
|
339
|
+
}
|
|
340
|
+
evalOrThrow(code, filename) {
|
|
341
|
+
this.resetBudget();
|
|
342
|
+
const result = this.ctx.evalCode(code, filename);
|
|
343
|
+
if ("error" in result && result.error) {
|
|
344
|
+
const err = this.ctx.dump(result.error);
|
|
345
|
+
result.error.dispose();
|
|
346
|
+
throw new Error(`program error in ${filename}: ${typeof err === "object" ? JSON.stringify(err) : String(err)}`);
|
|
347
|
+
}
|
|
348
|
+
result.value.dispose();
|
|
349
|
+
if (this.interrupted)
|
|
350
|
+
throw new PolicyError(`step budget exceeded in ${filename}`);
|
|
351
|
+
}
|
|
352
|
+
resetBudget() {
|
|
353
|
+
this.stepsRemaining = this.policy.stepBudgetPerTurn;
|
|
354
|
+
this.interrupted = false;
|
|
355
|
+
}
|
|
356
|
+
/** Drain the VM job queue to quiescence. One turn = one step budget. */
|
|
357
|
+
drain() {
|
|
358
|
+
this.resetBudget();
|
|
359
|
+
// executePendingJobs runs queued promise jobs; loop defensively.
|
|
360
|
+
for (;;) {
|
|
361
|
+
const res = this.runtime.executePendingJobs();
|
|
362
|
+
if ("error" in res && res.error) {
|
|
363
|
+
const err = this.ctx.dump(res.error);
|
|
364
|
+
res.error.dispose();
|
|
365
|
+
throw new Error(`program job error: ${typeof err === "object" ? JSON.stringify(err) : String(err)}`);
|
|
366
|
+
}
|
|
367
|
+
const executed = "value" in res ? res.value : 0;
|
|
368
|
+
if (this.interrupted)
|
|
369
|
+
throw new PolicyError("step budget exceeded");
|
|
370
|
+
if (executed === 0)
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Deliver exactly one completion, then drain to quiescence.
|
|
376
|
+
* This one-per-drain policy is replay-visible and frozen (§5.3).
|
|
377
|
+
*/
|
|
378
|
+
deliver(seq, outcome) {
|
|
379
|
+
const deferred = this.deferreds.get(seq);
|
|
380
|
+
if (!deferred)
|
|
381
|
+
throw new Error(`no pending call with seq ${seq}`);
|
|
382
|
+
this.deferreds.delete(seq);
|
|
383
|
+
this.resetBudget();
|
|
384
|
+
if (outcome.status === "ok") {
|
|
385
|
+
const bytes = canonicalJson(outcome.value);
|
|
386
|
+
const source = this.ctx.newString(bytes);
|
|
387
|
+
const parsed = this.ctx.callFunction(this.jsonParse, this.ctx.undefined, source);
|
|
388
|
+
source.dispose();
|
|
389
|
+
if ("error" in parsed && parsed.error) {
|
|
390
|
+
const err = this.ctx.dump(parsed.error);
|
|
391
|
+
parsed.error.dispose();
|
|
392
|
+
deferred.dispose();
|
|
393
|
+
if (this.interrupted) {
|
|
394
|
+
throw new PolicyError(`step budget exceeded materializing result for call ${seq}`);
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`failed to materialize result for seq ${seq}: ${JSON.stringify(err)}`);
|
|
397
|
+
}
|
|
398
|
+
deferred.resolve(parsed.value);
|
|
399
|
+
parsed.value.dispose();
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
const error = this.ctx.newError(outcome.error);
|
|
403
|
+
deferred.reject(error);
|
|
404
|
+
error.dispose();
|
|
405
|
+
}
|
|
406
|
+
deferred.dispose(); // frees the deferred's resolve/reject/handle
|
|
407
|
+
if (this.interrupted)
|
|
408
|
+
throw new PolicyError(`step budget exceeded materializing result for call ${seq}`);
|
|
409
|
+
this.drain();
|
|
410
|
+
}
|
|
411
|
+
/** Sequence numbers of calls made but not yet delivered. */
|
|
412
|
+
pendingSeqs() {
|
|
413
|
+
return [...this.deferreds.keys()].sort((a, b) => a - b);
|
|
414
|
+
}
|
|
415
|
+
state() {
|
|
416
|
+
const stateHandle = this.ctx.getProp(this.ctx.global, "__orc_state");
|
|
417
|
+
const state = this.ctx.dump(stateHandle);
|
|
418
|
+
stateHandle.dispose();
|
|
419
|
+
if (state === "pending")
|
|
420
|
+
return { state: "pending" };
|
|
421
|
+
const handle = this.ctx.getProp(this.ctx.global, "__orc_result");
|
|
422
|
+
if (state === "ok") {
|
|
423
|
+
this.resetBudget();
|
|
424
|
+
const serialized = this.ctx.callFunction(this.jsonSerialize, this.ctx.undefined, handle);
|
|
425
|
+
handle.dispose();
|
|
426
|
+
if ("error" in serialized && serialized.error) {
|
|
427
|
+
const err = this.ctx.dump(serialized.error);
|
|
428
|
+
serialized.error.dispose();
|
|
429
|
+
return {
|
|
430
|
+
state: "error",
|
|
431
|
+
error: `program result is not valid JSON: ${String(err?.message ?? err)}`,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const json = this.ctx.getString(serialized.value);
|
|
435
|
+
serialized.value.dispose();
|
|
436
|
+
if (this.interrupted)
|
|
437
|
+
return { state: "error", error: "step budget exceeded materializing program result" };
|
|
438
|
+
return { state: "ok", result: JSON.parse(json) };
|
|
439
|
+
}
|
|
440
|
+
const value = this.ctx.dump(handle);
|
|
441
|
+
handle.dispose();
|
|
442
|
+
return { state: "error", error: String(value) };
|
|
443
|
+
}
|
|
444
|
+
dispose() {
|
|
445
|
+
if (this.disposed)
|
|
446
|
+
return;
|
|
447
|
+
this.disposed = true;
|
|
448
|
+
// Undelivered deferreds (e.g. validate previews, deadlocks): free them so
|
|
449
|
+
// the runtime disposes cleanly (avoids the QuickJS gc_obj_list assertion).
|
|
450
|
+
for (const d of this.deferreds.values()) {
|
|
451
|
+
try {
|
|
452
|
+
d.dispose();
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
/* already consumed */
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
this.deferreds.clear();
|
|
459
|
+
this.jsonParse?.dispose();
|
|
460
|
+
this.jsonSerialize?.dispose();
|
|
461
|
+
try {
|
|
462
|
+
this.ctx.dispose();
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
/* leaked handles on release builds are silent; best effort */
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
this.runtime.dispose();
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
/* ditto */
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./contracts.js";
|
|
2
|
+
export * from "./canonical.js";
|
|
3
|
+
export * from "./rundir.js";
|
|
4
|
+
export * from "./status.js";
|
|
5
|
+
export * from "./compile.js";
|
|
6
|
+
export * from "./engine.js";
|
|
7
|
+
export * from "./supervisor.js";
|
|
8
|
+
export * from "./config.js";
|
|
9
|
+
export * from "./cost.js";
|
|
10
|
+
export * from "./jsonschema.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./contracts.js";
|
|
2
|
+
export * from "./canonical.js";
|
|
3
|
+
export * from "./rundir.js";
|
|
4
|
+
export * from "./status.js";
|
|
5
|
+
export * from "./compile.js";
|
|
6
|
+
export * from "./engine.js";
|
|
7
|
+
export * from "./supervisor.js";
|
|
8
|
+
export * from "./config.js";
|
|
9
|
+
export * from "./cost.js";
|
|
10
|
+
export * from "./jsonschema.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Ajv } from "ajv";
|
|
2
|
+
import { Ajv2019 } from "ajv/dist/2019.js";
|
|
3
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
4
|
+
const options = {
|
|
5
|
+
allErrors: false,
|
|
6
|
+
strict: false,
|
|
7
|
+
validateFormats: false,
|
|
8
|
+
};
|
|
9
|
+
/** Validate a JSON value against the caller's JSON Schema. */
|
|
10
|
+
export function validateAgainstSchema(value, schema, path = "$") {
|
|
11
|
+
try {
|
|
12
|
+
const dialect = schema !== null && typeof schema === "object" && !Array.isArray(schema)
|
|
13
|
+
? schema.$schema
|
|
14
|
+
: undefined;
|
|
15
|
+
// A schema belongs to one call. A fresh validator keeps caller-owned $ids
|
|
16
|
+
// and generated validators from accumulating across runs.
|
|
17
|
+
const ajv = typeof dialect === "string" && dialect.includes("2020-12")
|
|
18
|
+
? new Ajv2020(options)
|
|
19
|
+
: typeof dialect === "string" && dialect.includes("2019-09")
|
|
20
|
+
? new Ajv2019(options)
|
|
21
|
+
: new Ajv(options);
|
|
22
|
+
const validate = ajv.compile(schema);
|
|
23
|
+
if (validate(value))
|
|
24
|
+
return null;
|
|
25
|
+
const error = validate.errors?.[0];
|
|
26
|
+
return `${path}${error?.instancePath ?? ""}: ${error?.message ?? "schema validation failed"}`;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return `${path}: invalid schema: ${err instanceof Error ? err.message : String(err)}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/rundir.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Json, JournalRecord, RunManifest, TraceRecord, ControlMessage } from "./contracts.js";
|
|
2
|
+
/** State home: ~/.orc or $ORC_HOME. One run = one directory under runs/. */
|
|
3
|
+
export declare function orcHome(): string;
|
|
4
|
+
export declare function runsDir(): string;
|
|
5
|
+
export declare function runDir(runId: string): string;
|
|
6
|
+
export declare function newRunId(name?: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Append-only JSONL file with an explicit fsync mode.
|
|
9
|
+
* The journal uses fsync-per-append (the WAL); the trace sidecar fsyncs only
|
|
10
|
+
* on leaf close records.
|
|
11
|
+
*/
|
|
12
|
+
export declare class JsonlAppender<T> {
|
|
13
|
+
readonly filePath: string;
|
|
14
|
+
private fd;
|
|
15
|
+
constructor(filePath: string);
|
|
16
|
+
append(record: T, opts?: {
|
|
17
|
+
fsync?: boolean;
|
|
18
|
+
}): void;
|
|
19
|
+
close(): void;
|
|
20
|
+
}
|
|
21
|
+
/** Read a JSONL file tolerating a torn final line (crash-during-append). */
|
|
22
|
+
export declare function readJsonl<T>(filePath: string): T[];
|
|
23
|
+
export interface RunPaths {
|
|
24
|
+
dir: string;
|
|
25
|
+
manifest: string;
|
|
26
|
+
program: string;
|
|
27
|
+
journal: string;
|
|
28
|
+
traces: string;
|
|
29
|
+
control: string;
|
|
30
|
+
results: string;
|
|
31
|
+
report: string;
|
|
32
|
+
lock: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function runPaths(runId: string): RunPaths;
|
|
35
|
+
export declare function createRunDir(manifest: RunManifest, programBundle: string): RunPaths;
|
|
36
|
+
export declare function readManifest(runId: string): RunManifest;
|
|
37
|
+
export declare function listRuns(): RunManifest[];
|
|
38
|
+
/** Content-addressed result body storage. */
|
|
39
|
+
export declare function writeResult(paths: RunPaths, body: Json): {
|
|
40
|
+
sha: string;
|
|
41
|
+
sizeBytes: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function readResult(paths: RunPaths, sha: string): Json;
|
|
44
|
+
export declare function readJournal(runId: string): JournalRecord[];
|
|
45
|
+
export declare function readTraces(runId: string): TraceRecord[];
|
|
46
|
+
export declare function readControl(runId: string): ControlMessage[];
|
|
47
|
+
export declare function appendControl(runId: string, msg: ControlMessage): void;
|
|
48
|
+
export declare function acquireLock(paths: RunPaths): Promise<{
|
|
49
|
+
release(): Promise<void>;
|
|
50
|
+
beat(): void;
|
|
51
|
+
lost: Promise<never>;
|
|
52
|
+
holderPid: number;
|
|
53
|
+
}>;
|