# nanocode-ts

[gakonst/nanocode-ts](https://github.com/gakonst/nanocode-ts) compiles the nanocode Rust SDK into one WebAssembly module and runs it from TypeScript, with adapters for Vercel Functions and Cloudflare Durable Objects.

## Overview

The module holds the SDK's agent, durability, turn, and event logic for both harnesses. The TypeScript host supplies the platform through a few interfaces: SQLite transactions, model requests, host tools, and remote Code Mode frames.

```mermaid
flowchart LR
  host[TypeScript host] -->|command| module[nanocode_wasm.wasm]
  module -->|store and log| sql[(SqlHost)]
  module -->|http_start| dispatch[dispatch]
  module -->|tool_call| tools[Host tools]
  module -->|events| published[published]
  dispatch --> provider[Provider or gateway]
```

To try it, ask the agent on the [home page](/), which runs the module in the page over an in-memory sqlite-wasm database. [nanocode on Vercel](/vercel) and [nanocode on Cloudflare](/cloudflare/) run it through the two adapters.

> [!NOTE]
> This page describes gakonst/nanocode-ts at commit `e79b665` of 2026-10-20, the revision that this site pins. The [nanocode-wasm](/docs/api/wasm) reference documents the same host under the package's former name.

```ts title="host.ts"
import { readFile } from 'node:fs/promises'
import { AgentRuntime, sessionUuid, type SqlHost } from 'nanocode-ts'

// 1. Compile the module and instantiate it over a SQL host
declare const sql: SqlHost
const module = await WebAssembly.compile(
  await readFile('node_modules/nanocode-ts/dist/nanocode_wasm.wasm'),
)
const runtime = await AgentRuntime.instantiate(module, sql, {
  published: (events) => console.log(events),
})

// 2. Open a Claude session. dispatch adds the API key.
await runtime.open(
  {
    endpoint: 'https://api.anthropic.com/v1/messages',
    provider: 'claude',
    scope: 'owner',
    session_id: sessionUuid(),
    subscription: false,
  },
  {
    dispatch: (request, signal) =>
      fetch(request.endpoint, {
        body: request.body as Uint8Array<ArrayBuffer>,
        headers: [
          ...request.headers,
          ['x-api-key', process.env.ANTHROPIC_API_KEY ?? ''],
        ],
        method: 'POST',
        redirect: 'error',
        signal,
      }),
  },
)

// 3. Queue a turn and drive it
await runtime.submit('owner', {
  metadata: {},
  prompt: 'Say hello.',
  request_id: 'turn-1',
})
await runtime.drive('turn-1')
```

## Install

The package is not on npm. A host depends on it from git at a commit, which the lockfile pins. The repository commits `dist/` with the compiled JavaScript, the declarations, and the module, so an install needs neither Rust nor a TypeScript build.

```json title="package.json"
{
  "dependencies": {
    "nanocode-ts": "github:gakonst/nanocode-ts#e79b665113011d4f8808e867191bd92b9a775c20"
  }
}
```

| Entrypoint                       | Contents                                                             |
| -------------------------------- | -------------------------------------------------------------------- |
| `nanocode-ts`                    | `AgentRuntime`, `EventHub`, `RemoteExecutor`, and the host contracts |
| `nanocode-ts/vercel`             | `nanocodeHarness` and its SQL journal, for durable-harness           |
| `nanocode-ts/cloudflare`         | `DurableObjectSql`, the `SqlHost` over Durable Object storage        |
| `nanocode-ts/nanocode_wasm.wasm` | The compiled module                                                  |

Wrangler loads `.wasm` imports as `WebAssembly.Module`, so a Worker imports the module directly. Other hosts compile `dist/nanocode_wasm.wasm` with `WebAssembly.compile`, as the Vercel adapter does.

## The Module

`npm run build` compiles the crate for `wasm32-unknown-unknown` with the `wasm` profile, then compiles the TypeScript into `dist/`. The build fingerprints the Rust sources, both lockfiles, and the toolchain, and skips Cargo when none of them changed.

```toml title="Cargo.toml"
# Small code, one codegen unit, and no unwinding or symbols
[profile.wasm]
inherits = "release"
opt-level = "s"
lto = "fat"
codegen-units = 1
panic = "abort"
strip = "symbols"
```

On 2026-10-20, the module committed at `e79b665` measured 8,374,626 bytes, or 2,572,519 bytes with `gzip -9`. Nearly all of it is SDK code that both harnesses, subagents, and hands use. Fat LTO removes the native-only crates of the `nanocode` facade.

## The Bridge

The module imports seven functions from the `host` import module and exports six: `memory`, `allocate`, `command`, `complete`, `complete_bytes`, and `poll`. Requests and replies are JSON. Request bodies, response chunks, socket frames, and SQL rows cross as raw bytes.

```rust title="src/wasm/bridge.rs"
// The functions that the TypeScript host supplies
#[link(wasm_import_module = "host")]
unsafe extern "C" {
    fn dispatch(id: u32, pointer: *const u8, length: usize);
    fn dispatch_body(
        id: u32,
        pointer: *const u8,
        length: usize,
        body: *const u8,
        body_length: usize,
    );
    fn cancel(id: u32);
    fn clock(monotonic: u32) -> f64;
    fn random(pointer: *mut u8, length: usize);
    fn reply(id: u32, pointer: *const u8, length: usize);
    fn panicked(pointer: *const u8, length: usize);
}
```

The host writes a command, such as `{"drive": {"next": "turn-1"}}`, calls `command`, then calls `poll`. The module runs its single-threaded `LocalPool` until every task waits on the host. Each wait is one `dispatch` with an ID, and the host answers it with `complete` and another `poll`.

```mermaid
sequenceDiagram
  participant Host as TypeScript host
  participant Module as WASM module
  Host->>Module: command(drive), then poll()
  Module->>Host: dispatch(store)
  Host->>Module: complete_bytes(rows), then poll()
  Module->>Host: dispatch(http_start)
  Host->>Module: complete(response), then poll()
  Module->>Host: reply(finished turn)
```

If the SDK drops a waiting future, the module calls `cancel` with its ID, and the host aborts the request, timer, or tool call behind it. A panic calls `panicked`, which fails the runtime and rejects its pending commands.

## Host Contracts

`AgentRuntime.instantiate` accepts the `SqlHost` and `published`, and `open` accepts the rest. The host-agnostic sources check against the standard web platform types only, so the core package needs no Node or Cloudflare types.

* **`SqlHost.transaction`:** Runs one operation's statements in one atomic SQLite transaction and returns each statement's rows as arrays. The SDK owns the tables and every statement, and binds only text.
* **`published`:** Receives each batch of logged events after it commits, in sequence order, with the encoded size of each event.
* **`dispatch`:** Sends a model HTTPS request and returns a fetch `Response`. The SDK sends no credentials, so the host or its egress adds them.
* **`socket`:** Opens a Codex Responses WebSocket with the SDK's handshake headers. `admit` runs before every frame that the module sends.
* **`tools.call`:** Runs a host tool. `context.call_id` stays the same when a turn replays, so a host can key an idempotency key on it.
* **`remote`:** Carries remote Code Mode frames for an executor and for hands. `RemoteExecutor` implements it over any `MessageSocket`.

```ts title="sql.ts"
import { DatabaseSync } from 'node:sqlite'
import type { SqlHost, SqlValue } from 'nanocode-ts'

const database = new DatabaseSync('session.db')

// Runs each SDK operation all or nothing, with rows as arrays
export const sql: SqlHost = {
  transaction(_kind, _operation, statements) {
    database.exec('BEGIN IMMEDIATE')
    try {
      const rows = statements.map(([query, ...values]) => {
        const statement = database.prepare(query)
        statement.setReturnArrays(true)
        const result = statement.all(...(values as string[]))
        return result as unknown as SqlValue[][]
      })
      database.exec('COMMIT')
      return rows
    } catch (error) {
      database.exec('ROLLBACK')
      throw error
    }
  },
}
```

`SqlHost.value_limit` is the largest value that the host's SQLite stores. The module logs a larger event as an `omitted` marker, fails a turn whose result is larger, and answers `too_large` for a prompt that does not fit.

## Adapters

### Vercel Functions

`nanocodeHarness` from `nanocode-ts/vercel` runs a session as a durable-harness `Harness`. It records the SQL writes in batches, each a step named `nanocode.sql` and keyed `sql:<n>` in the session's Workflow run. Each drive replays the committed batches into an in-memory `node:sqlite` database.

```ts title="lib/agent.ts"
import { durableAgent } from 'durable-harness'
import { nanocodeHarness } from 'nanocode-ts/vercel'

// A Claude session whose SQL writes are steps of its Workflow run
export const agent = durableAgent({
  durability: 'on',
  harness: nanocodeHarness({
    durability: 'on',
    model: 'claude-haiku-5-5',
    provider: 'claude',
  }),
})
```

Host tools run as `tool:<call_id>` steps. Model requests go to the Vercel AI Gateway with `AI_GATEWAY_API_KEY` or the Vercel OIDC token. With `durability: 'on'`, a write returns at once, and with `'strict'`, it returns after its batch commits.

### Durable Objects

`DurableObjectSql` from `nanocode-ts/cloudflare` is the `SqlHost` over a Durable Object's `storage.sql.exec` and `transactionSync`. Its storage type is structural, so the adapter needs no `@cloudflare/workers-types`. It sets `value_limit` to workerd's 8 MiB.

```ts title="session.ts"
import { AgentRuntime } from 'nanocode-ts'
import { DurableObjectSql } from 'nanocode-ts/cloudflare'
import module from 'nanocode-ts/nanocode_wasm.wasm'

// The object's own SQLite holds the session, and the observer logs
// each transaction's rows and duration
const sql = new DurableObjectSql(ctx.storage, (observation) =>
  console.log(observation),
)
const runtime = await AgentRuntime.instantiate(module, sql, {
  published,
})
```

[gakonst/nanocode-cloudflare-durable](https://github.com/gakonst/nanocode-cloudflare-durable) builds its session object on this adapter, and [gakonst/nanocode-vercel-durable](https://github.com/gakonst/nanocode-vercel-durable) runs the Vercel harness.

## Measurements

The repository's scripts write these measurements to `output/`. `measure-node.mjs` ran on an Apple Silicon laptop, against an in-process mock provider for both backends, so its turn times leave out model latency.

| Measurement                                                                    | Result                                            |
| ------------------------------------------------------------------------------ | ------------------------------------------------- |
| Instantiate the compiled module, `measure-node.mjs`, 2026-10-19                | 0.3 to 0.7 ms                                     |
| Open a new session, or reopen one, same runs                                   | 9 to 27 ms, or 6 to 11 ms                         |
| A first turn, then a turn with one host tool call, same runs                   | 33 to 50 ms, then 11 to 16 ms                     |
| Open a Claude session with a 2.1 MB checkpoint, `measure-cold.mjs`, 2026-10-20 | Median 14.6 ms with binary SQL rows, 21 ms before |
| The same open in a fresh process                                               | Median 36 ms, 42.6 ms before                      |

### Event Tails

`EventHub` bounds its tail by event count and by encoded bytes. On 2026-10-20 in Node 22.23, `measure-events.mjs` published 128 events of each size into a hub bounded at 128 events and 4 MiB.

| Event size | Count bound only  | With the 4 MiB bound         |
| ---------- | ----------------- | ---------------------------- |
| 64 KiB     | 8,437,864 bytes   | 63 events in 4,152,544 bytes |
| 1 MiB      | 134,266,928 bytes | 3 events in 3,146,448 bytes  |

### Opt Level

The repository's notes of 2026-10-19 record the WASM CPU time per turn of a 110-turn Worker session with 200 deltas per reply, in two runs per level. `"s"` keeps most of the saving of `3` without the larger module that every cold start compiles.

| `opt-level` | CPU per turn     | Module   | Gzipped |
| ----------- | ---------------- | -------- | ------- |
| `"z"`       | 24.0 and 30.5 ms | 7.39 MB  | 2.31 MB |
| `"s"`       | 19.1 and 17.8 ms | 8.01 MB  | 2.48 MB |
| `3`         | 8.4 and 18.7 ms  | 10.51 MB | 3.39 MB |

## Limits

* **One session per instance:** An instance runs one session scope, and `open` throws if the instance is already open.
* **One thread:** The Rust side keeps its state in `thread_local` cells and runs one `LocalPool`, with no `Mutex` or `RwLock`.
* **Code Mode placement:** The module embeds no JavaScript engine. `exec` runs cells in the host's runtime through [`LocalHand`](/docs/api/wasm/LocalHand) with `code_mode: true` (a Dynamic Worker per cell on Cloudflare Workers), on a remote executor, or on hands.
* **Git releases:** Every change to `src/` or to the SDK revision rebuilds the module and commits `dist/` again.
* **Host limits:** The host passes in limits, such as `value_limit` and `max_frame_bytes`. The package defines none of its own.

## More

- [Source](https://github.com/gakonst/nanocode-ts): The module, the host, and both adapters.

- [In the Browser](/docs/guides/browser): Run the module in a page over sqlite-wasm.

- [Cloudflare Workers](/docs/guides/workers): Run sessions in Durable Objects.

- [nanocode on Vercel](/vercel): The Vercel adapter in a live demo.

- [nanocode on Cloudflare](/cloudflare/): The Cloudflare adapter in a live demo.

- [nanocode-wasm](/docs/api/wasm): Reference for AgentRuntime and the host contracts.
