API
Three packages publish from this repository, and they release in lock step. nx.json leaves release.projectsRelationship at Nx’s fixed setting, so one version number covers all three and there is no combination of versions to test. Each job below names where a host imports from and which export it starts at:
| Job | Import from | Entry point |
|---|---|---|
| Run a program in this process | @supolka/codemode-executor |
IsolateExecutor |
| Run a program in a daemon over the network | @supolka/cloudflare-codemode-executor |
connect |
| Serve the protocol to other processes | @supolka/codemode-executor/server |
createExecutorServer |
| Handle the tool calls a program makes | @supolka/codemode-executor-protocol |
createEnvelopeDispatch |
Replace DynamicWorkerExecutor in a @cloudflare/codemode host |
@supolka/cloudflare-codemode-executor |
CodemodeExecutor |
| Implement the contract on another runtime | @supolka/codemode-executor-protocol |
Executor and ExecutorApi |
Executor protocol defines the contract those entry points implement, and Error codes and texts says what each one can fail with. No lane of pnpm verify reads this page against the packages, so every listing below is transcribed and not derived.
@supolka/codemode-executor-protocol
Section titled “@supolka/codemode-executor-protocol”This package is the contract both sides implement. It declares no dependency and names no runtime API, so it compiles wherever a host runs, and every export leaves through the package root. These types describe one execution, what comes back from it, and the Executor interface itself:
| Export | What it is |
|---|---|
Executor |
The in-language contract: execute, invokeHandle, closeHandles, dispose. Both shipped executors implement it. |
ExecuteRequest |
One execution stated in full: code, providers, dispatch, modules, timeout, countHostWaits, outbound, retainHandles. |
ExecuteResult |
What comes back: result, error, logs, and executionId with handles when a run retained them. |
ExecuteResultMessage |
The same envelope on the wire, where the run’s own value is resultJson, the JSON text the isolate produced. |
ExecuteCallOptions |
The options half of the wire execute: timeout, countHostWaits, modules and retainHandles. |
ProviderDescriptor |
A provider as the sandbox is told about it: name, tools, and the optional positionalArgs, prelude and proxySource. |
ModuleMap |
The names a program may import, each a ModuleSource. |
ModuleSource |
One module: a source string, or an entry keyed js, cjs, json, text or data. |
ModuleEntry |
The wire form of one map entry, the pair [name, source]. |
ToolDispatchFn |
(provider, tool, argsJson) => Promise<string>, the function a host implements for tool calls. It returns the envelope JSON. |
OutboundFetchFn |
(request) => Promise<response>, the function that performs sandbox fetch when the caller supplies one. |
OutboundRequestMessage, OutboundResponseMessage |
A request and a response as plain data, used by outbound requests and by handle invocations. |
OutboundMode |
The wire form of the outbound setting: native, stub or disabled. |
LogLine |
A captured console line: its level and its text. |
Constants below are the contract’s numbers and its pinned texts, and a host that inlines one keeps a copy no release updates:
| Export | Value | What it governs |
|---|---|---|
PROTOCOL_VERSION |
1 |
The handshake, where the two versions must be exactly equal. |
DEFAULT_TIMEOUT |
60000 |
The run bound when neither the request nor the server names one, one minute. |
MAX_SUPPORTED_TIMEOUT |
2147483647 |
The widest delay a 32-bit timer accepts, close to 25 days. A null timeout lifts the bound to the largest value the serving side supports, never above this. |
PAYLOAD_LIMIT |
8388608 |
The largest single value one execution may send outward, 8 × 1024 × 1024 characters. |
CONSOLE_CAPTURE_LIMIT |
2097152 |
The captured console text of a whole run, 2 × 1024 × 1024 characters. |
TIMEOUT_MESSAGE |
Execution timed out |
The phrase the codemode dialect pins. |
EXECUTION_COMPLETED_MESSAGE |
Execution has already completed. |
The error text of a tool call that arrives after the run finished. |
CONSOLE_CAPTURE_MESSAGE |
Composed from CONSOLE_CAPTURE_LIMIT |
The final warn line of a run that filled its console limit, quoted on Error codes and texts. |
One codec turns a value into the single JSON string of the dispatch grammar, binary included:
| Export | What it does |
|---|---|
createEnvelopeDispatch(state, respond) |
Wraps a plain responder into a ToolDispatchFn. It serializes {result} from what the responder returns and {error} from what it throws, and once state.active is false it returns {error} with EXECUTION_COMPLETED_MESSAGE without calling the responder at all. |
parseDispatchArguments(argsJson) |
Turns the arguments string into an argument list, where the empty string is no arguments and a non-array payload is one argument. |
stringifyForDispatch(value), parseForDispatch(json) |
The codec itself, tagging a Uint8Array, an ArrayBuffer or a typed array as base64. |
decodeDispatchValue(value), DecodedDispatchValue<T> |
Rebuilds binary inside a value that already crossed as data, and the type that mirrors it. |
ExecutorError, ExecutorErrorCode, rehydrateExecutorError, describeFailure, the PayloadPart union and one class per declared failure make up the failure surface. Error codes and texts enumerates every code and every text. Exports below are for whoever implements the contract, not for a host calling an executor that already does:
| Export | What it does |
|---|---|
ExecutorApi |
The RPC surface a server implements over the WebSocket session. |
assertProtocolCompatible(remoteVersion, localVersion) |
Throws PROTOCOL_MISMATCH unless the two version numbers are equal. localVersion defaults to PROTOCOL_VERSION. |
assertOutboundFetch(outbound) |
Throws PROTOCOL_MISMATCH when the stub outbound mode arrived without its function. |
validateProviders(providers) |
Tests every provider name against RESERVED_PROVIDER_NAMES, PROVIDER_NAME_PATTERN and JAVASCRIPT_RESERVED_WORDS, then against the names already seen, and returns the first failure text or null. Error codes and texts quotes each one. |
RESERVED_PROVIDER_NAMES, JAVASCRIPT_RESERVED_WORDS, PROVIDER_NAME_PATTERN |
Two name sets and one pattern, the rules validateProviders tests against. A host that builds provider names out of user data tests them against these instead of against the failure wording. |
listModuleEntries(modules) |
Turns a ModuleMap into the ModuleEntry pairs the wire sends, rendering a json source through renderJsonModule first. |
renderJsonModule(value) |
Renders an object as the module source export default <json>;. |
separateImports(code) |
Splits the leading import block off an entry and returns { imports, body }. The adapter reuses it when it normalizes model output. |
Most hosts install this package for one job: handling the tool calls a program makes. createEnvelopeDispatch with parseDispatchArguments covers it, and the NestJS example is written that way:
import { createEnvelopeDispatch, parseDispatchArguments } from '@supolka/codemode-executor-protocol'
const state = { active: true }
try { return await executor.execute({ code, providers: PROVIDERS, outbound: null, dispatch: createEnvelopeDispatch(state, async (provider, tool, argsJson) => callTool(provider, tool, parseDispatchArguments(argsJson)), ), })} finally { state.active = false}In the finally block state.active goes false, and every tool call after that point gets back {"error":"Execution has already completed."} without reaching callTool.
@supolka/codemode-executor
Section titled “@supolka/codemode-executor”This package is the Node core. Its root export runs executions, and each subpath exposes one layer beneath it, for a host building on a part instead of on the whole.
Root export
Section titled “Root export”IsolateExecutor runs programs with no daemon between the host and the run: the pool it owns forks the children beside the calling process. Constructing it spawns nothing. The first execution starts the pool, and dispose ends the children:
import { IsolateExecutor } from '@supolka/codemode-executor'
const executor = new IsolateExecutor({ timeout: 10_000, memoryLimit: 128 })
const { result, error, logs } = await executor.execute({ code: 'async () => { const users = await store.listUsers(); return users.length }', providers: [{ name: 'store', tools: ['listUsers'] }], dispatch, outbound: null,})
await executor.dispose()timeout: 10_000 and memoryLimit: 128 are construction-time defaults, and every field of IsolateExecutorOptions works that way. A request field wins over the default for timeout, countHostWaits, modules and outbound. memoryLimit and pool are fixed when the executor is built, and no request moves them.
| Option | Default | Declared in |
|---|---|---|
timeout |
60000 ms, and null lifts the bound to MAX_SUPPORTED_TIMEOUT |
packages/protocol/src/protocol.ts |
memoryLimit |
128, in megabytes |
packages/executor/src/executor/defaults.ts |
countHostWaits |
false, and a wait inside the caller’s own function then stays outside the bound |
packages/executor/src/executor/isolate-executor.ts |
outbound |
Absent, and the executor performs requests natively; null closes the network |
packages/executor/src/executor/isolate-executor.ts |
modules |
Absent | packages/executor/src/executor/isolate-executor.ts |
pool |
The PoolOptions defaults below |
packages/executor/src/pool/pool.ts |
DEFAULT_GRACE is the grace period after a run’s bound, one second, declared in packages/executor/src/executor/defaults.ts. IsolateExecutor passes that value on every request and takes no option for it. Hosts that need another value set grace on the request they hand to WorkerPool or Sandbox, or set executor.defaults.grace on a server.
Beside the executor, the root exports a result limiter and the protocol declarations a host branches on:
| Export | What it does |
|---|---|
IsolateExecutor, IsolateExecutorOptions |
The executor above and its construction options. |
limitExecutionResultValue(value, responseLimit) |
Measures a value in UTF-8 bytes and returns an IntactExecutionResult at or under the limit, a TruncatedExecutionResult above it. It caps what a host displays, not what the executor allows. |
formatLimitedExecutionOutput(result) |
Returns the display text, and appends a --- TRUNCATED --- marker with the note where the value was cut. |
IntactExecutionResult, TruncatedExecutionResult, LimitedExecutionResult |
The two results and their union. |
Logger, PoolOptions |
The pool’s logger type and its option type, re-exported from ./pool. |
ExecutorError, ExecutorErrorCode, rehydrateExecutorError, MAX_SUPPORTED_TIMEOUT, ExecuteRequest, ExecuteResult, Executor |
Re-exported from the protocol package, so a host that installs the core alone still branches on codes. |
./server subpath
Section titled “./server subpath”createExecutorServer(options) returns a ServerHandle that owns a pool and speaks the protocol over WebSocket. The daemon is this function plus configuration:
import { createExecutorServer } from '@supolka/codemode-executor/server'
const server = createExecutorServer({ port: 8080, responseLimit: 8 * 1024 * 1024, executor: { pool: { maxWorkers: 4 }, defaults: { maxTimeout: 120_000 } },})
await server.listen()listen() binds the port and close() releases it. The same handle has address, stopAccepting, resumeAccepting, the accepting flag and the pool itself. Turning acceptance off is the drain: open connections keep working and their executions keep running, but the next caller is turned away and /readyz says so. A deployment sets the rest through ServerOptions. Daemons map their environment onto this table, and Deploying the daemon documents that mapping setting by setting.
| Option | Default | What it decides |
|---|---|---|
host, port |
0.0.0.0 and 8080 |
Where the listener binds. |
socketPath |
Absent | A unix socket instead of a host and port. |
onConnection |
Absent, and every caller is then admitted as anonymous |
The identity on the handshake, or a rejection. It reads a ConnectionHandshake and returns a ConnectionDecision or a promise for one, and a hook that throws rejects the connection. |
executor.pool |
The PoolOptions defaults |
The pool behind the server. |
executor.outboundMode |
Absent | The operator’s veto over the native mode. The string 'null' leaves a request that asked for native fetching with no network at all. |
executor.defaults |
timeout 60000 ms, grace 1000 ms and memoryLimit 128 MB, applied in packages/executor/src/server/bridge.ts |
The bounds a request falls back to. maxTimeout caps every requested timeout, and with none set the cap is MAX_SUPPORTED_TIMEOUT. |
responseLimit |
null, and the protocol’s own bound is then the only cap |
The bytes of one serialized result envelope, and of one handle invocation’s answer, headers serialized and body counted whole. |
maxConnectionAge |
Absent | How long one connection may stay open before the server closes it. |
pingInterval |
30000 |
How often the server pings a connection to check that it is still there, every thirty seconds. |
shutdownDelay |
0 |
How long close keeps serving after readiness flips. |
shutdownDrain |
0 |
How long close waits for the executions still running once the delay has passed. |
metrics |
Absent | A MetricsSource with a contentType and a render(). Without one, /metrics responds 404. |
instrumented |
true |
Whether the connection metrics and the request span are built. |
logger |
Absent | Where the server writes. |
runBridgedExecution is the leg between an arriving RPC call and the pool, and it owns the provider-name check, the response limit and the stub lifetimes. A transport built on it inherits all three without writing any. BridgeRequest, ExecutionDefaults and ServerOutboundMode are its types, exported for whoever serves this protocol over a transport of its own.
./pool subpath
Section titled “./pool subpath”WorkerPool is the supervisor under both executors. Hosts import it directly to run a protocol of their own on top, or to subscribe to what the pool publishes. Its surface is execute, invokeHandle, closeHandles, hasHandles, shutdown, the queueFull flag and the events emitter, and Telemetry lists every event and its payload. PoolOptions shapes the deployment, and packages/executor/src/pool/pool.ts declares every default below:
| Option | Default | What it decides |
|---|---|---|
maxWorkers |
availableParallelism() |
How many child processes run at once. |
executionsPerWorker |
1 |
How many executions one child runs at a time. |
maxQueue |
100 |
How many executions may wait before QUEUE_OVERFLOW. |
retireAfterExecutions |
50 |
How many executions a worker runs before it is replaced. |
workerMemoryLimit |
536870912 |
The process memory, in bytes, past which a worker is retired, 512 MiB. |
terminationDelay |
5000 |
How long past its timeout and grace a hung execution’s worker survives before SIGKILL, five seconds. |
handlesIdleTimeout |
300000 |
How long a retained execution survives with no invocation, five minutes. |
childEntryPath |
child-entry.js beside the pool |
Which module a worker forks. |
childExecArgv |
['--no-node-snapshot'] |
The flags a child runs under. isolated-vm requires this one on every Node this package supports. |
childEnv, childCwd |
process.env and the parent’s directory |
The environment a child inherits. |
instrumented |
true |
Whether the pool builds instruments and traces. |
logger |
Absent | Where the pool writes. |
Every payload PoolEvents names is exported here, so a subscriber has a name for what its handler receives: ExecutionStartEvent, ExecutionSettleEvent, ExecutionRejectEvent, WorkerSpawnEvent, WorkerCrashEvent, WorkerRetireEvent, HandlesCloseEvent and QueueDepthEvent, along with PoolEvents itself, HandlesCloseReason, RetireReason and PoolExecuteRequest. packages/executor/src/pool/index.spec.ts compares each payload against the event map by type, and it stops compiling when one of them stops being exported from this subpath. ExecutorError and ExecutorErrorCode are re-exported here as well. ShutdownOptions is exported here too, the two numbers shutdown reads. drainTimeout is how long it waits for the executions and handle invocations still in flight, and at 0 it signals the children with no wait at all. grace is the gap between the SIGTERM each child gets and the SIGKILL that follows, and it falls back to the DEFAULT_SHUTDOWN_GRACE this subpath exports, 1000 ms.
./sandbox subpath
Section titled “./sandbox subpath”Sandbox is one isolate per execution with no process supervision around it, for an embedder that supervises its own children. Hosts that need the crash boundary of a child process use WorkerPool or IsolateExecutor instead. V8 still enforces the memory limit here, but a guest that takes the whole process down takes this one with it. Composing a sandbox reads the runtime snapshot, so a process that composes one at startup has it before the first request arrives. The pool’s child composes exactly one, at startup.
import { Sandbox } from '@supolka/codemode-executor/sandbox'
const sandbox = new Sandbox({ // Shared across executions, so a module compiled once is not compiled again. cachedDataStore: new Map(), hooks: { onExecutionSettled: ({ duration, outcome }) => record(duration, outcome) },})
const answer = await sandbox.execute({ code: 'async () => 6 * 7', providers: [], timeout: 30_000, grace: 1_000, memoryLimit: 128, outbound: null,})cachedDataStore and hooks are both optional, though the options object itself is not, so the bare form is new Sandbox({}). SandboxExecuteRequest defaults no field for the caller, because this layer has no configuration of its own to fall back on:
| Member | What it sets |
|---|---|
code |
Required. The entry form: an arrow expression, optionally preceded by static imports of module-map entries. |
providers |
Required. The provider descriptors, and [] where a run has none. |
timeout, grace, memoryLimit |
Required. timeout and grace in milliseconds, memoryLimit in megabytes. |
outbound |
Required. A function performs the request, null closes the network, and undefined lets the executor perform it. |
modules, countHostWaits, retainHandles |
Optional, and each reads as it reads on an ExecuteRequest. |
dispatch |
Optional. A ToolDispatchFn handles tool calls, and null or an absent function sends every call the not-found envelope instead. |
onHostWaitChanged |
Optional. The run clock calls it with true when the run starts waiting on the host and with false when guest code owns it again, and a throw from it is discarded. The pool’s child forwards both edges so the layer above pauses with the run. |
execute returns an ExecuteResult and never rejects for a failure in user code, so a syntax error, a bad provider list, a resolution failure, a timeout and a user throw all arrive as error on a resolved promise. invokeHandle calls one handle of a retained execution, closeHandles releases that execution, and hasHandles returns whether an execution still holds any. SandboxHooks.onExecutionSettled receives a SandboxExecutionSettledEvent carrying the duration and the outcome, synchronously, and a throw from it neither fails the run nor fires the hook a second time.
@supolka/cloudflare-codemode-executor
Section titled “@supolka/cloudflare-codemode-executor”This package ships the dialect adapter and the remote executor, in JavaScript that names no runtime API, so any JavaScript runtime can host it. The testbed proves that by running the client inside workerd with nodejs_compat off. connect(url, options) opens a session to an executor server and resolves to a RemoteExecutor, an object that implements the same Executor interface IsolateExecutor implements:
import { CodemodeExecutor, connect } from '@supolka/cloudflare-codemode-executor'
const executor = new CodemodeExecutor(await connect('ws://executor:8080'))The second argument of connect is ConnectOptions, and packages/cloudflare-codemode/src/remote-executor.ts declares every default below:
| Option | Default | What it decides |
|---|---|---|
websocket |
Absent, and capnweb then builds the socket from the runtime’s own WebSocket |
The constructor the session builds its socket from. |
headers |
Absent | The handshake headers. Passing them without websocket makes connect reject with CONNECTION_REJECTED, because the standard constructor cannot send them. |
rotateAfterExecutions |
100 |
How many executions one session runs before it is retired and reopened. |
reconnect |
{ retries: 3, backoff: 250 }, and false disables it |
How a call that finds no live session reopens one, backing off 250 ms, then 500, then 750. A version mismatch never retries. |
timeout, countHostWaits, modules, outbound |
Absent | The per-connection defaults a request field overrides. |
Retention has no connection-time default on purpose. retainHandles rides the request alone, because keeping the isolate is tied to reading the granted executionId back, and a connection-wide flag would grant handles to callers who never asked to hold one.
CodemodeExecutor wraps any native executor into the @cloudflare/codemode dialect, in process or remote without knowing which. Everything the dialect pins is its responsibility: the raw model output cleanup, the reserved names, the tool name sanitization, the log prefixes, the connector control markers, and rendering the native timeout text back down to the one sentence the dialect expects. ExtendedExecuteOptions adds modules, timeout and outbound per call, so a host that bundles a fresh module graph per execution keeps one warm executor. ExtendedProvider adds positionalArgs and proxySource to a dialect provider. Replacing DynamicWorkerExecutor says what the move costs a migrating host.
This package re-exports ExecutorError, rehydrateExecutorError, ExecuteRequest, ExecuteResult, Executor, ExecutorErrorCode, LogLine, ModuleMap and OutboundFetchFn from the protocol package, so a host on another runtime installs this one alone. The adapter’s own functions are exported from the root as well, for a host that extends the adapter, not one that only calls it:
| Export | What it does |
|---|---|
normalizeCode(code) |
Turns whatever a model emitted into the entry form. |
sanitizeToolName(name) |
Applies the dialect’s name rule to one tool name: a hyphen, a dot or whitespace becomes _, anything else outside [a-zA-Z0-9_$] is dropped, a leading digit takes a leading _, a reserved word takes a trailing _, and an empty result is _ on its own. |
detectSanitizeCollision(providers) |
Returns the collision text for the first pair of tool names inside one provider that sanitize to the same identifier, and null where no pair does. |
formatSanitizeCollision(providerName, firstTool, secondTool, sanitized) |
Composes that text. @cloudflare/codemode pins its wording, so a migrated host keeps reading the same sentence. |
planExecution(providers, connectors) |
Turns dialect providers and connectors into an ExecutionPlan: the ProviderDescriptor list that crosses the wire, and the ToolHandler map that keeps the functions in this process. |
validateExecution(providers, connectors) |
Returns the dialect’s own rejection text for a declaration it refuses, or null. |
ExecutionPlan, ToolHandler |
The plan and the handler signature. |
RemoteExecutor, WebSocketConstructor |
The class connect resolves to, and the type of the socket constructor the websocket option takes. |