Skip to content

The published API

Every package here releases in lock step. nx.json leaves release.projectsRelationship at Nx’s fixed setting, so one version number covers all of them and there is no combination of versions to test. Which of them you install follows from what you are trying to do.

The job The package The 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
Serve 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

Symbol by symbol, the executor protocol says what these mean, and Failures says what each can fail with. No lane of pnpm verify reads this page against the packages, so the listings below are transcribed and not derived. But the pool’s event payloads are the exception, and the paragraph that lists them names the test that holds them to the code.

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 below leaves through the package root. Types a caller writes against describe one execution and what comes back from it.

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, carrying 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 that serves a tool call.
OutboundFetchFn (request) => Promise<response>, the function that serves 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, carrying its level and its text.

Constants below are the contract’s numbers and its pinned texts. Importing them is safer than repeating the values, because a host that inlines one is holding a copy that stays at the value it was written with.

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 resolves to it.
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 answer to a tool call that lands after the run settled.
CONSOLE_CAPTURE_MESSAGE Composed from CONSOLE_CAPTURE_LIMIT The last line a run that filled its console budget receives, quoted on Failures.

One codec turns values into the one JSON string the dispatch grammar carries, binary included.

Export What it does
createEnvelopeDispatch(state, respond) Wraps a plain responder into a ToolDispatchFn: it serializes the envelope, answers a late call with the completed text, and turns a rejection into {error}.
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.

The failure surface is ExecutorError, ExecutorErrorCode, rehydrateExecutorError, describeFailure, the PayloadPart union and one class per declared failure. Failures enumerates every code and every text. The rest of the package is for whoever implements the contract, not for a host calling an executor that already does. ExecutorApi is the RPC surface a server serves. assertProtocolCompatible and assertOutboundFetch are the checks a server runs on what arrives: the first refuses a version that is not exactly its own, the second refuses a stub outbound mode that came without its function. validateProviders holds a provider list to RESERVED_PROVIDER_NAMES, PROVIDER_NAME_PATTERN and JAVASCRIPT_RESERVED_WORDS, and Failures says what each of those refuses. separateImports splits the leading import block off an entry, and the adapter reuses it when it normalizes model output.

Most hosts reach this package for one job: serving the tool calls a program makes. createEnvelopeDispatch with parseDispatchArguments is all of 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
}

This package is the Node core. Its root export runs executions, and each subpath under it opens one layer of the machinery below, for a host that wants to build on a part and not on the whole.

Hosts construct IsolateExecutor to run programs in their own 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()

Every field of IsolateExecutorOptions sets a construction-time default. A request field wins over that default for timeout, countHostWaits, modules and outbound. memoryLimit and pool are settled 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, so a wait the caller serves 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 an execution gets after its bound, one second, in packages/executor/src/executor/defaults.ts. But it is not an option of this class. A host that needs a different one sets grace on the request it hands to WorkerPool or Sandbox, or sets executor.defaults.grace on a server.

One more export from the root is a result limiter. It caps what a host shows, not what the executor allows. limitExecutionResultValue(value, responseLimit) answers an IntactExecutionResult or a TruncatedExecutionResult, and formatLimitedExecutionOutput renders either one with a truncation note. ExecutorError, ExecutorErrorCode, rehydrateExecutorError, MAX_SUPPORTED_TIMEOUT, ExecuteRequest, ExecuteResult and Executor are re-exported from the protocol package, so a host that imports the core alone can still branch on codes. Logger and PoolOptions come along for a host that configures the pool from here.

createExecutorServer(options) answers 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()

That handle carries listen, close, 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 ServerOptions. Daemons map their environment onto this table, and the operator guide 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, which admits everyone as anonymous The identity on the handshake, or a rejection. It reads a ConnectionHandshake and answers a ConnectionDecision, 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: null denies the native mode however a caller asked.
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, plus maxTimeout, which caps every requested timeout.
responseLimit null, which adds no cap beyond the protocol’s own bound The bytes one result envelope or handle answer may carry.
maxConnectionAge Absent How long one connection may stay open before the server closes it.
pingInterval 30000 How often a connection is asked whether it is still there, every thirty seconds.
shutdownDelay 0 How long close keeps serving after readiness flips.
metrics Absent A MetricsSource that makes /metrics answer a scrape.
instrumented true Whether the connection metrics and the request span are built.
logger Absent Where the server writes.

runBridgedExecution, BridgeRequest, ExecutionDefaults and ServerOutboundMode are for an implementer serving this protocol over a transport of its own. runBridgedExecution is the leg between an arriving RPC call and the pool. It owns the provider-name check, the response limit and the stub lifetimes, so a transport built on it inherits every one of them without writing any.

WorkerPool is the supervisor under both executors. A host reaches for it directly to run its own protocol 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. Telemetry lists every event and its payload. PoolOptions is where the shape of the deployment lives, and every default below is declared in packages/executor/src/pool/pool.ts.

Option Default What it decides
maxWorkers availableParallelism() How many child processes run at once.
executionsPerWorker 1 How many executions one child serves at a time.
maxQueue 100 How many executions may wait before QUEUE_OVERFLOW.
retireAfterExecutions 50 How many executions a worker serves 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 The package’s own child entry 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. A test holds it to the code. packages/executor/src/pool/index.spec.ts compares each payload against the event map by type, and stops compiling if one of them ever stops being exported from this subpath.

Sandbox is one isolate per execution with no process supervision around it, for an embedder that supervises its own children. Hosts that want the crash boundary of a child process want WorkerPool or IsolateExecutor instead. A memory limit is enforced by the engine and survives 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,
})

Both members of the constructor options are optional, though the object itself is not, so the bare form is new Sandbox({}). SandboxExecuteRequest defaults no field for the caller: code, providers, timeout, grace, memoryLimit and outbound are all required, because this layer holds no configuration of its own to fall back on. Optional members are modules, dispatch, countHostWaits, retainHandles and the host-wait reporter the pool’s child uses to pause the run clock. outbound reads the same three ways it reads everywhere: a function serves the request, null closes the network, and undefined lets the executor perform it. execute answers an ExecuteResult. It returns user-code failures inside the envelope and does not reject for them, 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 and closeHandles serve retained executions, hasHandles answers whether an execution still holds any, and SandboxHooks.onExecutionSettled receives a SandboxExecutionSettledEvent carrying the duration and whether the run succeeded. A hook that throws is swallowed and never fires twice.

This package holds 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 answers a RemoteExecutor. That object serves the same Executor contract IsolateExecutor serves:

import { CodemodeExecutor, connect } from '@supolka/cloudflare-codemode-executor'
const executor = new CodemodeExecutor(await connect('ws://executor:8080'))

ConnectOptions decides how the connection behaves, and its defaults are declared in packages/cloudflare-codemode/src/remote-executor.ts.

Option Default What it decides
websocket The runtime’s own WebSocket The constructor the session builds its socket from. Handshake headers need one passed here.
headers Absent The handshake headers, which require websocket.
rotateAfterExecutions 100 How many executions one session serves 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. Moving a host onto this executor 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 rest of the surface exists for whoever builds on the dialect, not for a host that only calls it. normalizeCode turns whatever a model emitted into the entry form. sanitizeToolName, detectSanitizeCollision and formatSanitizeCollision are the dialect’s tool-name rules and its pinned collision text. planExecution and validateExecution, with ExecutionPlan and ToolHandler, turn dialect providers and connectors into descriptors and handlers. The class RemoteExecutor and the type WebSocketConstructor are exported for a host that names them in its own signatures. Naming them is the only reason to reach past connect.