Skip to content

Repository files navigation

@torkbot/code-mode

Run agent-authored TypeScript ESM against host-controlled tools on a pluggable, long-lived JavaScript runtime.

Code mode owns the complete program contract: whether per-execution input exists, its type when declared, tool declarations, checking, TypeScript erasure, host-side schema validation, tool routing, telemetry, and its internal wire protocol. Runtime drivers own where programs execute, how modules resolve, how the runner is booted, and how strongly execution can be isolated or cancelled.

Install

npm install @torkbot/code-mode

The library requires Node.js 24 or newer on the host. Agent programs can run in any environment with a conforming runtime driver.

Quick start

import {
  createClient,
  createProgramContract,
  defineTool,
} from "@torkbot/code-mode";
import { createHostNodeRuntime } from "@torkbot/code-mode/host-node";

const contract = createProgramContract({
  inputSchema: WeatherRequested,
  tools: [
    defineTool(
      "getWeather",
      {
        description: "Get weather for a location.",
        inputSchema: WeatherInput,
        outputSchema: WeatherReport,
      },
      async ({ signal }, input) => {
        signal.throwIfAborted();
        return weatherService.get(input.location, { signal });
      },
    ),
  ],
});

const runtime = await createHostNodeRuntime(
  {
    nodePath: process.execPath,
    cwd: process.cwd(),
  },
  AbortSignal.timeout(5_000),
);

try {
  const client = createClient({ runtime, contract });
  const source = `
import { inspect } from "node:util";

export default async function ({ input, codemode, console }: AgentProgramScope) {
  const weather = await codemode.getWeather({ location: input.location });
  console.log(inspect(weather));
}
`.trimStart();

  const validation = await client.validate(
    source,
    AbortSignal.timeout(5_000),
  );
  if (validation.kind === "invalid") {
    throw new Error(validation.report);
  }

  const outcome = await client.run(source, {
    input: {
      type: "weather.requested",
      location: "London",
    },
    signal: AbortSignal.timeout(30_000),
    onTelemetry(event) {
      if (event.kind === "program-output") {
        process[event.stream].write(event.text);
      }
    },
  });
} finally {
  await runtime[Symbol.asyncDispose]();
}

createHostNodeRuntime() starts one Node.js process and returns after its runner is ready. Every subsequent client.run() is an execution request on that live connection. Dispose the runtime when its owner is done with it.

Program contract

A program contract has one of two explicit forms:

const inputless = createProgramContract({
  tools,
});

const inputBearing = createProgramContract({
  inputSchema: EventInput,
  tools,
});

inputSchema is not an optional value with a fallback. Its presence selects an input-bearing contract; its absence selects an inputless contract. Accordingly, client.run(source, { signal }) is the complete inputless execution call, while an input-bearing client requires client.run(source, { input: schemaInput, signal }).

A submitted program is a real ECMAScript module. It may use static imports and must have a callable default export assignable to its generated contract. An input-bearing contract generates:

interface AgentProgramScope {
  readonly input: {
    readonly type: "weather.requested";
    readonly location: string;
  };
  readonly codemode: Tools;
  readonly console: CodeModeConsole;
}

type AgentProgram = (scope: AgentProgramScope) => unknown;

An inputless contract instead generates:

interface AgentProgramScope {
  readonly codemode: Tools;
  readonly console: CodeModeConsole;
}

The runner evaluates a fresh root module for every execution. It calls the default export with { input, codemode, console } for an input-bearing execution and { codemode, console } for an inputless execution, awaits it, and ignores its fulfilled value. Declared input is the host-validated, transformed value for that execution. There is no continuity between root modules; a runtime may still use its platform's normal cache for imported dependencies.

Only the console passed in AgentProgramScope is captured. Calls to an ambient global console or an imported console are outside the runtime contract. The captured console has this minimum surface:

interface CodeModeConsole {
  debug(...values: unknown[]): void;
  error(...values: unknown[]): void;
  info(...values: unknown[]): void;
  log(...values: unknown[]): void;
  warn(...values: unknown[]): void;
}

Runtimes format console arguments. Code mode only promises text chunks and their provenance:

type ProgramOutput = {
  readonly stream: "stdout" | "stderr";
  readonly text: string;
};

The consumer is expected to interpret text. There is no structured-value, circular-reference, or console-formatting contract.

validate() checks the submitted module itself. run() uses Amaro's strip-only transform to erase TypeScript in place, with no parser-owned wrapper and no prepended source. Runtime stacks therefore retain submitted line and column coordinates.

Schemas, tools, and checking

A declared program input schema and every tool input/output schema implement both StandardSchemaV1 and StandardJSONSchemaV1:

type CodeModeSchema<Input, Output> =
  & StandardSchemaV1<Input, Output>
  & StandardJSONSchemaV1<Input, Output>;

Standard Schema validates and transforms values on the host. Standard JSON Schema generates exact agent-facing declarations. Transforming schemas have six distinct type positions:

Boundary Type
Host supplies program input SchemaInput<ProgramInputSchema>
Program receives validated input SchemaOutput<ProgramInputSchema>
Program supplies tool input SchemaInput<InputSchema>
Handler receives validated input SchemaOutput<InputSchema>
Handler returns an output candidate SchemaInput<OutputSchema>
Program receives validated output SchemaOutput<OutputSchema>

Tool names must be unique JavaScript identifiers. then and the inherited Object property names are reserved. Descriptions must be non-empty because they become user-facing JSDoc in generated declarations.

The declaration printer supports closed objects, arrays, strings, numbers, integers, booleans, and null. String schemas may use const to emit a string literal type. Object schemas must explicitly set additionalProperties: false.

anyOf and oneOf support one deliberately narrow form: at least two closed object branches must share a required string-const property, and each branch must give that property a distinct value. The result is emitted as a discriminated TypeScript union. The distinct required values also make the branches mutually exclusive, so a TypeScript union preserves oneOf semantics. Other composition shapes and unsupported schema keywords fail instead of being represented loosely.

contract.typeDefinitions contains the complete program declaration surface: the input declaration when one exists, tools, console, and default-export contract. Program input declarations use the schema's output JSON Schema because that is the value supplied after host validation. Runtime.loadTypeDefinitionFiles() supplies checker-only declarations for the execution environment. Those files are mounted in an in-memory TypeScript project and are never sent as part of an execution request.

This remains honest for contracts built dynamically. Host TypeScript may know a ledger-built schema only as CodeModeSchema<unknown, unknown>, while its concrete output JSON Schema still generates a strong discriminated union for the submitted program.

validate() has no runtime side effects. run() does not implicitly typecheck. For an input-bearing contract, it validates and transforms program input before submitting an execution; invalid or non-JSON transformed input never reaches the runtime. Inputless contracts perform no input validation and submit no input field. Tool inputs and outputs are also validated on the host. Non-JSON tool values fail that program without closing the shared runtime.

Outcomes, cancellation, and telemetry

type RunOutcome =
  | { readonly kind: "success" }
  | {
      readonly kind: "program-failed";
      readonly error: TelemetryError;
    };

Module evaluation errors, a non-callable default export, thrown or rejected programs, unknown tools, handler failures, and tool schema failures resolve as program-failed. The default export's fulfilled value never affects the outcome. Transport failures and execution cancellation reject the promise. For an input-bearing contract, invalid host-supplied program input also rejects: it is an embedder failure, not a failure caused by the submitted program. Its execution-failed telemetry has program-input-validation details with a bounded schema report.

An execution signal governs only that execution and its active tool handlers. It does not close the runtime or cancel other multiplexed executions.

Telemetry reports:

  • program-output with stream and text;
  • tool call start, completion, and failure;
  • terminal execution completion or infrastructure failure.

Telemetry callbacks are observational. Their throws and rejected promises do not alter program execution.

Runtime lifecycle

The public runtime used by a client is already connected:

interface Runtime extends AsyncDisposable {
  readonly description: string;
  readonly finished: Promise<RuntimeFinished>;

  loadTypeDefinitionFiles(
    signal: AbortSignal,
  ): Promise<readonly TypeDefinitionFile[]>;

  execute(request: RuntimeExecuteRequest): Promise<RunOutcome>;

  [Symbol.asyncDispose](): Promise<void>;
}

RuntimeExecuteRequest has input-bearing and inputless forms. The former requires a host-validated, JSON-compatible input; the latter forbids the property. The wire message and runner scope preserve that distinction rather than representing absence as undefined, null, or {}.

description is opaque environment context an embedder may present to an agent. finished always resolves when the runtime becomes unusable: closed for an orderly end, or failed with the underlying error. Async disposal closes the connection and waits for driver-owned resources to stop.

Scheduling belongs to the driver. Clients always receive promises; a driver may serialize them. The built-in Host Node driver is fully multiplexed.

Authoring a runtime driver

Driver authors use @torkbot/code-mode/runtime:

interface RuntimeDriver<Options> {
  readonly description: string;

  loadTypeDefinitionFiles(
    signal: AbortSignal,
  ): Promise<readonly TypeDefinitionFile[]>;

  connect(
    options: Options,
    request: {
      readonly runnerSource: string;
      readonly signal: AbortSignal;
    },
  ): Promise<RuntimeConnection>;
}

interface RuntimeConnection extends AsyncDisposable {
  readonly channel: {
    readonly readable: ReadableStream<Uint8Array>;
    readonly writable: WritableStream<Uint8Array>;
  };
  readonly finished: Promise<RuntimeFinished>;
  [Symbol.asyncDispose](): Promise<void>;
}

Expose a user-facing factory by closing over the driver:

import { createRuntimeFactory } from "@torkbot/code-mode/runtime";

const driver: RuntimeDriver<MyOptions> = {
  description: "My runtime",
  loadTypeDefinitionFiles,
  connect,
};

export const createMyRuntime = createRuntimeFactory(driver);

The factory owns the version-matched runner, readiness handshake, internal execution IDs, wire protocol, and failed-boot cleanup. Its signal governs boot through readiness and then detaches. The caller that creates the runtime owns its later lifetime.

connect() boots the execution environment, wires one runner to the returned byte channel, and returns the connection. The driver can use any process, isolate, VM, worker, socket, provider API, or in-memory transport. Agent source, tool calls, output, outcomes, cancellation, and opaque correlation are carried by execution requests after boot; they are not driver options.

The byte framing and messages are internal to this package. Drivers transport bytes; they do not reproduce or interpret the protocol.

Bootstrapping the runner

Platforms that can preinstall the package use the normal ESM export:

import { startRunner } from "@torkbot/code-mode/runner";

await startRunner({
  channel,
  schedule: (execute) => execute(),
  importModule: ({ source, signal }) => importFreshRoot(source, signal),
  createConsole: (emit) => createPlatformConsole(emit),
});

Some runtimes cannot preinstall npm modules. Every connect() request therefore also receives runnerSource: self-contained ESM source exporting the same startRunner() implementation. A driver can evaluate or upload that source and append only its platform glue. The equivalent artifact is exported from @torkbot/code-mode/runner/source for driver tooling and inspection, but normal runtime users never plumb it.

schedule() wraps the complete module-evaluation and default-export invocation. Call its callback immediately to multiplex, or queue callbacks to serialize a runtime. importModule() must evaluate each request as a fresh root ESM module and use the target's native static-import resolution. Its signal provides logical cancellation; runtimes with stronger interruption primitives can apply them there. createConsole() must call emit with text plus stdout or stderr.

Authoring a Node.js 24 driver

Node.js 24 driver authors can reuse code-mode's version-matched declarations and guest execution semantics without adopting the built-in Host Node process lifecycle:

import {
  assertNode24Version,
  createNode24BootstrapSource,
  loadNode24TypeDefinitionFiles,
} from "@torkbot/code-mode/node-runtime";

const driver: RuntimeDriver<MyNodeOptions> = {
  description: "My Node.js 24 runtime",
  loadTypeDefinitionFiles: loadNode24TypeDefinitionFiles,
  async connect(options, { runnerSource, signal }) {
    const version = await readRuntimeNodeVersion(options, signal);
    assertNode24Version(version, "My Node runtime");

    const bootstrapSource = createNode24BootstrapSource({
      runnerSource,
      channelFileDescriptor: 3,
    });
    return launchRuntimeNode(options, bootstrapSource, signal);
  },
};

The generated self-contained ESM attaches to the supplied full-duplex file descriptor, multiplexes executions, evaluates fresh root modules with native Node resolution from process.cwd(), and constructs the captured console with node:console. The driver still owns how the source reaches Node, how the file descriptor is connected, ambient stdio, boot cancellation, and process lifecycle. This keeps Host Node, Sandbox Node, and other Node substrates aligned without importing each other's launch mechanics.

Built-in Host Node driver

import { createHostNodeRuntime } from "@torkbot/code-mode/host-node";

const runtime = await createHostNodeRuntime(
  { nodePath: process.execPath, cwd: process.cwd() },
  bootSignal,
);

The driver requires Node.js 24. It starts one long-lived child process per runtime, connects its runner over fd 3, and multiplexes executions. Programs use native Node ESM resolution rooted at cwd, including ordinary ESM/CJS package interop. The child constructs the captured console with node:console; its ambient stdout, stderr, and global console are not program-output channels.

Host Node is not a sandbox. Use it for trusted programs and conformance tests. Sandbox vendors and @torkbot/code-mode-sandbox should implement their own driver while reusing the standard factory and the Node runtime authoring surface where applicable.

Node retains each unique root module record in its module map until the runtime process is disposed. Imported dependencies may also remain cached. Code mode does not pretend to evict native module records or add process recycling policy; runtime owners should choose a lifecycle appropriate to their workload. To observe the retained-memory profile locally:

npm run benchmark:host-node-memory

The benchmark reports samples and does not impose a universal pass/fail limit.

Runtime conformance

Every driver should run the exported black-box suite:

import { testRuntime } from "@torkbot/code-mode/testing";

testRuntime({
  name: "my runtime",
  createRuntime(signal) {
    return createMyRuntime(requiredOptions, signal);
  },
});

The suite creates and disposes arbitrary runtime instances. It exercises both the public Runtime interface and the public client journey without observing a driver, connection, process, or wire message.

Breaking migration

The complete program contract replaces the tool-only primitive:

  • replace Toolbox with ProgramContract and let the constructor infer its input form;
  • replace createToolbox(tools) with createProgramContract({ tools });
  • add inputSchema only for a genuinely input-bearing program contract;
  • replace ToolSchema with CodeModeSchema;
  • pass contract, not toolbox, to createClient();
  • for input-bearing contracts, supply required input to client.run() and read required input in AgentProgramScope;
  • for inputless contracts, do not pass input; it is absent from AgentProgramScope;
  • runtime authors must preserve whether RuntimeExecuteRequest.input is present when invoking the default export, without wrapping or rewriting submitted source.

About

A runtime-agnostic code-mode library for executing typed agent-authored code against host-provided tools over a bidirectional channel.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages