melchizedek-agents 1.6.0 → 1.7.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/DOCUMENTATION.md +5 -1
  3. package/config/agents/syndicate.schema.json +63 -0
  4. package/config/agents/syndicateSchema.yaml +50 -0
  5. package/dist/lib/a2a/executor.js +21 -0
  6. package/dist/lib/compile.d.ts +4 -3
  7. package/dist/lib/compile.js +8 -3
  8. package/dist/lib/index.d.ts +8 -0
  9. package/dist/lib/index.js +5 -0
  10. package/dist/lib/loadSyndicate.d.ts +13 -0
  11. package/dist/lib/loadSyndicate.js +11 -1
  12. package/dist/lib/observability/supabaseSpanExporter.js +2 -1
  13. package/dist/lib/observability/tracer.d.ts +2 -2
  14. package/dist/lib/observability/tracer.js +2 -2
  15. package/dist/lib/runtime/native/checkpoint.d.ts +2 -1
  16. package/dist/lib/runtime/native/checkpoint.js +6 -2
  17. package/dist/lib/runtime/native/interrupts.js +9 -2
  18. package/dist/lib/runtime/nativeTurn.d.ts +2 -1
  19. package/dist/lib/runtime/nativeTurn.js +5 -1
  20. package/dist/lib/runtime/syndicateTurn.d.ts +11 -1
  21. package/dist/lib/runtime/syndicateTurn.js +383 -5
  22. package/dist/lib/sandbox/sandbox.d.ts +121 -0
  23. package/dist/lib/sandbox/sandbox.js +276 -0
  24. package/dist/lib/sandbox/worker.d.ts +29 -0
  25. package/dist/lib/sandbox/worker.js +280 -0
  26. package/dist/lib/script/config.d.ts +35 -0
  27. package/dist/lib/script/config.js +17 -0
  28. package/dist/lib/script/journal.d.ts +137 -0
  29. package/dist/lib/script/journal.js +193 -0
  30. package/dist/lib/script/run.d.ts +138 -0
  31. package/dist/lib/script/run.js +364 -0
  32. package/dist/lib/syndicateSchema.d.ts +20 -0
  33. package/dist/lib/syndicateSchema.js +98 -6
  34. package/dist/lib/toolRegistry.js +4 -0
  35. package/dist/lib/tools/runCodeTool.d.ts +50 -0
  36. package/dist/lib/tools/runCodeTool.js +59 -0
  37. package/package.json +5 -1
@@ -0,0 +1,276 @@
1
+ /**
2
+ * lib/sandbox/sandbox.ts — run JavaScript the engine did not write, where
3
+ * it can reach nothing but what it is handed (ADR 0137 Part A).
4
+ *
5
+ * WHY this exists:
6
+ * Two features need code to run on this host: the `run_code` tool, whose
7
+ * code a model writes (lib/tools/runCodeTool.ts), and a syndicate's
8
+ * `script:`, whose code an operator deploys (lib/script/). ADR 0033 refused
9
+ * local execution because ADK's executors ran code on the host or needed a
10
+ * Docker daemon. This is neither: QuickJS compiled to wasm, with no host
11
+ * reach of any kind, on a worker thread (lib/sandbox/worker.ts) so a run's
12
+ * compute never stalls the turns, streams and MCP calls of the process.
13
+ *
14
+ * WHAT A RUN GETS: a fresh runtime in a fresh Worker, a memory limit
15
+ * (default 64 MB), a stack limit, and a CPU budget (default 5 s) measured
16
+ * only while the code computes. Values cross as JSON both ways. The code is
17
+ * a module whose default export is called with the input (`mode: 'input'`)
18
+ * or with an API object (`mode: 'api'`): the caller's values, its host
19
+ * functions as async functions, and `parallel`.
20
+ *
21
+ * THE STOPS: the interrupt handler inside the worker stops the code at its
22
+ * CPU budget; a watchdog here terminates the worker when it computed past
23
+ * the budget without answering; the caller's signal terminates it; and a
24
+ * host function may throw SandboxHalt to end the run on purpose (a script
25
+ * pausing for a person). Runs are capped by a small pool, so the memory
26
+ * they hold is bounded: MELCHIZEDEK_CODE_SANDBOX_WORKERS, default 16.
27
+ *
28
+ * THE POSTURE: nothing runs unless the operator set
29
+ * MELCHIZEDEK_CODE_SANDBOX=on (codeSandboxEnabled). The sandbox grants no
30
+ * power the code's caller lacks, so the switch is a posture, not a
31
+ * boundary: a deployment that never meant to run code does not start
32
+ * because a YAML changed (ADR 0137 item 4, as ADR 0132 gates stdio MCP).
33
+ */
34
+ import { Worker } from 'node:worker_threads';
35
+ /** The operator's switch for running code (ADR 0137 item 4). */
36
+ export const CODE_SANDBOX_ENV = 'MELCHIZEDEK_CODE_SANDBOX';
37
+ /** True when the operator turned the sandbox on: MELCHIZEDEK_CODE_SANDBOX=on (or true, 1). */
38
+ export function codeSandboxEnabled(env = process.env) {
39
+ return /^(on|true|1)$/i.test((env[CODE_SANDBOX_ENV] ?? '').trim());
40
+ }
41
+ /** The message a refusal carries when the switch is off. */
42
+ export function codeSandboxOffMessage(what) {
43
+ return `${what} runs code in the engine's sandbox, which is off; the operator turns it on with ${CODE_SANDBOX_ENV}=on (ADR 0137)`;
44
+ }
45
+ /** A syndicate that needs the sandbox, refused while it is off: it names the variable that turns it on. */
46
+ export class CodeSandboxOffError extends Error {
47
+ code = 'CODE_SANDBOX_OFF';
48
+ constructor(what) {
49
+ super(codeSandboxOffMessage(what));
50
+ this.name = 'CodeSandboxOffError';
51
+ }
52
+ }
53
+ /** What in a syndicate runs code: its `script:`, and each agent that lists `run_code`. */
54
+ export function codeUsersOf(config) {
55
+ const out = [];
56
+ if (config.script)
57
+ out.push(`${config.syndicate_name || 'the syndicate'}'s script`);
58
+ for (const agent of [config.orchestrator, ...(config.subagents ?? [])]) {
59
+ if (agent?.tools?.includes('run_code'))
60
+ out.push(`${agent.name ?? 'an agent'}'s run_code tool`);
61
+ }
62
+ return out;
63
+ }
64
+ /** Throws CodeSandboxOffError when `config` runs code and the operator has not turned the sandbox on. */
65
+ export function refuseWithoutSandbox(config, env = process.env) {
66
+ if (codeSandboxEnabled(env))
67
+ return;
68
+ const users = codeUsersOf(config);
69
+ if (users.length)
70
+ throw new CodeSandboxOffError(users.join(' and '));
71
+ }
72
+ export const DEFAULT_SANDBOX_LIMITS = Object.freeze({
73
+ memoryBytes: 64 * 1024 * 1024,
74
+ stackBytes: 1024 * 1024,
75
+ cpuMs: 5_000,
76
+ maxLogChars: 20_000,
77
+ maxResultChars: 1_000_000,
78
+ maxParallel: 32,
79
+ });
80
+ /**
81
+ * Thrown by a host function to end the run on purpose: the worker is
82
+ * terminated, nothing more of the code runs, and the result carries this
83
+ * error as `halt`. A script pausing for a person halts (lib/script/).
84
+ */
85
+ export class SandboxHalt extends Error {
86
+ detail;
87
+ constructor(message, detail) {
88
+ super(message);
89
+ this.name = 'SandboxHalt';
90
+ this.detail = detail;
91
+ }
92
+ }
93
+ // ── The pool ─────────────────────────────────────────────────────────────────
94
+ const DEFAULT_POOL_SIZE = 16;
95
+ function poolSize() {
96
+ const n = Number(process.env.MELCHIZEDEK_CODE_SANDBOX_WORKERS);
97
+ return Number.isInteger(n) && n > 0 ? Math.min(n, 256) : DEFAULT_POOL_SIZE;
98
+ }
99
+ let running = 0;
100
+ const waiting = [];
101
+ async function acquire(signal) {
102
+ if (running >= poolSize()) {
103
+ await new Promise((resolve, reject) => {
104
+ const go = () => {
105
+ signal?.removeEventListener('abort', cancel);
106
+ resolve();
107
+ };
108
+ const cancel = () => {
109
+ const at = waiting.indexOf(go);
110
+ if (at !== -1)
111
+ waiting.splice(at, 1);
112
+ reject(new Error('canceled'));
113
+ };
114
+ waiting.push(go);
115
+ signal?.addEventListener('abort', cancel, { once: true });
116
+ });
117
+ }
118
+ running++;
119
+ let released = false;
120
+ return () => {
121
+ if (released)
122
+ return;
123
+ released = true;
124
+ running--;
125
+ waiting.shift()?.();
126
+ };
127
+ }
128
+ // ── One run ──────────────────────────────────────────────────────────────────
129
+ /** The worker file beside this one: `.ts` when the engine runs from source, `.js` from the package. */
130
+ function workerUrl() {
131
+ return new URL(import.meta.url.endsWith('.ts') ? './worker.ts' : './worker.js', import.meta.url);
132
+ }
133
+ /** How long past its CPU budget a worker that stopped answering is given before it is terminated. */
134
+ const WATCHDOG_GRACE_MS = 1_000;
135
+ /**
136
+ * Run `source` in a fresh sandbox. Never throws for anything the code does:
137
+ * a thrown error, a limit, a cancel or a halt come back in the result.
138
+ */
139
+ export async function runInSandbox(opts) {
140
+ const limits = { ...DEFAULT_SANDBOX_LIMITS, ...opts.limits };
141
+ const empty = { logs: '', logsTruncated: false, cpuMs: 0 };
142
+ if (opts.signal?.aborted)
143
+ return { ok: false, error: { code: 'CANCELED', message: 'The run was canceled.' }, ...empty };
144
+ let valueJson;
145
+ try {
146
+ valueJson = JSON.stringify(opts.value ?? null) ?? 'null';
147
+ }
148
+ catch (err) {
149
+ return { ok: false, error: { code: 'CODE_ERROR', message: `The input is not JSON-serializable: ${err.message}` }, ...empty };
150
+ }
151
+ let release;
152
+ try {
153
+ release = await acquire(opts.signal);
154
+ }
155
+ catch {
156
+ return { ok: false, error: { code: 'CANCELED', message: 'The run was canceled.' }, ...empty };
157
+ }
158
+ const functions = opts.functions ?? {};
159
+ const worker = new Worker(workerUrl(), {
160
+ // Source runs need type stripping in the worker; the package runs compiled JS.
161
+ execArgv: import.meta.url.endsWith('.ts') ? ['--experimental-strip-types', '--disable-warning=ExperimentalWarning'] : [],
162
+ // The wasm heap is held by setMemoryLimit; this bounds the JS around it.
163
+ resourceLimits: { maxOldGenerationSizeMb: 96, maxYoungGenerationSizeMb: 16 },
164
+ stdout: true,
165
+ stderr: true,
166
+ });
167
+ return new Promise((resolve) => {
168
+ let settled = false;
169
+ let outstanding = 0;
170
+ let idleSince = performance.now();
171
+ let idleTotal = 0;
172
+ const finish = (result) => {
173
+ if (settled)
174
+ return;
175
+ settled = true;
176
+ clearInterval(watchdog);
177
+ opts.signal?.removeEventListener('abort', onAbort);
178
+ void worker.terminate();
179
+ release();
180
+ resolve(result);
181
+ };
182
+ const onAbort = () => finish({ ok: false, error: { code: 'CANCELED', message: 'The run was canceled.' }, ...empty });
183
+ opts.signal?.addEventListener('abort', onAbort, { once: true });
184
+ // The watchdog: compute is what the worker does while no host call is outstanding.
185
+ const watchdog = setInterval(() => {
186
+ const computing = outstanding === 0 ? idleTotal + (performance.now() - idleSince) : idleTotal;
187
+ if (computing > limits.cpuMs + WATCHDOG_GRACE_MS) {
188
+ finish({ ok: false, error: { code: 'CPU_LIMIT', message: `The code ran past its ${limits.cpuMs} ms CPU budget and was stopped.` }, ...empty });
189
+ }
190
+ }, 250);
191
+ watchdog.unref?.();
192
+ const startCall = () => {
193
+ if (outstanding === 0)
194
+ idleTotal += performance.now() - idleSince;
195
+ outstanding++;
196
+ };
197
+ const endCall = () => {
198
+ outstanding--;
199
+ if (outstanding === 0)
200
+ idleSince = performance.now();
201
+ };
202
+ worker.on('message', (message) => {
203
+ if (settled)
204
+ return;
205
+ if (message?.type === 'call') {
206
+ const fn = Object.prototype.hasOwnProperty.call(functions, message.name) ? functions[message.name] : undefined;
207
+ startCall();
208
+ void (async () => {
209
+ try {
210
+ if (!fn)
211
+ throw new Error(`${message.name} is not a function the host passed in`);
212
+ const args = JSON.parse(message.args);
213
+ const value = await fn(Array.isArray(args) ? args : []);
214
+ const json = value === undefined ? undefined : JSON.stringify(value);
215
+ if (!settled)
216
+ worker.postMessage({ type: 'reply', id: message.id, ok: true, value: json });
217
+ }
218
+ catch (err) {
219
+ if (err instanceof SandboxHalt) {
220
+ finish({ ok: false, halt: err, error: { code: 'HALTED', message: err.message }, ...empty });
221
+ return;
222
+ }
223
+ const e = err instanceof Error ? err : new Error(String(err));
224
+ if (!settled)
225
+ worker.postMessage({ type: 'reply', id: message.id, ok: false, error: { name: e.name || 'Error', message: e.message } });
226
+ }
227
+ finally {
228
+ endCall();
229
+ }
230
+ })();
231
+ return;
232
+ }
233
+ if (message?.type === 'done') {
234
+ const base = { logs: String(message.logs ?? ''), logsTruncated: !!message.logsTruncated, cpuMs: Number(message.cpuMs ?? 0) };
235
+ if (!message.ok) {
236
+ finish({ ok: false, error: message.error, ...base });
237
+ return;
238
+ }
239
+ const text = String(message.result ?? 'null');
240
+ if (text.length > limits.maxResultChars) {
241
+ finish({ ok: false, error: { code: 'RESULT_TOO_LARGE', message: `The result is ${text.length.toLocaleString('en-US')} characters of JSON; the limit is ${limits.maxResultChars.toLocaleString('en-US')}.` }, ...base });
242
+ return;
243
+ }
244
+ let result;
245
+ try {
246
+ result = JSON.parse(text);
247
+ }
248
+ catch {
249
+ result = null;
250
+ }
251
+ finish({ ok: true, result, ...base });
252
+ }
253
+ });
254
+ worker.on('error', (err) => {
255
+ const oom = err.code === 'ERR_WORKER_OUT_OF_MEMORY';
256
+ finish({ ok: false, error: { code: oom ? 'MEMORY_LIMIT' : 'SANDBOX_CRASHED', message: oom ? 'The code ran out of memory.' : `The sandbox failed: ${err.message}` }, ...empty });
257
+ });
258
+ worker.on('exit', (code) => {
259
+ finish({ ok: false, error: { code: 'SANDBOX_CRASHED', message: `The sandbox exited (${code}) before the code finished.` }, ...empty });
260
+ });
261
+ worker.postMessage({
262
+ type: 'run',
263
+ source: opts.source,
264
+ mode: opts.mode ?? 'input',
265
+ value: valueJson,
266
+ functions: Object.keys(functions),
267
+ limits: {
268
+ memoryBytes: limits.memoryBytes,
269
+ stackBytes: limits.stackBytes,
270
+ cpuMs: limits.cpuMs,
271
+ maxLogChars: limits.maxLogChars,
272
+ maxParallel: Math.max(1, Math.min(limits.maxParallel, 32)),
273
+ },
274
+ });
275
+ });
276
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * lib/sandbox/worker.ts — one sandbox run, on its own worker thread
3
+ * (ADR 0137 item 1). lib/sandbox/sandbox.ts starts it, one Worker per run,
4
+ * and talks to it in messages; nothing else imports this file.
5
+ *
6
+ * WHAT RUNS HERE: QuickJS compiled to wasm (the release sync build), in a
7
+ * fresh runtime with a memory limit, a stack limit and an interrupt
8
+ * handler that stops the code after its CPU budget. The budget counts only
9
+ * the time this thread spends inside the interpreter, so a run awaiting the
10
+ * host (an agent, a tool) is not charged for the wait.
11
+ *
12
+ * WHAT THE CODE CAN REACH: the argument its default export is called with,
13
+ * `console.log` (captured, bounded), and nothing else. There is no fetch,
14
+ * no timer, no file system, no `process`, no environment; `Date.now`, the
15
+ * current-time `Date` constructor and `Math.random` throw, naming the
16
+ * function to use instead when the host passes one. The host functions the
17
+ * caller names arrive as async functions on the argument; their arguments
18
+ * and results cross as JSON, both ways.
19
+ *
20
+ * THE PROTOCOL (parent ↔ worker):
21
+ * run → { type: 'run', source, mode, value, functions, limits }
22
+ * call ← { type: 'call', id, name, args } a host function was called
23
+ * reply → { type: 'reply', id, ok, value | error }
24
+ * done ← { type: 'done', ok, result | error, logs, logsTruncated, cpuMs }
25
+ * The parent terminates the worker after `done`, on the turn's signal, and
26
+ * when it ran past its budget without answering, a harder stop than the
27
+ * interrupt handler, which QuickJS consults only between bytecodes.
28
+ */
29
+ export {};
@@ -0,0 +1,280 @@
1
+ /**
2
+ * lib/sandbox/worker.ts — one sandbox run, on its own worker thread
3
+ * (ADR 0137 item 1). lib/sandbox/sandbox.ts starts it, one Worker per run,
4
+ * and talks to it in messages; nothing else imports this file.
5
+ *
6
+ * WHAT RUNS HERE: QuickJS compiled to wasm (the release sync build), in a
7
+ * fresh runtime with a memory limit, a stack limit and an interrupt
8
+ * handler that stops the code after its CPU budget. The budget counts only
9
+ * the time this thread spends inside the interpreter, so a run awaiting the
10
+ * host (an agent, a tool) is not charged for the wait.
11
+ *
12
+ * WHAT THE CODE CAN REACH: the argument its default export is called with,
13
+ * `console.log` (captured, bounded), and nothing else. There is no fetch,
14
+ * no timer, no file system, no `process`, no environment; `Date.now`, the
15
+ * current-time `Date` constructor and `Math.random` throw, naming the
16
+ * function to use instead when the host passes one. The host functions the
17
+ * caller names arrive as async functions on the argument; their arguments
18
+ * and results cross as JSON, both ways.
19
+ *
20
+ * THE PROTOCOL (parent ↔ worker):
21
+ * run → { type: 'run', source, mode, value, functions, limits }
22
+ * call ← { type: 'call', id, name, args } a host function was called
23
+ * reply → { type: 'reply', id, ok, value | error }
24
+ * done ← { type: 'done', ok, result | error, logs, logsTruncated, cpuMs }
25
+ * The parent terminates the worker after `done`, on the turn's signal, and
26
+ * when it ran past its budget without answering, a harder stop than the
27
+ * interrupt handler, which QuickJS consults only between bytecodes.
28
+ */
29
+ import { parentPort } from 'node:worker_threads';
30
+ import { newQuickJSWASMModuleFromVariant } from 'quickjs-emscripten-core';
31
+ import variant from '@jitl/quickjs-wasmfile-release-sync';
32
+ const port = parentPort;
33
+ if (!port)
34
+ throw new Error('lib/sandbox/worker.ts runs as a worker thread (lib/sandbox/sandbox.ts starts it)');
35
+ /**
36
+ * Runs before the code: captures the host bridge and removes it from the
37
+ * global object, masks the clock and randomness, and installs `console`.
38
+ * It returns the function the host calls the default export through.
39
+ */
40
+ const PRELUDE = String.raw `(() => {
41
+ const host = globalThis.__host;
42
+ const writeLog = globalThis.__log;
43
+ const config = JSON.parse(globalThis.__config);
44
+ delete globalThis.__host;
45
+ delete globalThis.__log;
46
+ delete globalThis.__config;
47
+ const has = (name) => config.functions.includes(name);
48
+ const blocked = (what, instead) => function () {
49
+ throw new Error(what + ' is not available in the sandbox' + (instead && has(instead) ? '; use ' + instead + '()' : ''));
50
+ };
51
+ Math.random = blocked('Math.random', 'random');
52
+ const RealDate = Date;
53
+ class SandboxDate extends RealDate {
54
+ constructor(...args) {
55
+ if (args.length === 0) throw new Error('new Date() with no arguments reads the clock, which is not available in the sandbox' + (has('now') ? '; use now()' : ''));
56
+ super(...args);
57
+ }
58
+ }
59
+ SandboxDate.now = blocked('Date.now', 'now');
60
+ globalThis.Date = SandboxDate;
61
+ const show = (v) => {
62
+ if (typeof v === 'string') return v;
63
+ try { const s = JSON.stringify(v); return s === undefined ? String(v) : s; } catch { return String(v); }
64
+ };
65
+ const print = (...args) => writeLog(args.map(show).join(' '));
66
+ globalThis.console = Object.freeze({ log: print, info: print, warn: print, error: print, debug: print });
67
+ const call = (name) => (...args) => host(name, JSON.stringify(args)).then((r) => (r === undefined ? undefined : JSON.parse(r)));
68
+ const parallel = async (thunks, opts) => {
69
+ if (!Array.isArray(thunks)) throw new TypeError('parallel takes an array of functions');
70
+ const asked = opts && typeof opts.max === 'number' ? Math.floor(opts.max) : 4;
71
+ const max = Math.max(1, Math.min(asked, config.maxParallel));
72
+ const results = new Array(thunks.length);
73
+ let next = 0;
74
+ const lane = async () => {
75
+ while (next < thunks.length) {
76
+ const i = next++;
77
+ if (typeof thunks[i] !== 'function') throw new TypeError('parallel: item ' + i + ' is not a function');
78
+ results[i] = await thunks[i]();
79
+ }
80
+ };
81
+ await Promise.all(Array.from({ length: Math.min(max, thunks.length) }, lane));
82
+ return results;
83
+ };
84
+ return (fn, valueJson) => {
85
+ const value = JSON.parse(valueJson);
86
+ let arg = value;
87
+ if (config.mode === 'api') {
88
+ arg = { ...(value && typeof value === 'object' ? value : {}) };
89
+ for (const name of config.functions) arg[name] = call(name);
90
+ arg.parallel = parallel;
91
+ Object.freeze(arg);
92
+ }
93
+ return Promise.resolve()
94
+ .then(() => fn(arg))
95
+ .then((r) => {
96
+ const json = JSON.stringify(r);
97
+ return json === undefined ? 'null' : json;
98
+ });
99
+ };
100
+ })()`;
101
+ function post(message) {
102
+ port.postMessage(message);
103
+ }
104
+ port.once('message', (message) => {
105
+ run(message).catch((err) => {
106
+ post({ type: 'done', ok: false, error: { code: 'SANDBOX_CRASHED', message: err instanceof Error ? err.message : String(err) }, logs: '', logsTruncated: false, cpuMs: 0 });
107
+ });
108
+ });
109
+ async function run(message) {
110
+ const { limits } = message;
111
+ // The package's typings describe its CommonJS shape; under ESM the default import is the variant itself.
112
+ const mod = await newQuickJSWASMModuleFromVariant(variant);
113
+ const runtime = mod.newRuntime();
114
+ runtime.setMemoryLimit(limits.memoryBytes);
115
+ runtime.setMaxStackSize(limits.stackBytes);
116
+ // CPU: time inside the interpreter only.
117
+ let used = 0;
118
+ let segmentStart = 0;
119
+ let cpuExceeded = false;
120
+ runtime.setInterruptHandler(() => {
121
+ if (segmentStart && used + (performance.now() - segmentStart) > limits.cpuMs) {
122
+ cpuExceeded = true;
123
+ return true;
124
+ }
125
+ return false;
126
+ });
127
+ const compute = (fn) => {
128
+ segmentStart = performance.now();
129
+ try {
130
+ return fn();
131
+ }
132
+ finally {
133
+ used += performance.now() - segmentStart;
134
+ segmentStart = 0;
135
+ }
136
+ };
137
+ let logs = '';
138
+ let logsTruncated = false;
139
+ let finished = false;
140
+ const finish = (outcome) => {
141
+ if (finished)
142
+ return;
143
+ finished = true;
144
+ post({ type: 'done', ...outcome, logs, logsTruncated, cpuMs: Math.round(used) });
145
+ };
146
+ const vm = runtime.newContext();
147
+ const pending = new Map();
148
+ let nextId = 1;
149
+ const hostFn = vm.newFunction('__host', (nameHandle, argsHandle) => {
150
+ const name = vm.getString(nameHandle);
151
+ const args = vm.getString(argsHandle);
152
+ const deferred = vm.newPromise();
153
+ const id = nextId++;
154
+ pending.set(id, deferred);
155
+ post({ type: 'call', id, name, args });
156
+ return deferred.handle;
157
+ });
158
+ vm.setProp(vm.global, '__host', hostFn);
159
+ hostFn.dispose();
160
+ const logFn = vm.newFunction('__log', (lineHandle) => {
161
+ if (logsTruncated)
162
+ return;
163
+ const line = vm.getString(lineHandle);
164
+ const room = limits.maxLogChars - logs.length;
165
+ if (line.length + 1 > room) {
166
+ logs += line.slice(0, Math.max(0, room));
167
+ logsTruncated = true;
168
+ }
169
+ else {
170
+ logs += (logs ? '\n' : '') + line;
171
+ }
172
+ });
173
+ vm.setProp(vm.global, '__log', logFn);
174
+ logFn.dispose();
175
+ const configHandle = vm.newString(JSON.stringify({ functions: message.functions, mode: message.mode, maxParallel: limits.maxParallel }));
176
+ vm.setProp(vm.global, '__config', configHandle);
177
+ configHandle.dispose();
178
+ /** An error the VM threw or rejected with, as the parent reports it. */
179
+ const describe = (handle) => {
180
+ const dumped = vm.dump(handle);
181
+ if (cpuExceeded)
182
+ return { code: 'CPU_LIMIT', message: `The code ran past its ${limits.cpuMs} ms CPU budget.` };
183
+ // Out of memory, QuickJS can have no room left for an error object and throws null instead.
184
+ if (dumped === null || dumped === undefined) {
185
+ return { code: 'CODE_ERROR', message: `The code threw ${String(dumped)}; code that builds large values throws null when it reaches the ${Math.round(limits.memoryBytes / 1048576)} MB memory limit.` };
186
+ }
187
+ const err = dumped && typeof dumped === 'object' ? dumped : { message: String(dumped) };
188
+ const name = typeof err.name === 'string' ? err.name : 'Error';
189
+ const text = typeof err.message === 'string' ? err.message : String(dumped);
190
+ if (/out of memory/i.test(text))
191
+ return { code: 'MEMORY_LIMIT', message: `The code ran past its ${Math.round(limits.memoryBytes / 1048576)} MB memory limit.` };
192
+ if (/stack overflow/i.test(text))
193
+ return { code: 'STACK_LIMIT', message: 'The code overflowed its stack.' };
194
+ return { code: 'CODE_ERROR', message: `${name}: ${text}` };
195
+ };
196
+ const pump = () => {
197
+ const jobs = compute(() => runtime.executePendingJobs());
198
+ if (jobs.error) {
199
+ const error = describe(jobs.error);
200
+ jobs.error.dispose();
201
+ finish({ ok: false, error });
202
+ return false;
203
+ }
204
+ if (cpuExceeded) {
205
+ finish({ ok: false, error: { code: 'CPU_LIMIT', message: `The code ran past its ${limits.cpuMs} ms CPU budget.` } });
206
+ return false;
207
+ }
208
+ return true;
209
+ };
210
+ port.on('message', (reply) => {
211
+ if (finished || reply.type !== 'reply')
212
+ return;
213
+ const deferred = pending.get(reply.id);
214
+ if (!deferred)
215
+ return;
216
+ pending.delete(reply.id);
217
+ if (reply.ok) {
218
+ if (reply.value === undefined)
219
+ deferred.resolve();
220
+ else {
221
+ const value = vm.newString(reply.value);
222
+ deferred.resolve(value);
223
+ value.dispose();
224
+ }
225
+ }
226
+ else {
227
+ const error = vm.newError({ name: reply.error?.name ?? 'Error', message: reply.error?.message ?? 'the host call failed' });
228
+ deferred.reject(error);
229
+ error.dispose();
230
+ }
231
+ deferred.dispose();
232
+ pump();
233
+ });
234
+ const prelude = compute(() => vm.evalCode(PRELUDE, 'prelude.js'));
235
+ if (prelude.error) {
236
+ finish({ ok: false, error: { code: 'SANDBOX_CRASHED', message: `prelude: ${JSON.stringify(vm.dump(prelude.error))}` } });
237
+ return;
238
+ }
239
+ const invoke = prelude.value;
240
+ const evaluated = compute(() => vm.evalCode(message.source, 'code.js', { type: 'module' }));
241
+ if (evaluated.error) {
242
+ finish({ ok: false, error: describe(evaluated.error) });
243
+ return;
244
+ }
245
+ // A module evaluates to a promise of its namespace (top-level await may hold it open);
246
+ // resolvePromise attaches its handlers in the VM, which run as pending jobs.
247
+ const loading = vm.resolvePromise(evaluated.value);
248
+ if (!pump())
249
+ return;
250
+ const loaded = await loading;
251
+ if (finished)
252
+ return;
253
+ if (loaded.error) {
254
+ finish({ ok: false, error: describe(loaded.error) });
255
+ return;
256
+ }
257
+ const exported = vm.getProp(loaded.value, 'default');
258
+ if (vm.typeof(exported) !== 'function') {
259
+ finish({ ok: false, error: { code: 'CODE_ERROR', message: 'The code must `export default` a function; it is called with the input.' } });
260
+ return;
261
+ }
262
+ const valueHandle = vm.newString(message.value);
263
+ const called = compute(() => vm.callFunction(invoke, vm.undefined, exported, valueHandle));
264
+ valueHandle.dispose();
265
+ if (called.error) {
266
+ finish({ ok: false, error: describe(called.error) });
267
+ return;
268
+ }
269
+ const settling = vm.resolvePromise(called.value);
270
+ if (!pump())
271
+ return;
272
+ const settled = await settling;
273
+ if (finished)
274
+ return;
275
+ if (settled.error) {
276
+ finish({ ok: false, error: describe(settled.error) });
277
+ return;
278
+ }
279
+ finish({ ok: true, result: vm.getString(settled.value) });
280
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * lib/script/config.ts — the `script:` block's shape (ADR 0137 Part B), a
3
+ * leaf the loader, the schema's types and the runner share.
4
+ *
5
+ * The fourth orchestration method: DELEGATE, PLAN-DISPATCH and WORKFLOW
6
+ * decide the order agents run in, and none can hold logic. A script is one
7
+ * JavaScript module, run in the engine's sandbox (lib/sandbox/), that calls
8
+ * the syndicate's own agents, its allowed tools and a person as functions,
9
+ * so the routing, the per-call model and the pauses live in the syndicate
10
+ * document and change with a deploy, not an engine release. The runner is
11
+ * lib/script/run.ts.
12
+ */
13
+ /** The `script:` block. */
14
+ export interface ScriptConfig {
15
+ /** One module whose default export is an async function of the script API. Not interpolated. */
16
+ source: string;
17
+ /** The models agent(…, { model }) may move a call to; every agent is compiled on each. */
18
+ models?: string[];
19
+ /** The registry tools tool(name, args) may run. */
20
+ tools?: string[];
21
+ /** Tools from `tools` whose every call waits for a person's approval. */
22
+ require_approval?: string[];
23
+ /**
24
+ * The syndicate's `variables`, bound as the loader bound them (the YAML's
25
+ * defaults, then the caller's bindings): what the script reads as
26
+ * `variables`. Written by the loader, never by hand.
27
+ */
28
+ variables?: Record<string, string | number | boolean>;
29
+ }
30
+ /** True when this syndicate orchestrates with a script. */
31
+ export declare function isScriptSyndicate(config: {
32
+ script?: unknown;
33
+ }): config is {
34
+ script: ScriptConfig;
35
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * lib/script/config.ts — the `script:` block's shape (ADR 0137 Part B), a
3
+ * leaf the loader, the schema's types and the runner share.
4
+ *
5
+ * The fourth orchestration method: DELEGATE, PLAN-DISPATCH and WORKFLOW
6
+ * decide the order agents run in, and none can hold logic. A script is one
7
+ * JavaScript module, run in the engine's sandbox (lib/sandbox/), that calls
8
+ * the syndicate's own agents, its allowed tools and a person as functions,
9
+ * so the routing, the per-call model and the pauses live in the syndicate
10
+ * document and change with a deploy, not an engine release. The runner is
11
+ * lib/script/run.ts.
12
+ */
13
+ /** True when this syndicate orchestrates with a script. */
14
+ export function isScriptSyndicate(config) {
15
+ const script = config.script;
16
+ return !!script && typeof script === 'object' && typeof script.source === 'string';
17
+ }