@brian-agent/codemode 0.0.0-stage → 0.1.1
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/README.md +373 -2
- package/dist/cloudflare.d.ts +53 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +64 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/declarations.d.ts +43 -0
- package/dist/declarations.d.ts.map +1 -0
- package/dist/declarations.js +320 -0
- package/dist/declarations.js.map +1 -0
- package/dist/identifier.d.ts +6 -0
- package/dist/identifier.d.ts.map +1 -0
- package/dist/identifier.js +13 -0
- package/dist/identifier.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/portable.d.ts +15 -0
- package/dist/portable.d.ts.map +1 -0
- package/dist/portable.js +12 -0
- package/dist/portable.js.map +1 -0
- package/dist/runtime/execution.d.ts +66 -0
- package/dist/runtime/execution.d.ts.map +1 -0
- package/dist/runtime/execution.js +494 -0
- package/dist/runtime/execution.js.map +1 -0
- package/dist/runtime/host.d.ts +13 -0
- package/dist/runtime/host.d.ts.map +1 -0
- package/dist/runtime/host.js +86 -0
- package/dist/runtime/host.js.map +1 -0
- package/dist/runtime/inline.d.ts +63 -0
- package/dist/runtime/inline.d.ts.map +1 -0
- package/dist/runtime/inline.js +80 -0
- package/dist/runtime/inline.js.map +1 -0
- package/dist/runtime/prelude-source.d.ts +45 -0
- package/dist/runtime/prelude-source.d.ts.map +1 -0
- package/dist/runtime/prelude-source.js +520 -0
- package/dist/runtime/prelude-source.js.map +1 -0
- package/dist/runtime/protocol.d.ts +110 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +15 -0
- package/dist/runtime/protocol.js.map +1 -0
- package/dist/runtime/remote.d.ts +102 -0
- package/dist/runtime/remote.d.ts.map +1 -0
- package/dist/runtime/remote.js +175 -0
- package/dist/runtime/remote.js.map +1 -0
- package/dist/runtime/vm.d.ts +24 -0
- package/dist/runtime/vm.d.ts.map +1 -0
- package/dist/runtime/vm.js +293 -0
- package/dist/runtime/vm.js.map +1 -0
- package/dist/runtime/worker.d.ts +2 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +38 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/source.d.ts +36 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +98 -0
- package/dist/source.js.map +1 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm.d.ts +12 -0
- package/dist/wasm.d.ts.map +1 -0
- package/dist/wasm.js +24 -0
- package/dist/wasm.js.map +1 -0
- package/package.json +75 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/runtime/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/D,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAC7C,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACpD;AAED,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC9C,iGAAiG;IACjG,IAAI,EAAE,kBAAkB,CAAC;IACzB;;;OAGG;IACH,SAAS,EAAE,iBAAiB,CAAC;CAC7B;AAED,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC,MAAM,MAAM,mBAAmB,GAC5B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC/F;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAA;CAAE;AAC9C;;;GAGG;GACD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAE,GAC9F;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAE;AAC9E,2EAA2E;GACzE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACpC,0GAA0G;GACxG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE;AAC1C;;;GAGG;GACD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,MAAM,mBAAmB;AAC9B;;;GAGG;AACH;IACC,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAWlF;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAElF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isWorkerToHostMessage(value) {
|
|
2
|
+
if (typeof value !== "object" || value === null)
|
|
3
|
+
return false;
|
|
4
|
+
const type = value.type;
|
|
5
|
+
return (type === "call" ||
|
|
6
|
+
type === "output" ||
|
|
7
|
+
type === "done" ||
|
|
8
|
+
type === "crash" ||
|
|
9
|
+
type === "started" ||
|
|
10
|
+
type === "idle");
|
|
11
|
+
}
|
|
12
|
+
export function isHostToWorkerMessage(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && value.type === "result";
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/runtime/protocol.ts"],"names":[],"mappings":"AA6EA,MAAM,UAAU,qBAAqB,CAAC,KAAc;IACnD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,IAAI,GAAI,KAA4B,CAAC,IAAI,CAAC;IAChD,OAAO,CACN,IAAI,KAAK,MAAM;QACf,IAAI,KAAK,QAAQ;QACjB,IAAI,KAAK,MAAM;QACf,IAAI,KAAK,OAAO;QAChB,IAAI,KAAK,SAAS;QAClB,IAAI,KAAK,MAAM,CACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAc;IACnD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAA4B,CAAC,IAAI,KAAK,QAAQ,CAAC;AACvG,CAAC","sourcesContent":["import type { CodemodeDuring, CodemodeOutputItem } from \"../types.ts\";\nimport type { CodemodeWasmModule } from \"../wasm.ts\";\n\n/**\n * Messages between the host (main thread) and the worker. Tool arguments,\n * results, and values cross as JSON strings: the worker passes them into and\n * out of the QuickJS VM as strings and never builds structured values itself.\n */\n\n/**\n * Everything a VM needs to run one script, independent of where it runs. Plain data, so it can\n * cross a `postMessage` or a Workers RPC call unchanged.\n */\nexport interface VmStartData {\n\tcode: string;\n\t/** `jsName` is the identifier the script uses; `description` is listed in `ALL_TOOLS`. */\n\ttools: { name: string; jsName: string; description: string }[];\n\tglobals: { name: string; spread: boolean }[];\n\tmemoryLimitBytes: number | undefined;\n\t/** Snapshot for `load()`: key to JSON text. */\n\tstore: Record<string, string>;\n\t/**\n\t * Run deterministically (`CodemodeSandboxBase.start()`): UTC, a virtual clock starting at `startedAt` (ms since\n\t * the epoch) that moves only with delivered results, randomness from `seed`, no `WeakRef`, results applied in\n\t * `seq` order, and `started`/`idle` messages.\n\t */\n\tdeterministic?: { startedAt: number; seed: number };\n}\n\nexport interface WorkerData extends VmStartData {\n\t/** Compiled `quickjs-wasi` module. Structured clone shares the compiled code with the worker. */\n\twasm: CodemodeWasmModule;\n\t/**\n\t * One Int32 the host sets to non-zero before terminating the worker. The VM's interrupt handler\n\t * polls it, because Bun's `worker.terminate()` cannot stop a thread that is spinning in wasm.\n\t */\n\tinterrupt: SharedArrayBuffer;\n}\n\n/** JSON-encoded `{ name?, message, stack? }` of an error thrown by the script. */\nexport type ScriptErrorJson = string;\n\nexport type WorkerToHostMessage =\n\t| { type: \"call\"; id: number; target: \"tool\" | \"global\"; name: string; args: string | undefined }\n\t| { type: \"output\"; item: CodemodeOutputItem }\n\t/**\n\t * `writes` is a JSON array of `[key, json]` for `store()` and `[key]` for deletions. Deterministic mode: `during`\n\t * is the result the VM was processing when the script finished (0: its first run) and the calls issued by then.\n\t */\n\t| { type: \"done\"; ok: true; value: string | undefined; writes: string; during?: CodemodeDuring }\n\t| { type: \"done\"; ok: false; error: ScriptErrorJson; during?: CodemodeDuring }\n\t/** The VM failed outside the script's control, for example a wasm trap. */\n\t| { type: \"crash\"; message: string }\n\t/** Deterministic mode, before the script runs: what the VM is, so a replay can refuse a different one. */\n\t| { type: \"started\"; fingerprint: string }\n\t/**\n\t * Deterministic mode, after the script waits again: having processed result `seq` (0: the script's first run), it\n\t * has issued `issued` calls in total.\n\t */\n\t| { type: \"idle\"; seq: number; issued: number };\n\nexport type HostToWorkerMessage =\n\t/**\n\t * `payload` is the JSON result when `ok`, otherwise the error message. Deterministic mode: `seq` numbers the\n\t * results from 1 in the order the VM applies them, and the VM's clock reads `now` while it processes this one.\n\t */\n\t{\n\t\ttype: \"result\";\n\t\tid: number;\n\t\tok: boolean;\n\t\tpayload: string | undefined;\n\t\tseq?: number;\n\t\tnow?: number;\n\t\t/** Deterministic mode, `ok: false`: a JSON object whose properties the script's `Error` gets. */\n\t\tproperties?: string;\n\t};\n\nexport function isWorkerToHostMessage(value: unknown): value is WorkerToHostMessage {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst type = (value as { type?: unknown }).type;\n\treturn (\n\t\ttype === \"call\" ||\n\t\ttype === \"output\" ||\n\t\ttype === \"done\" ||\n\t\ttype === \"crash\" ||\n\t\ttype === \"started\" ||\n\t\ttype === \"idle\"\n\t);\n}\n\nexport function isHostToWorkerMessage(value: unknown): value is HostToWorkerMessage {\n\treturn typeof value === \"object\" && value !== null && (value as { type?: unknown }).type === \"result\";\n}\n"]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the VM in another isolate, reached over an RPC boundary such as a Cloudflare Durable Object
|
|
3
|
+
* stub, while tools keep running in the caller.
|
|
4
|
+
*
|
|
5
|
+
* Problem: a VM on the calling thread blocks that thread while the script computes. In a Durable
|
|
6
|
+
* Object that serves an agent session, a CPU-heavy script would stall every other request to the
|
|
7
|
+
* session (including the user's abort) for seconds. Measured on Cloudflare (October 2026), a
|
|
8
|
+
* script that computed for 10 seconds delayed the session's other requests by 10 seconds when it
|
|
9
|
+
* ran in the session object or in a Dynamic Worker, but by about 50 ms when it ran in a Durable
|
|
10
|
+
* Object of a separately deployed Worker.
|
|
11
|
+
*
|
|
12
|
+
* Solution: the caller keeps the host role (`RemoteCodemodeSandbox`: tools, call records,
|
|
13
|
+
* deadline, abort) and the remote side only runs the VM (`serveCodemodeRemote`, behind
|
|
14
|
+
* `CodemodeSandboxDurableObject` in `@brian-agent/codemode/cloudflare`).
|
|
15
|
+
*
|
|
16
|
+
* Protocol, one RPC call per execution:
|
|
17
|
+
*
|
|
18
|
+
* client remote
|
|
19
|
+
* remote.execute(request, exchange) ───────▶ starts the VM (in-thread, with interrupt budget)
|
|
20
|
+
* ◀─────── exchange([...outputs, call]) one call per tool call
|
|
21
|
+
* runs the tool, replies with result ──────▶ VM continues
|
|
22
|
+
* ◀─────── exchange([...outputs, done]) terminal, reply ignored
|
|
23
|
+
* execute() resolves ◀────────────────────── VM disposed
|
|
24
|
+
*
|
|
25
|
+
* `exchange` is a function the client passes in; over Workers RPC it becomes a stub that calls back
|
|
26
|
+
* into the client. Output is buffered on the remote side and sent with the next call or the
|
|
27
|
+
* terminal message, so a script that prints in a loop does not make one RPC per line. Each
|
|
28
|
+
* `exchange` carries at most one call, so concurrent tool calls are concurrent RPCs. Calls on one
|
|
29
|
+
* stub are delivered in order, which keeps output ordered before the call or result that follows.
|
|
30
|
+
*
|
|
31
|
+
* Termination: the client cannot interrupt a remote VM that is computing. When the client finishes
|
|
32
|
+
* first (deadline, abort, `close()`), it answers every pending and later `exchange` with
|
|
33
|
+
* `{ type: "stop" }`, and the remote stops the VM at that point. A script that computes without
|
|
34
|
+
* calling a tool runs until its interrupt budget is used up. The remote reports that as a
|
|
35
|
+
* `failure` message, which the client turns into the same `kind: "timeout"` error an in-thread
|
|
36
|
+
* VM produces.
|
|
37
|
+
*
|
|
38
|
+
* Failure: if the RPC call rejects (the remote isolate crashed, hit its CPU limit, or was
|
|
39
|
+
* redeployed) or returns without a terminal message, the execution fails with `kind: "sandbox"`.
|
|
40
|
+
*/
|
|
41
|
+
import type { CodemodeError, CodemodeSandboxBaseOptions } from "../types.ts";
|
|
42
|
+
import type { CodemodeWasmModule } from "../wasm.ts";
|
|
43
|
+
import { CodemodeSandboxBase } from "./execution.ts";
|
|
44
|
+
import { type HostToWorkerMessage, type VmStartData, type WorkerToHostMessage } from "./protocol.ts";
|
|
45
|
+
export interface CodemodeRemoteRequest {
|
|
46
|
+
data: VmStartData;
|
|
47
|
+
/** Requested interrupt budget. The remote may lower it, see {@link CodemodeServeOptions}. */
|
|
48
|
+
interruptBudget?: number;
|
|
49
|
+
}
|
|
50
|
+
/** What the remote sends to the client: VM messages, or a failure outside the VM bridge. */
|
|
51
|
+
export type CodemodeRemoteMessage = WorkerToHostMessage | {
|
|
52
|
+
type: "failure";
|
|
53
|
+
error: CodemodeError;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The client's answer to an exchange: the result of the call the exchange carried, `null` when it
|
|
57
|
+
* carried no call, or `stop` once the client no longer wants the execution.
|
|
58
|
+
*/
|
|
59
|
+
export type CodemodeRemoteReply = HostToWorkerMessage | {
|
|
60
|
+
type: "stop";
|
|
61
|
+
} | null;
|
|
62
|
+
export type CodemodeRemoteExchange = (messages: CodemodeRemoteMessage[]) => Promise<CodemodeRemoteReply>;
|
|
63
|
+
/**
|
|
64
|
+
* The remote end, usually a Durable Object stub for `CodemodeSandboxDurableObject`. Anything with
|
|
65
|
+
* this method works, for example an in-process object in tests.
|
|
66
|
+
*/
|
|
67
|
+
export interface CodemodeRemote {
|
|
68
|
+
execute(request: CodemodeRemoteRequest, exchange: CodemodeRemoteExchange): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
export interface RemoteCodemodeSandboxOptions extends CodemodeSandboxBaseOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Returns the remote that runs one execution, called once per `execute()`. For a Durable Object,
|
|
73
|
+
* pick the instance here, for example `() => env.CODEMODE_SANDBOX.getByName(sessionId)`.
|
|
74
|
+
*/
|
|
75
|
+
remote: () => CodemodeRemote;
|
|
76
|
+
/** Interrupt budget requested from the remote. Default: the remote's default. */
|
|
77
|
+
interruptBudget?: number;
|
|
78
|
+
}
|
|
79
|
+
export interface CodemodeServeOptions {
|
|
80
|
+
wasm: CodemodeWasmModule | Promise<CodemodeWasmModule>;
|
|
81
|
+
/** Upper bound for a request's interrupt budget, and the budget when a request names none. */
|
|
82
|
+
maxInterruptBudget?: number;
|
|
83
|
+
/** Upper bound for a request's VM memory, and the limit when a request names none. */
|
|
84
|
+
maxMemoryLimitBytes?: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Remote side of {@link RemoteCodemodeSandbox}: runs one execution's VM on this thread and
|
|
88
|
+
* forwards everything it reports through `exchange`. Resolves once the VM has stopped and the
|
|
89
|
+
* terminal message has been delivered, or the client asked to stop. Never rejects.
|
|
90
|
+
*/
|
|
91
|
+
export declare function serveCodemodeRemote(request: CodemodeRemoteRequest, exchange: CodemodeRemoteExchange, options: CodemodeServeOptions): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Runs each execution's VM on a remote, usually a Cloudflare Durable Object deployed with
|
|
94
|
+
* `CodemodeSandboxDurableObject`, while tools run here. Same API and results as
|
|
95
|
+
* `CodemodeSandbox`, so it can replace it. Differences: a script that computes without calling a
|
|
96
|
+
* tool is limited by the remote's interrupt budget rather than `timeoutMs`, and after a deadline or
|
|
97
|
+
* abort the remote VM keeps computing until it next calls a tool or uses up that budget.
|
|
98
|
+
*/
|
|
99
|
+
export declare class RemoteCodemodeSandbox extends CodemodeSandboxBase {
|
|
100
|
+
constructor(options: RemoteCodemodeSandboxOptions);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=remote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../../src/runtime/remote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAiD,MAAM,gBAAgB,CAAC;AAEpG,OAAO,EACN,KAAK,mBAAmB,EAExB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,WAAW,CAAC;IAClB,6FAA6F;IAC7F,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,4FAA4F;AAC5F,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAEpG;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAEhF,MAAM,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,qBAAqB,EAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAEzG;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,CAAC,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzF;AAED,MAAM,WAAW,4BAA6B,SAAQ,0BAA0B;IAC/E;;;OAGG;IACH,MAAM,EAAE,MAAM,cAAc,CAAC;IAC7B,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,8FAA8F;IAC9F,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sFAAsF;IACtF,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,sBAAsB,EAChC,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAyFf;AAqED;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,mBAAmB;IAC7D,YAAY,OAAO,EAAE,4BAA4B,EAEhD;CACD"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { CodemodeSandboxBase, errorMessage } from "./execution.js";
|
|
2
|
+
import { DEFAULT_INTERRUPT_BUDGET, inlineLauncher } from "./inline.js";
|
|
3
|
+
import { isWorkerToHostMessage, } from "./protocol.js";
|
|
4
|
+
/** Output buffered on the remote before it is flushed without waiting for a call. */
|
|
5
|
+
const FLUSH_ITEMS = 1_000;
|
|
6
|
+
const FLUSH_CHARS = 1 << 20;
|
|
7
|
+
/**
|
|
8
|
+
* Remote side of {@link RemoteCodemodeSandbox}: runs one execution's VM on this thread and
|
|
9
|
+
* forwards everything it reports through `exchange`. Resolves once the VM has stopped and the
|
|
10
|
+
* terminal message has been delivered, or the client asked to stop. Never rejects.
|
|
11
|
+
*/
|
|
12
|
+
export function serveCodemodeRemote(request, exchange, options) {
|
|
13
|
+
const maxBudget = options.maxInterruptBudget ?? DEFAULT_INTERRUPT_BUDGET;
|
|
14
|
+
const budget = Math.min(request.interruptBudget ?? maxBudget, maxBudget);
|
|
15
|
+
const requestedMemory = request.data.memoryLimitBytes;
|
|
16
|
+
const maxMemory = options.maxMemoryLimitBytes;
|
|
17
|
+
const memoryLimitBytes = requestedMemory === undefined
|
|
18
|
+
? maxMemory
|
|
19
|
+
: maxMemory === undefined
|
|
20
|
+
? requestedMemory
|
|
21
|
+
: Math.min(requestedMemory, maxMemory);
|
|
22
|
+
const data = { ...request.data, memoryLimitBytes };
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let channel;
|
|
25
|
+
let ended = false;
|
|
26
|
+
let buffered = [];
|
|
27
|
+
let bufferedChars = 0;
|
|
28
|
+
/** Replies that arrived before the launcher returned the channel. */
|
|
29
|
+
const unsent = [];
|
|
30
|
+
const takeBuffer = () => {
|
|
31
|
+
const batch = buffered;
|
|
32
|
+
buffered = [];
|
|
33
|
+
bufferedChars = 0;
|
|
34
|
+
return batch;
|
|
35
|
+
};
|
|
36
|
+
const end = (terminal) => {
|
|
37
|
+
if (ended)
|
|
38
|
+
return;
|
|
39
|
+
ended = true;
|
|
40
|
+
const batch = takeBuffer();
|
|
41
|
+
const delivered = terminal ? exchange([...batch, terminal]).catch(() => null) : Promise.resolve();
|
|
42
|
+
void delivered
|
|
43
|
+
.then(() => channel?.stop())
|
|
44
|
+
.catch(() => undefined)
|
|
45
|
+
.then(() => resolve());
|
|
46
|
+
};
|
|
47
|
+
const reply = (message) => {
|
|
48
|
+
if (ended || message === null)
|
|
49
|
+
return;
|
|
50
|
+
if (message.type === "stop") {
|
|
51
|
+
end(undefined);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (channel)
|
|
55
|
+
channel.post(message);
|
|
56
|
+
else
|
|
57
|
+
unsent.push(message);
|
|
58
|
+
};
|
|
59
|
+
const onMessage = (message) => {
|
|
60
|
+
if (ended)
|
|
61
|
+
return;
|
|
62
|
+
const typed = message;
|
|
63
|
+
switch (typed.type) {
|
|
64
|
+
case "output": {
|
|
65
|
+
buffered.push(typed);
|
|
66
|
+
bufferedChars += typed.item.type === "text" ? typed.item.text.length : typed.item.data.length;
|
|
67
|
+
if (buffered.length >= FLUSH_ITEMS || bufferedChars >= FLUSH_CHARS) {
|
|
68
|
+
exchange(takeBuffer()).then(reply, () => end(undefined));
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "started":
|
|
73
|
+
buffered.push(typed);
|
|
74
|
+
break;
|
|
75
|
+
// Sent at once: a host replaying a script waits for it to see that the script issued no further call.
|
|
76
|
+
case "call":
|
|
77
|
+
case "idle":
|
|
78
|
+
exchange([...takeBuffer(), typed]).then(reply, () => end(undefined));
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
end(typed);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const launch = inlineLauncher(options.wasm, budget);
|
|
85
|
+
Promise.resolve(launch(data, {
|
|
86
|
+
message: onMessage,
|
|
87
|
+
failure: (error) => end({ type: "failure", error }),
|
|
88
|
+
})).then((started) => {
|
|
89
|
+
channel = started;
|
|
90
|
+
if (ended) {
|
|
91
|
+
void started.stop().catch(() => undefined);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const message of unsent.splice(0))
|
|
95
|
+
started.post(message);
|
|
96
|
+
}, (error) => end({ type: "failure", error: { kind: "sandbox", message: errorMessage(error) } }));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function remoteLauncher(remote, interruptBudget) {
|
|
100
|
+
return (data, events) => {
|
|
101
|
+
let stopped = false;
|
|
102
|
+
let settled = false;
|
|
103
|
+
const waiting = new Map();
|
|
104
|
+
const exchange = (messages) => new Promise((resolve) => {
|
|
105
|
+
if (stopped || !Array.isArray(messages)) {
|
|
106
|
+
resolve({ type: "stop" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Register the waiter first: the host may answer a call synchronously, for example when the
|
|
110
|
+
// tool throws before returning a promise.
|
|
111
|
+
const call = messages.find((message) => isWorkerToHostMessage(message) && message.type === "call");
|
|
112
|
+
if (call?.type === "call")
|
|
113
|
+
waiting.set(call.id, resolve);
|
|
114
|
+
else
|
|
115
|
+
resolve(null);
|
|
116
|
+
for (const message of messages) {
|
|
117
|
+
if (typeof message === "object" && message !== null && message.type === "failure") {
|
|
118
|
+
settled = true;
|
|
119
|
+
events.failure(message.error);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Malformed messages still go to the host, which fails the execution as a broken bridge.
|
|
123
|
+
events.message(message);
|
|
124
|
+
if (isWorkerToHostMessage(message) && (message.type === "done" || message.type === "crash"))
|
|
125
|
+
settled = true;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
let target;
|
|
129
|
+
try {
|
|
130
|
+
target = remote();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw new Error(`Failed to reach the remote sandbox: ${errorMessage(error)}`);
|
|
134
|
+
}
|
|
135
|
+
const request = interruptBudget === undefined ? { data } : { data, interruptBudget };
|
|
136
|
+
Promise.resolve()
|
|
137
|
+
.then(() => target.execute(request, exchange))
|
|
138
|
+
.then(() => {
|
|
139
|
+
if (!settled && !stopped) {
|
|
140
|
+
events.failure({ kind: "sandbox", message: "Remote sandbox returned before the script settled" });
|
|
141
|
+
}
|
|
142
|
+
}, (error) => {
|
|
143
|
+
if (!stopped)
|
|
144
|
+
events.failure({ kind: "sandbox", message: `Remote sandbox failed: ${errorMessage(error)}` });
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
post: (message) => {
|
|
148
|
+
const resolve = waiting.get(message.id);
|
|
149
|
+
if (!resolve)
|
|
150
|
+
return;
|
|
151
|
+
waiting.delete(message.id);
|
|
152
|
+
resolve(message);
|
|
153
|
+
},
|
|
154
|
+
stop: async () => {
|
|
155
|
+
stopped = true;
|
|
156
|
+
for (const resolve of waiting.values())
|
|
157
|
+
resolve({ type: "stop" });
|
|
158
|
+
waiting.clear();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Runs each execution's VM on a remote, usually a Cloudflare Durable Object deployed with
|
|
165
|
+
* `CodemodeSandboxDurableObject`, while tools run here. Same API and results as
|
|
166
|
+
* `CodemodeSandbox`, so it can replace it. Differences: a script that computes without calling a
|
|
167
|
+
* tool is limited by the remote's interrupt budget rather than `timeoutMs`, and after a deadline or
|
|
168
|
+
* abort the remote VM keeps computing until it next calls a tool or uses up that budget.
|
|
169
|
+
*/
|
|
170
|
+
export class RemoteCodemodeSandbox extends CodemodeSandboxBase {
|
|
171
|
+
constructor(options) {
|
|
172
|
+
super(options, remoteLauncher(options.remote, options.interruptBudget));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.js","sourceRoot":"","sources":["../../src/runtime/remote.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runs the VM in another isolate, reached over an RPC boundary such as a Cloudflare Durable Object\n * stub, while tools keep running in the caller.\n *\n * Problem: a VM on the calling thread blocks that thread while the script computes. In a Durable\n * Object that serves an agent session, a CPU-heavy script would stall every other request to the\n * session (including the user's abort) for seconds. Measured on Cloudflare (October 2026), a\n * script that computed for 10 seconds delayed the session's other requests by 10 seconds when it\n * ran in the session object or in a Dynamic Worker, but by about 50 ms when it ran in a Durable\n * Object of a separately deployed Worker.\n *\n * Solution: the caller keeps the host role (`RemoteCodemodeSandbox`: tools, call records,\n * deadline, abort) and the remote side only runs the VM (`serveCodemodeRemote`, behind\n * `CodemodeSandboxDurableObject` in `@brian-agent/codemode/cloudflare`).\n *\n * Protocol, one RPC call per execution:\n *\n * client remote\n * remote.execute(request, exchange) ───────▶ starts the VM (in-thread, with interrupt budget)\n * ◀─────── exchange([...outputs, call]) one call per tool call\n * runs the tool, replies with result ──────▶ VM continues\n * ◀─────── exchange([...outputs, done]) terminal, reply ignored\n * execute() resolves ◀────────────────────── VM disposed\n *\n * `exchange` is a function the client passes in; over Workers RPC it becomes a stub that calls back\n * into the client. Output is buffered on the remote side and sent with the next call or the\n * terminal message, so a script that prints in a loop does not make one RPC per line. Each\n * `exchange` carries at most one call, so concurrent tool calls are concurrent RPCs. Calls on one\n * stub are delivered in order, which keeps output ordered before the call or result that follows.\n *\n * Termination: the client cannot interrupt a remote VM that is computing. When the client finishes\n * first (deadline, abort, `close()`), it answers every pending and later `exchange` with\n * `{ type: \"stop\" }`, and the remote stops the VM at that point. A script that computes without\n * calling a tool runs until its interrupt budget is used up. The remote reports that as a\n * `failure` message, which the client turns into the same `kind: \"timeout\"` error an in-thread\n * VM produces.\n *\n * Failure: if the RPC call rejects (the remote isolate crashed, hit its CPU limit, or was\n * redeployed) or returns without a terminal message, the execution fails with `kind: \"sandbox\"`.\n */\nimport type { CodemodeError, CodemodeSandboxBaseOptions } from \"../types.ts\";\nimport type { CodemodeWasmModule } from \"../wasm.ts\";\nimport { CodemodeSandboxBase, errorMessage, type VmChannel, type VmLauncher } from \"./execution.ts\";\nimport { DEFAULT_INTERRUPT_BUDGET, inlineLauncher } from \"./inline.ts\";\nimport {\n\ttype HostToWorkerMessage,\n\tisWorkerToHostMessage,\n\ttype VmStartData,\n\ttype WorkerToHostMessage,\n} from \"./protocol.ts\";\n\nexport interface CodemodeRemoteRequest {\n\tdata: VmStartData;\n\t/** Requested interrupt budget. The remote may lower it, see {@link CodemodeServeOptions}. */\n\tinterruptBudget?: number;\n}\n\n/** What the remote sends to the client: VM messages, or a failure outside the VM bridge. */\nexport type CodemodeRemoteMessage = WorkerToHostMessage | { type: \"failure\"; error: CodemodeError };\n\n/**\n * The client's answer to an exchange: the result of the call the exchange carried, `null` when it\n * carried no call, or `stop` once the client no longer wants the execution.\n */\nexport type CodemodeRemoteReply = HostToWorkerMessage | { type: \"stop\" } | null;\n\nexport type CodemodeRemoteExchange = (messages: CodemodeRemoteMessage[]) => Promise<CodemodeRemoteReply>;\n\n/**\n * The remote end, usually a Durable Object stub for `CodemodeSandboxDurableObject`. Anything with\n * this method works, for example an in-process object in tests.\n */\nexport interface CodemodeRemote {\n\texecute(request: CodemodeRemoteRequest, exchange: CodemodeRemoteExchange): Promise<void>;\n}\n\nexport interface RemoteCodemodeSandboxOptions extends CodemodeSandboxBaseOptions {\n\t/**\n\t * Returns the remote that runs one execution, called once per `execute()`. For a Durable Object,\n\t * pick the instance here, for example `() => env.CODEMODE_SANDBOX.getByName(sessionId)`.\n\t */\n\tremote: () => CodemodeRemote;\n\t/** Interrupt budget requested from the remote. Default: the remote's default. */\n\tinterruptBudget?: number;\n}\n\n/** Output buffered on the remote before it is flushed without waiting for a call. */\nconst FLUSH_ITEMS = 1_000;\nconst FLUSH_CHARS = 1 << 20;\n\nexport interface CodemodeServeOptions {\n\twasm: CodemodeWasmModule | Promise<CodemodeWasmModule>;\n\t/** Upper bound for a request's interrupt budget, and the budget when a request names none. */\n\tmaxInterruptBudget?: number;\n\t/** Upper bound for a request's VM memory, and the limit when a request names none. */\n\tmaxMemoryLimitBytes?: number;\n}\n\n/**\n * Remote side of {@link RemoteCodemodeSandbox}: runs one execution's VM on this thread and\n * forwards everything it reports through `exchange`. Resolves once the VM has stopped and the\n * terminal message has been delivered, or the client asked to stop. Never rejects.\n */\nexport function serveCodemodeRemote(\n\trequest: CodemodeRemoteRequest,\n\texchange: CodemodeRemoteExchange,\n\toptions: CodemodeServeOptions,\n): Promise<void> {\n\tconst maxBudget = options.maxInterruptBudget ?? DEFAULT_INTERRUPT_BUDGET;\n\tconst budget = Math.min(request.interruptBudget ?? maxBudget, maxBudget);\n\tconst requestedMemory = request.data.memoryLimitBytes;\n\tconst maxMemory = options.maxMemoryLimitBytes;\n\tconst memoryLimitBytes =\n\t\trequestedMemory === undefined\n\t\t\t? maxMemory\n\t\t\t: maxMemory === undefined\n\t\t\t\t? requestedMemory\n\t\t\t\t: Math.min(requestedMemory, maxMemory);\n\tconst data: VmStartData = { ...request.data, memoryLimitBytes };\n\n\treturn new Promise<void>((resolve) => {\n\t\tlet channel: VmChannel | undefined;\n\t\tlet ended = false;\n\t\tlet buffered: CodemodeRemoteMessage[] = [];\n\t\tlet bufferedChars = 0;\n\t\t/** Replies that arrived before the launcher returned the channel. */\n\t\tconst unsent: HostToWorkerMessage[] = [];\n\n\t\tconst takeBuffer = (): CodemodeRemoteMessage[] => {\n\t\t\tconst batch = buffered;\n\t\t\tbuffered = [];\n\t\t\tbufferedChars = 0;\n\t\t\treturn batch;\n\t\t};\n\t\tconst end = (terminal: CodemodeRemoteMessage | undefined): void => {\n\t\t\tif (ended) return;\n\t\t\tended = true;\n\t\t\tconst batch = takeBuffer();\n\t\t\tconst delivered = terminal ? exchange([...batch, terminal]).catch(() => null) : Promise.resolve();\n\t\t\tvoid delivered\n\t\t\t\t.then(() => channel?.stop())\n\t\t\t\t.catch(() => undefined)\n\t\t\t\t.then(() => resolve());\n\t\t};\n\t\tconst reply = (message: CodemodeRemoteReply): void => {\n\t\t\tif (ended || message === null) return;\n\t\t\tif (message.type === \"stop\") {\n\t\t\t\tend(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (channel) channel.post(message);\n\t\t\telse unsent.push(message);\n\t\t};\n\t\tconst onMessage = (message: unknown): void => {\n\t\t\tif (ended) return;\n\t\t\tconst typed = message as WorkerToHostMessage;\n\t\t\tswitch (typed.type) {\n\t\t\t\tcase \"output\": {\n\t\t\t\t\tbuffered.push(typed);\n\t\t\t\t\tbufferedChars += typed.item.type === \"text\" ? typed.item.text.length : typed.item.data.length;\n\t\t\t\t\tif (buffered.length >= FLUSH_ITEMS || bufferedChars >= FLUSH_CHARS) {\n\t\t\t\t\t\texchange(takeBuffer()).then(reply, () => end(undefined));\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"started\":\n\t\t\t\t\tbuffered.push(typed);\n\t\t\t\t\tbreak;\n\t\t\t\t// Sent at once: a host replaying a script waits for it to see that the script issued no further call.\n\t\t\t\tcase \"call\":\n\t\t\t\tcase \"idle\":\n\t\t\t\t\texchange([...takeBuffer(), typed]).then(reply, () => end(undefined));\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tend(typed);\n\t\t\t}\n\t\t};\n\n\t\tconst launch = inlineLauncher(options.wasm, budget);\n\t\tPromise.resolve(\n\t\t\tlaunch(data, {\n\t\t\t\tmessage: onMessage,\n\t\t\t\tfailure: (error) => end({ type: \"failure\", error }),\n\t\t\t}),\n\t\t).then(\n\t\t\t(started) => {\n\t\t\t\tchannel = started;\n\t\t\t\tif (ended) {\n\t\t\t\t\tvoid started.stop().catch(() => undefined);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const message of unsent.splice(0)) started.post(message);\n\t\t\t},\n\t\t\t(error: unknown) => end({ type: \"failure\", error: { kind: \"sandbox\", message: errorMessage(error) } }),\n\t\t);\n\t});\n}\n\nfunction remoteLauncher(remote: () => CodemodeRemote, interruptBudget: number | undefined): VmLauncher {\n\treturn (data, events) => {\n\t\tlet stopped = false;\n\t\tlet settled = false;\n\t\tconst waiting = new Map<number, (reply: CodemodeRemoteReply) => void>();\n\n\t\tconst exchange: CodemodeRemoteExchange = (messages) =>\n\t\t\tnew Promise<CodemodeRemoteReply>((resolve) => {\n\t\t\t\tif (stopped || !Array.isArray(messages)) {\n\t\t\t\t\tresolve({ type: \"stop\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Register the waiter first: the host may answer a call synchronously, for example when the\n\t\t\t\t// tool throws before returning a promise.\n\t\t\t\tconst call = messages.find((message) => isWorkerToHostMessage(message) && message.type === \"call\");\n\t\t\t\tif (call?.type === \"call\") waiting.set(call.id, resolve);\n\t\t\t\telse resolve(null);\n\t\t\t\tfor (const message of messages) {\n\t\t\t\t\tif (typeof message === \"object\" && message !== null && message.type === \"failure\") {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tevents.failure(message.error);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t// Malformed messages still go to the host, which fails the execution as a broken bridge.\n\t\t\t\t\tevents.message(message);\n\t\t\t\t\tif (isWorkerToHostMessage(message) && (message.type === \"done\" || message.type === \"crash\"))\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t}\n\t\t\t});\n\n\t\tlet target: CodemodeRemote;\n\t\ttry {\n\t\t\ttarget = remote();\n\t\t} catch (error) {\n\t\t\tthrow new Error(`Failed to reach the remote sandbox: ${errorMessage(error)}`);\n\t\t}\n\t\tconst request: CodemodeRemoteRequest = interruptBudget === undefined ? { data } : { data, interruptBudget };\n\t\tPromise.resolve()\n\t\t\t.then(() => target.execute(request, exchange))\n\t\t\t.then(\n\t\t\t\t() => {\n\t\t\t\t\tif (!settled && !stopped) {\n\t\t\t\t\t\tevents.failure({ kind: \"sandbox\", message: \"Remote sandbox returned before the script settled\" });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t(error: unknown) => {\n\t\t\t\t\tif (!stopped)\n\t\t\t\t\t\tevents.failure({ kind: \"sandbox\", message: `Remote sandbox failed: ${errorMessage(error)}` });\n\t\t\t\t},\n\t\t\t);\n\n\t\treturn {\n\t\t\tpost: (message) => {\n\t\t\t\tconst resolve = waiting.get(message.id);\n\t\t\t\tif (!resolve) return;\n\t\t\t\twaiting.delete(message.id);\n\t\t\t\tresolve(message);\n\t\t\t},\n\t\t\tstop: async () => {\n\t\t\t\tstopped = true;\n\t\t\t\tfor (const resolve of waiting.values()) resolve({ type: \"stop\" });\n\t\t\t\twaiting.clear();\n\t\t\t},\n\t\t};\n\t};\n}\n\n/**\n * Runs each execution's VM on a remote, usually a Cloudflare Durable Object deployed with\n * `CodemodeSandboxDurableObject`, while tools run here. Same API and results as\n * `CodemodeSandbox`, so it can replace it. Differences: a script that computes without calling a\n * tool is limited by the remote's interrupt budget rather than `timeoutMs`, and after a deadline or\n * abort the remote VM keeps computing until it next calls a tool or uses up that budget.\n */\nexport class RemoteCodemodeSandbox extends CodemodeSandboxBase {\n\tconstructor(options: RemoteCodemodeSandboxOptions) {\n\t\tsuper(options, remoteLauncher(options.remote, options.interruptBudget));\n\t}\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CodemodeWasmModule } from "../wasm.ts";
|
|
2
|
+
import type { HostToWorkerMessage, VmStartData, WorkerToHostMessage } from "./protocol.ts";
|
|
3
|
+
export interface VmSession {
|
|
4
|
+
deliver(message: HostToWorkerMessage): void;
|
|
5
|
+
/** Releases the VM's wasm instance. The session must not be used afterwards. */
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
export interface StartVmOptions {
|
|
9
|
+
data: VmStartData;
|
|
10
|
+
wasm: CodemodeWasmModule;
|
|
11
|
+
/**
|
|
12
|
+
* QuickJS's guard on its own stack, which turns deep recursion into a catchable `RangeError`.
|
|
13
|
+
* The wasm frames also use the host thread's native stack, so the guard must trip before that
|
|
14
|
+
* stack runs out: a host overflow fails the whole execution instead. Default: `MAX_STACK_SIZE`
|
|
15
|
+
* (512 KiB), which fits the 4 MB stack of a Node worker thread but not a 1 MB main thread.
|
|
16
|
+
*/
|
|
17
|
+
maxStackSize?: number;
|
|
18
|
+
/** Interrupt polls this VM may use, if the launcher counts them; part of the fingerprint. */
|
|
19
|
+
interruptBudget?: number;
|
|
20
|
+
post(message: WorkerToHostMessage): void;
|
|
21
|
+
shouldInterrupt(): boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function startVm(options: StartVmOptions): Promise<VmSession>;
|
|
24
|
+
//# sourceMappingURL=vm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vm.d.ts","sourceRoot":"","sources":["../../src/runtime/vm.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE3F,MAAM,WAAW,SAAS;IACzB,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5C,gFAAgF;IAChF,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACzC,eAAe,IAAI,OAAO,CAAC;CAC3B;AAiGD,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAqLzE"}
|