# Chidori TypeScript API Reference

Chidori runs TypeScript agents in a Rust runtime. Agents use normal async
TypeScript for orchestration, while all side effects go through the injected
`chidori` host object so the runtime can log, replay, stream, pause, and resume
work.

This file is optimized as an LLM-facing reference for generating Chidori agents
and tools.

## Agent Shape

An agent is a `.ts` file that imports `{ chidori, run }` from the virtual
`chidori:agent` module and registers its handler with `run(...)`:

```ts
import { chidori, run } from "chidori:agent";

run(async (input: { document: string }) => {
  const summary = await chidori.prompt(
    "Summarize in three bullets:\n\n" + input.document,
    { type: "final" },
  );
  return { summary };
});
```

Rules:

- Import `{ chidori, run }` from `chidori:agent` and call `run(handler)` at the
  top level. (Legacy fallback: `export async function agent(input, chidori)`
  is still accepted when `run(...)` wasn't called.)
- Optionally validate the input before the handler executes with
  `run(handler, { inputSchema })`. `inputSchema` is either a Standard Schema
  validator (any Zod/Valibot/ArkType schema — its validated value, defaults and
  coercions applied, replaces the input) or a plain JSON Schema object (checked
  structurally: type/properties/required/items/enum/const/bounds/pattern).
  Validation is deterministic and runs before any host call; a failure throws
  `InputValidationError` listing every issue, and `chidori serve` answers 400
  with the issue list (the failed session is still stored and echoed in the
  response).
- Type the input with an inline object type or a `type` alias, never an
  `interface` — interfaces have no implicit index signature, so they fail the
  handler's `AgentJson` constraint with a confusing type error.
- Return JSON-compatible values only.
- Use `chidori.*` for LLMs, tools, input, signals, memory, templates, workspace
  files, and logging. HTTP goes through the standard `fetch`, which the runtime
  captures.
- Prefer deterministic code. Durable runs use fixed `Date` and seeded
  `Math.random` policies by default.
- Local TypeScript imports are governed by runtime policy. Dynamic imports are
  rejected.

## CLI

```bash
chidori init my-agent --template chat      # scaffold a starter project (or: docs, worker)
chidori check agents/my_agent.ts
chidori run agents/my_agent.ts --input key=value
chidori run agents/my_agent.ts --input '{"document": "text"}'
chidori run agents/my_agent.ts --model deepseek-chat   # default model for prompts (or CHIDORI_MODEL)
chidori run agents/my_agent.ts --stream
chidori run agents/my_agent.ts --trace
chidori dev agents/my_agent.ts --input key=value  # edit-and-replay loop: records one run, re-runs on every
                                            # save with recorded calls replayed free; a divergent edit is
                                            # reported with its seq and re-records live from there
chidori chat --system "You are a concise assistant." --model claude-sonnet
chidori chat agents/chat.ts                 # chat through a conversational agent file
chidori serve agents/webhook.ts --port 8080 --trusted
chidori serve --port 8080                   # no FILE: fleet-only server (detached agents; sessions must name an agent)
chidori resume agents/my_agent.ts <run_id>  # replay a recorded run byte-for-byte with zero model calls (durable resume);
                                            # the run's recorded model applies automatically (override with --model)
chidori resume agents/my_agent.ts <run_id> --trusted  # crash recovery of a trusted tool-using run:
                                            # same posture flags as `run`; continuation journals into the same run dir
chidori resume agents/my_agent.ts <run_id> --allow-source-change  # edit-and-resume: replay against edited code (divergence-checked)
chidori verify agents/my_agent.ts <run_id>  # checkpoint-as-test: replay with NO provider and a deny-all policy;
                                            # asserts completion with byte-identical output. Exit 0 = pass. Built for CI.
                                            # Journaled workspace writes DO re-materialize on disk (reported as
                                            # "N workspace re-materialization(s)") — same bytes, fresh mtime.
chidori trace <run_id>
chidori snapshot <run_id>
chidori holdings <run_id>                   # what the run holds right now: pending operation, queued
                                            # signals, unsettled actors, detached agents, branches,
                                            # armed compensations
chidori rollback <run_id>                   # saga rollback: run registered compensations newest-first
chidori serve agents/app.ts --app chidori.app.yml  # boot from an application manifest: keep_alive
                                            # detached-agent fleet, cron schedules, webhook routes
```

`chidori init [dir] --template docs|chat|worker` scaffolds a starter project
(agent + README, plus a bundled docs corpus for `docs`); omit `--template` to pick interactively. The `docs` template chats
with the bundled Chidori docs; the `chat` template is a conversational agent;
the `worker` template is an autonomous tool-using loop (see
`examples/agents/worker.ts`).

`chidori chat` is an interactive multi-turn REPL backed by `conversation()`. With
no agent file it chats with the model directly; pass a conversational agent file
(one accepting `{ messages, system?, model? }` and returning
`{ transcript }` or `{ history }`, like the `chat` template) to chat through it.
Each turn is a durable host call and streams its reply token-by-token; the prior
turns replay for free, so only your newest message reaches the provider. Flags:
`--system`, `--model`. Type `exit`/`quit` or Ctrl-D to end. Sessions are durable
runs journaled under `.chidori/runs/<session_id>`; `chidori chat [FILE]
--resume <session_id>` reprints the transcript for $0 (completing a
crash-interrupted turn live) and continues the same session; `chidori trace
<session_id>` inspects it like any run.

`chidori snapshot <run_id>` prints `runtime.snapshot.json` metadata without
printing raw VM snapshot bytes.

## Host Object

The runtime injects a `chidori` object with these methods.

### prompt

```ts
const text = await chidori.prompt("Write a concise answer", {
  type: "final",
  model: "claude-sonnet",
  maxTokens: 500,
  temperature: 0.2,
});
```

Options:

- `type`: label for streamed prompt output, such as `"progress"`, `"draft"`,
  `"subagent"`, or `"final"`.
- `model`: provider model override. Prompts that don't set it use the run's
  default model (`--model` / `CHIDORI_MODEL`, falling back to
  `claude-sonnet-4-6`). The resolved default is recorded in the run's
  manifest, so `resume`/`branch-rerun` re-run under the same model
  automatically.
- `system`: system prompt for this call.
- `maxTokens`: output token cap.
- `maxTurns`: cap on provider tool-use turns — with `tools` set, `prompt()`
  runs a COMPLETE provider tool-use loop internally (call tools, feed back
  results, repeat up to `maxTurns`) and returns the final text. Reach for
  the manual `context().respond()`/`toolResult()` loop only when you need
  per-step control (inspecting calls, streaming progress between steps,
  custom budgets).
- `temperature`: sampling temperature.
- `tools`: the tools available to the loop — registered tool NAMES (from the
  MCP/native registry) and/or `defineTool(...)` HANDLES, freely
  mixed. Handles are plain objects defined inline or imported from any
  module; their `run` executes in the agent's own VM (closures + captured
  effects work, replay is deterministic), each invocation journaled as a
  `mark("tool:<name>")` record:

    import { chidori, run, defineTool } from "chidori:agent";
    const search = defineTool({
      name: "search",
      description: "Search the corpus.",
      parameters: { type: "object", properties: { q: { type: "string" } }, required: ["q"] },
      run: async ({ q }) => corpus.filter((d) => d.includes(q)),
    });
    const answer = await chidori.prompt("find it", { tools: [search], maxTurns: 6 });

- `format`: `"json"` parses the reply as JSON (a single wrapping markdown
  fence is tolerated). Unparseable output — e.g. a reply truncated by
  `maxTokens` — THROWS by default so it can't masquerade as a structured
  result; pass `strict: false` to fall back to the raw string instead.
- `strict`: applies to `format: "json"`. `true` (default) throws on
  unparseable output with the parse error and reply head; `false` restores
  the lenient raw-string fallback.
- `cache`: prompt-cache posture. Defaults to on (`"5m"`): the runtime marks
  the stable request head (system, tools, conversation prefix) so providers
  bill repeated prefixes at the cached rate. `false` disables for this call;
  `"1h"` requests the extended TTL. Caching never changes a response.

`prompt()` returns only the text. If the response was cut off by the
`maxTokens` cap the runtime prints a truncation warning to stderr (reasoning
models spend the same budget on hidden reasoning first, so budget generously
for them); use `context().respond()` when you need the structured
`stopReason` / token counts / `reasoning` yourself.

When streaming is enabled, prompt events include `prompt_type`, `stream_id`,
and `seq` so UIs can filter progress streams separately from final-answer
streams.

### context

```ts
const base = chidori
  .context()
  .system("You are a policy analyst.")
  .doc("policy-corpus", corpusText) // large stable reference block
  .cacheBreakpoint("5m");           // freeze the head as a cacheable prefix

let ctx = base;
for (const q of questions) {
  ctx = ctx.user(q);
  const { text, context } = await ctx.prompt({ type: "final" });
  ctx = context; // assistant turn appended; the prefix stays shared
}
```

An immutable, turn-structured prompt context. Builder methods (`system`,
`tools`, `doc`, `user`, `assistant`, `toolResult`, `cacheBreakpoint`) each
return a NEW context sharing the parent's segments, so `base.user("a")` and
`base.user("b")` are independent forks of the same prefix. Building is pure
in-VM work; only `prompt()` / `respond()` perform a durable host call. The
stable head is auto-marked for provider prompt caching, so each turn after the
first reads the shared prefix at the discounted cached rate.

- `prompt(options?)` → `{ text, context }`: send, return the answer plus the
  context extended with the assistant turn (including any tool-use exchange).
- `respond(options?)` → `{ response, context }`: one structured turn for
  author-driven tool loops (`response.toolCalls`, `response.blocks`; reasoning models also expose `response.reasoning`).
- `digest()` → stable content hash of the assembled request, also recorded in
  each prompt's call-log args as `request_digest`.
- `estimateTokens()` → rough local size estimate for window budgeting.
- `compact(options?)` → `Promise<Context>`: explicit, opt-in window
  compaction. Summarizes the older conversation turns into ONE durable
  summary segment (a recorded `prompt` host call, so it replays
  deterministically) and returns a new context: stable head + summary +
  fresh cache breakpoint + the newest `keepTurns` turns (default 2) kept
  verbatim. `budgetTokens` makes it a pure no-op (no host call) while
  `estimateTokens()` is within budget, so loops can call it unconditionally;
  `model` / `instructions` / `maxTokens` / `ttl` tune the summarizer.
  Compaction is never automatic — it changes what the model sees, so it is
  always an author decision.

See `examples/agents/context_qa.ts` for a complete corpus-Q&A agent.

### conversation

```ts
const chat = chidori.conversation({
  system: "You are a concise, friendly assistant.",
  tools: [search],                 // defineTool handles (see the `tools` prompt option), on every turn
  compact: { budgetTokens: 8000 }, // opt-in per-turn window management
});

const reply = await chat.say("Hi, who are you?"); // one durable prompt call
await chat.say("What can you help with?");         // prefix read at cached rate

chat.length;       // number of completed exchanges
chat.history();    // [{ role, text }, ...]
chat.context;      // the underlying immutable Context, for the lower-level API
```

A stateful chat-assistant wrapper over `context()` — the most common agent
shape. It owns the running dialogue: the system/tools head is frozen once as a
cacheable prefix, and each `say(message)` appends the user turn, makes one
durable `prompt` host call, and threads the assistant turn back in for the next
message. So the whole conversation is recorded, replays for $0, and reads the
shared prefix at the cached rate each turn.

- `say(message, options?)` → `Promise<string>`: send a user message, return the
  assistant reply text; the dialogue advances in place. `options` are
  per-turn `PromptOptions` (override the conversation defaults).
- `respond(message, options?)` → `Promise<LlmResponseJson>`: like `say()` but
  returns the structured response (`toolCalls`, `blocks`) for author-driven
  tool loops; append results with `chat.context.toolResult(...)`, then `say()`.
- `loop(options?)` → `Promise<{role,text}[]>`: drive an interactive dialogue —
  read each human message via `chidori.input()` (terminal stdin under
  `chidori run`, a paused session resume under `chidori serve`), reply with
  `say()`, repeat until the user types an exit word (`"exit"`/`"quit"`) or
  `until` returns true. Options: `prompt`, `inputOptions`, `exit`, `maxTurns`,
  `skipEmpty`, `turn`, `onReply`, `until`.

`conversation(options)` accepts `system`, `tools`, default `type`/`model`/
`maxTokens`/`temperature`/`cache`, `cacheTtl`, and `compact` (a `CompactOptions`
applied before each turn — a no-op until the tail exceeds budget). See
`examples/agents/conversation.ts`.

Setting `CHIDORI_PROMPT_CACHE_DIR=<dir>` opts into a local content-addressed
prompt cache keyed on the assembled `request_digest`: an exact repeat of a
prompt (same model, system, tools, messages, cache layout) — even from a
different run — is served locally without calling the provider, then recorded
as a normal call-log entry with the identical result (and no token usage,
since nothing was billed). The cache is live-path only: replay always
short-circuits to the call log first and never consults it.

### input

```ts
const answer = await chidori.input("Approve this request?", {
  type: "approval",
  choices: ["yes", "no"],
  default: "no",
  details: draft,   // the artifact under review — shown to the human
});
```

`details` carries the thing being approved (a draft, a diff, a report): the
CLI prints it above the prompt, and a paused session exposes it as
`pending_details` alongside `pending_prompt` — so approval gates are never
blind. It is display-only and never part of the durable record.

In server mode, `input()` pauses the session. Resume it with
`POST /sessions/{id}/resume` or `AgentClient.resume(id, response)`.

Under `chidori run`, `input()` reads one line from stdin. An empty answer —
blank enter, or end-of-file in a non-interactive run — resolves to the
declared `default`; EOF with no `default` fails the run rather than silently
returning an empty string.

### signal / pollSignal

```ts
// Pause at a named listen point until an outside party (human or agent)
// delivers { name, payload, from } via POST /sessions/{id}/signal. A durable
// per-run mailbox absorbs signals that arrive before the agent listens.
const review = await chidori.signal("review");

// With timeoutMs, resolves to { timedOut: true } after the deadline.
const r = await chidori.signal("review", { timeoutMs: 60000 });
if (r.timedOut) { /* nobody answered */ }

// Non-blocking: consume a queued signal or get null (recorded, replayable).
const steer = await chidori.pollSignal("steer");

// Fan-in: pass an array to pause until ANY listed name fires; result.name
// says which.
const fired = await chidori.signal(["review", "steer"]);
```

Every consumed signal is recorded in the call log, so multiplayer sessions
replay deterministically. Signals delivered to a run streaming over
`POST /sessions/stream` are pushed into the live agent's mailbox in-memory and
resume a matching pause in-process. See `docs/signals.md`.

### tool

A tool is merely a function with a documented signature — a `name`, a
`description`, and JSON-schema `parameters` — nothing more. Agent tools are
defined in-VM with `defineTool`, which wraps that signature around a `run`
function, and passed to `prompt` / `context().tools` as handles (see the
`tools` prompt option above). The
`chidori.tool(name, args)` host effect is for tools sourced from OUTSIDE the
agent — MCP-server tools (configured via `CHIDORI_MCP_*`) and Rust-native
tools registered by an embedding application — dispatched by name:

```ts
// e.g. a search tool exposed by a configured MCP server
const result = await chidori.tool("docs_search", { query: "snapshot runtime" });
```

A tool's `fetch` is SSRF-guarded by default: requests to hosts that resolve
to non-public addresses (localhost, RFC-1918 ranges) are refused even under
`--trusted`. Tools that talk to local services (an Ollama sidecar, a local
index) need `CHIDORI_HTTP_ALLOW_HOSTS=127.0.0.1` (comma-separated hosts,
IPs, or CIDRs; `*` disables the guard). Provider endpoints
(`CHIDORI_OPENAI_COMPAT_URL=http://localhost:11434`) are NOT affected — the
guard covers only agent/tool-initiated http effects.

### callAgent

```ts
const child = await chidori.callAgent("child.ts", { topic: "snapshots" });
```

Sub-agents share the parent runtime context and call log. Runtime dispatch
accepts TypeScript `.ts` sub-agents only.

### util.parallel

```ts
const [a, b] = await chidori.util.parallel([
  () => chidori.prompt("Draft option A", { type: "draft" }),
  () => chidori.prompt("Draft option B", { type: "draft" }),
]);
```

An in-VM helper with `Promise.all` semantics; `options.concurrency` caps
in-flight tasks. Everything under `chidori.util` is pure JavaScript control
flow and records nothing itself — only the durable calls made inside the
tasks appear in the journal.

### branch

```ts
const outcomes = await chidori.branch([
  { label: "outline-first", source: "strategies/outline_first.ts", input: { research } },
  { label: "draft-direct", source: "strategies/draft_direct.ts", input: { research } },
]);
const best = outcomes.filter((o) => o.status === "completed").reduce(pick);
```

Fork the run into one sub-run per variant from the current anchored state (the
parent's VFS plus each variant's explicit `input`). Each branch runs its own
source module (resolved like `callAgent` paths) on a fresh context whose
records occupy a reserved, disjoint sequence range nested under the `branch`
call, and returns `{ label, branchId, status, output?, pendingPrompt?,
error? }`. The whole fan-out is one recorded durable call: replay returns the
outcomes from the call log without re-running the branches. Variants run in
waves of `options.concurrency` worker threads (default 1 — sequential);
outcome order always follows variant order. Nested `chidori.branch` inside a
branch is rejected.

When the parent run persists, each branch is stored under
`.chidori/runs/<run>/branches/op-<seq>/branch-<k>/` (`source.ts`,
`checkpoint.json`, `branch.json`, plus the fork-time VFS anchor), making
branches independently operable after the parent moves on:

```bash
chidori branches <run-id>                                   # list branch stores
chidori branch-resume <run-id> <branch-id> --value "blue"   # answer a paused input()
chidori branch-rerun <run-id> <branch-id>                   # re-run edited source.ts from the anchor
```

A resumed or re-run branch updates only its own store; the parent's recorded
`branch` outcome is immutable history. See `docs/branching-execution.md`.

Every source version a run or branch executes is also recorded as a git-like
commit chain alongside the journal (content-addressed, hardlink-deduped):

```bash
chidori history <run-id>                    # commits + the journal ranges each ran
chidori history <run-id> --show <commit>    # print a stored version
chidori history <run-id> --diff <a>..<b>    # unified diff between versions
```

See `docs/source-history.md`.

### actors: spawn / send / receive / join / stop / status / lookup

```ts
// Start an agent module as a supervised, addressable, concurrent process.
// spawn returns a HANDLE: the actor's address plus its lifecycle methods.
const worker = await chidori.actors.spawn("workers/researcher.ts", { topic }, {
  name: "researcher",      // optional registry name for actors.lookup / actors.send
  restart: "resume",       // "never" (default) | "clean" | "resume"
  maxRestarts: 3,
  backoffMs: 500,          // doubles per attempt
  intercept: {             // narrow the child's context — never widen:
    model: "cheap-model",  //   default model for the child's prompts
    tools: ["search"],     //   registry tools, intersected with the spawner's
    workspace: "research", //   relative `..`-free subpath of the spawner's root
  },
});

// Message passing (never blocks). String-addressed sends take a pid, a
// registered name, or "parent" (the sender's spawner).
await worker.send("focus", { region: "EU" });
await chidori.actors.send("researcher", "focus", { region: "EU" });

// Blocking in-place consumption, in delivery order; fan-in via an array.
const msg = await chidori.receive("draft");   // { name, payload, from: { kind, id } }
const any = await chidori.receive(["draft", "cancel"], { timeoutMs: 60000 });
if (any.timedOut) { /* deadline passed */ }

// Settle: fold the actor's records into this run's log and get the outcome.
const outcome = await worker.join();
// → { pid, status: "completed"|"failed"|"paused"|"stopped", output?, error?, restarts }
await worker.stop();                          // cooperative stop, then join
await worker.status();                        // { pid, status, restarts, mailbox, waitingFor? }
await chidori.actors.lookup("researcher");    // a handle, or null
```

Actors are supervised siblings of branches (`docs/actors.md`): each runs its
own source module on an isolated VM and its records occupy a reserved,
disjoint sequence range, but an actor is detached and addressable — it runs
concurrently on its own thread with a durable mailbox. Inside an actor,
`chidori.signal`/`pollSignal` consume actor messages too (the mailbox uses
the signal envelope; `from.id` is the sending actor's pid, or `"run"`), and
`receive` waits in place without pausing the run. Restart strategies:
`clean` re-runs from scratch; `resume` replays the actor's accumulated log
minus the trailing failed records (including their nested effects — a failed
tool call's inner http record retries live, not from cache), so completed
work returns from cache and only the failing call retries. Every primitive
is a durable call — replay of the spawning run re-runs nothing — and a crash
before a join re-creates the actor on resume from the recorded spawn/send
records.

Actor death is observable: a `failed` or `paused` settle delivers a
`"__chidori.down__"` message to the owner's mailbox
(`{ pid, name, status, error?, pendingPrompt?, restarts }`) — include it in
fan-in receives (`receive(["result", "__chidori.down__"])`). A `receive`
(even with `timeoutMs`) fails fast once every spawned actor has settled and
nothing matching is queued.

Actors form **supervision trees**: an actor can spawn its own supervised
children (each level carves child sequence ranges out of its own, bounding
depth at four actor levels), `"parent"` addresses the sender's spawner (the
owning actor, or the run for a top-level actor), and join/stop are owner-only
— an actor is settled by whoever spawned it. Supervisors reap their children:
when an actor settles (or discards its log on a `clean` restart) its
still-live children are cooperatively stopped and their registered names
released; a `resume` restart keeps children (cached spawn records return the
same live pids). A run may spawn at most 128 actors in total, restarted
children included.

### agents: detached durable processes (spawn / send / join / stop / status / lookup)

```ts
// Start an agent module as a DETACHED durable process: its own run id and
// journal, a registered name that outlives this run, a durable mailbox, and
// a hibernate/wake lifecycle. spawn returns a handle like actors.spawn.
const svc = await chidori.agents.spawn("services/inbox-triager.ts", {}, {
  name: "inbox-triager",   // registry name; generated when omitted
  restart: "resume",       // "never" | "clean" | "resume" (default)
  maxRestarts: 3,
  backoffMs: 500,
  model: "deepseek-chat",  // optional; defaults to the spawner's resolved model
                           // and travels with the agent across wakes/restarts
});

await svc.send("email", { from: "a@x.com" });          // durable delivery; wakes a
await chidori.agents.send("inbox-triager", "email", p); // hibernating agent on match
await svc.status();   // { name, runId, status, restarts, waitingFor?, deadline?, output?, error? }
await svc.join({ timeoutMs: 30000 });   // waits for completed|failed|stopped|paused
await svc.stop();                        // cooperative stop
await chidori.agents.lookup("inbox-triager");   // { name, runId, status } handle, or null
```

Detached agents vs actors: an actor lives inside its spawning run (records
fold into the parent at a join; unjoined work dies with the run); a detached
agent is its own durable run and **outlives the spawner**. Inside the agent,
`chidori.signal(name)` is a hibernate point — the agent holds no thread and
no VM while waiting; a matching send (from any run, or `POST
/agents/detached/{name}/send` on the server) wakes it under resume-by-replay.
The fleet survives process restarts: `chidori serve` re-arms every
registered agent at boot. Requires persistence. Statuses: `running`,
`hibernating`, `paused` (interactive input/approval — no interactive
counterpart yet), `completed`, `failed`, `stopped`. See
`docs/detached-agents.md`.

### alarm

```ts
const fired = await chidori.alarm(24 * 60 * 60 * 1000);  // → { timedOut: true }
```

A durable timer on the signal machinery: the run (or detached agent)
hibernates and is woken at the deadline, surviving process restarts — the
deadline is persisted and re-armed at boot. In a detached agent this is the
idiomatic "do maintenance every N hours even with no traffic" primitive.

### http (fetch / node:http)

There is no `chidori.http`. Use the standard `fetch` API (or the
`node:http`/`node:https` client modules); the runtime replaces them with
captured versions backed by one policy-gated host op:

```ts
const response = await fetch("https://example.com/webhook", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ ok: true }),
});
const data = await response.json();
```

Because the capture lives at the base networking layer, every request — even
one made inside a dependency — is policy-checked, logged, and replayed from the
call log when available. `fetch` returns a standard `Response`
(`status`/`ok`/`headers`/`json()`/`text()`).

### template

```ts
const prompt = await chidori.template("prompts/summary.jinja", {
  document: input.document,
});
```

Use templates for reusable prompt text. Inline templates are also supported.

### memory

```ts
await chidori.memory.set("draft", { text: "..." });
const draft = await chidori.memory.get("draft");
const keys = await chidori.memory.list();
await chidori.memory.delete("draft");
await chidori.memory.clear();
```

Memory actions are logged and replay-aware. The store lives at
`<root>/.chidori/memory/<namespace>.json`, where `<root>` is the run's
workspace root (the agent file's directory under `run`/`resume`/`serve`, or
`CHIDORI_WORKSPACE_ROOT`) — so memory is anchored to the agent, like runs and
workspace, and persists across sessions regardless of the process's current
directory. `CHIDORI_MEMORY_DIR` overrides the root.

### workspace

```ts
const entries = await chidori.workspace.list({ completeOnly: true });
const text = await chidori.workspace.read("notes/draft.md");
const entry = await chidori.workspace.write("notes/draft.md", "...", { language: "markdown" });
await chidori.workspace.delete("notes/draft.md", "superseded");
const manifest = await chidori.workspace.manifest();
```

Durable file store rooted at the project directory (the agent file's dir)
under `run`, `resume`, `serve`, and detached agents alike;
`CHIDORI_WORKSPACE_ROOT` overrides the root. Entries carry `{ path, status,
sha256, bytes }`. Every action is policy-gated (`workspace:write` /
`workspace:delete` are refused under `untrusted`) and recorded in the call
log. `remove` is an alias for `delete`.

### appData

```ts
await chidori.appData.write("insert into notes (body) values ($1)", ["hi"]);
const rows = await chidori.appData.query("select * from notes", []);
```

Host-brokered writes/queries against a run-bound app-data cluster (generative
UI). Params are bound server-side, never string-concatenated; the guest never
holds a DB credential. Journaled like `http`. Requires a host-side
`CHIDORI_APP_DATA` binding; without one, calls return
`{ appDataError: { kind: "no_cluster", ... } }`.

### renderDOM

```ts
document.body.appendChild(document.createElement("div"));
const batch = chidori.renderDOM();
```

Agents get a virtual `document` / `window`. `renderDOM()` flushes the pending
DOM mutation batch as a journaled `dom_render` effect — recorded live, served
from the journal on replay. See `docs/dom-runtime-prototype.md`.

### mark

```ts
await chidori.mark("after-draft", { tokens: 120 });
```

`mark()` records a labelled call-log marker — an annotation for the trace,
nothing more (the durable VALUE checkpoint is `chidori.step`). Persisted runs
also refresh snapshot manifest metadata at durable host safepoints.

### step

```ts
const plan = await chidori.step("plan", () => buildPlanDeterministically(input));
```

`step(name, fn)` is a durable value checkpoint: `fn` runs once and its
JSON-serializable result is journaled; replay and resume return the recorded
value (or re-throw the recorded error) without re-running `fn`. Wrap expensive
deterministic computation in a step so resuming a long run does not re-pay it.
The callback must be pure, synchronous compute: host effects, captured
randomness, filesystem writes, timers, and async callbacks throw inside a step.
See `docs/value-checkpoints.md`.

### compensation (saga rollback)

```ts
await chidori.compensation.register("deprovision", "comp/deprovision.ts", { serverId });
// → { registered: true }
```

Durably register an inverse action — an agent module + its input — for a side
effect the run just performed. Registration is one journal record and performs
nothing; on a successful run the registrations are void. When a run stops
short (cancelled, failed, abandoned), roll it back explicitly:

```bash
chidori rollback <run_id>        # runs registered compensations newest-first
```

or `POST /sessions/{id}/cancel` with `{"compensate": true}` (deferred with a
note when the session is still live). Each compensation executes as its own
ordinary run — journaled, replayable, `chidori trace`-able. A failed
compensation is reported and rollback continues past it. A completed rollback
writes `rollback.json` into the run directory; a second rollback refuses
(inverse actions are not re-fired). The agent path resolves like `callAgent`
and must exist at registration.

### log

```ts
await chidori.log("Fetched candidates", { count: 3 });
```

Use structured logs for progress and debugging. Logs are call-log records.

### util.retry and util.tryCall

```ts
const value = await chidori.util.retry(
  () => fetch("https://example.com").then((r) => r.json()),
  { attempts: 3 },
);

const result = await chidori.util.tryCall(() => chidori.tool("maybe_fails", {}));
if (!result.ok) {
  await chidori.log("Tool failed", { error: result.error });
}
```

`RetryOptions` also accepts `delayMs` and `backoff`, but the helper
retries immediately — no delay is applied between attempts.

## Defining Tools

A tool is a plain object made with `defineTool`: JSON-compatible metadata
(`name`, `description`, JSON-schema `parameters`) wrapped around an async
`run(args, chidori)` function. Define it inline or import it from any module —
there is no `tools/` directory and no registration step — and pass the handle
in the `tools` prompt option. The `run` body executes in the agent's own VM,
so closures over agent state work, and its side effects (`fetch`, workspace,
...) are the same captured effects the agent already has: journaled live,
replayed deterministically. Each invocation is journaled as a
`mark("tool:<name>")` record for the trace.

```ts
import { chidori, run, defineTool } from "chidori:agent";

// A real web lookup: `fetch` inside a tool body is the captured fetch, so
// every request is policy-gated, journaled, and replays for $0.
const wikiSearch = defineTool({
  name: "wiki_search",
  description: "Search Wikipedia and return the top matching titles and URLs.",
  parameters: {
    type: "object",
    properties: {
      query: { type: "string", description: "Search query" },
    },
    required: ["query"],
  },
  run: async (args: { query: string }) => {
    const url =
      "https://en.wikipedia.org/w/api.php?action=opensearch&format=json" +
      "&limit=5&search=" +
      encodeURIComponent(args.query);
    const resp = await fetch(url);
    if (!resp.ok) throw new Error(`wiki_search failed: HTTP ${resp.status}`);
    const [, titles, , urls] = (await resp.json()) as [string, string[], string[], string[]];
    return titles.map((title, i) => ({ title, url: urls[i] }));
  },
});

run(async (input: { question: string }) => {
  const answer = await chidori.prompt(input.question, {
    tools: [wikiSearch],
    maxTurns: 4,
  });
  return { answer };
});
```

The `parameters` field is a JSON Schema object and is what the provider reads
for tool calling. Tools sourced from OUTSIDE the agent — MCP-server tools and
Rust-native registrations — are invoked by NAME instead, via the
`chidori.tool` host effect or a name string in `tools` (see `tool` above).

## Streaming

CLI:

```bash
chidori run examples/agents/streaming_progress.ts --stream
```

`--stream` changes only how progress is reported (NDJSON events on stdout):
the run keeps the plain `run` posture — implicit workspace root at the agent's
directory, journaled under `.chidori/runs/<run_id>` — and the final `done`
event carries `run_id` and `status` (`completed`, `paused` with
`pending_signal`, or `failed`).

HTTP:

```ts
for await (const event of client.stream({ topic: "snapshots" })) {
  if (event.type === "prompt_delta" && event.prompt_type === "progress") {
    process.stdout.write(event.delta);
  }
}
```

Stream event types:

- `call`: a host call record.
- `prompt_start`: prompt stream started.
- `prompt_delta`: incremental token text.
- `prompt_end`: prompt stream ended.
- `paused`: the run paused at a `signal()` listen point and
  stays live; a delivered signal (or the timeout deadline) resumes it on the
  same stream.
- `done`: run completed, failed, or paused.

Prompt labels work inside sub-agents and parallel branches because prompt
events are emitted through the shared runtime context.

## Sessions

Start a server:

```bash
chidori serve examples/agents/webhook.ts --port 8080 --trusted
```

`chidori serve` is deny-by-default: without `--trusted` or explicit
`CHIDORI_POLICY*` configuration, gated effects (network requests via
`fetch`/`node:http`, tool calls, workspace mutations) are refused under the
built-in `untrusted` profile (read-only workspace introspection stays
allowed). `--untrusted` forces that profile over any env configuration.
`chidori run` is ask-by-default: gated effects prompt for a y/a/N approval (`a` = allow that target for the rest of the run) at
the terminal, and fail closed when no terminal is available (scripts, CI) —
pass `--trusted` there for the permissive allow-all behavior. LLM prompts and
pure compute are never gated.

Session endpoints:

- `POST /sessions`: create and run a session.
- `GET /sessions`: list sessions.
- `GET /sessions/{id}`: get session state.
- `GET /sessions/{id}/checkpoint`: get call log plus optional
  `snapshot_manifest`.
- `GET /sessions/{id}/snapshot`: get only snapshot manifest metadata.
- `GET /sessions/{id}/holdings`: what the run is holding right now — pending
  host operation, queued signals, unsettled actors, detached agents (with
  registry state), open branches, armed compensations. CLI twin:
  `chidori holdings <run_id>`.
- `POST /sessions/{id}/resume`: resume a paused `input()` session.
- `POST /sessions/{id}/signal`: deliver `{ name, payload?, from? }` — resolves
  a matching signal pause (200), delivers live to a streaming run
  (202 `delivered_live`), or enqueues into the durable mailbox (202 `queued`).
- `POST /sessions/{id}/approve`: approve or deny a policy-gated call that
  paused the run as `awaitingapproval`.
- `POST /sessions/{id}/cancel`: cancel a running session.
- `POST /sessions/{id}/replay`: replay from a call-log checkpoint.
- `POST /sessions/stream`: run with SSE events.
- `ANY /*` (any other route): the request is folded into
  `{event: {method, path, headers, query, body}}` and run as the agent's
  input. `{status, body, headers?}` output shapes the HTTP response; a run
  that PAUSES (signal/input/approval) is stored as a real session and
  answered 202 with the session view, so the caller can deliver/resume it.

Sessions persist across server restarts by default (SQLite at
`.chidori/sessions.sqlite3` next to the agent). `CHIDORI_DB_PATH` overrides
the path; `CHIDORI_DB_PATH=:memory:` opts out of durable sessions.

## TypeScript SDK

```ts
import { AgentClient, Checkpoint } from "@1kbirds/chidori";

const client = new AgentClient("http://localhost:8080");
// Production server (CHIDORI_API_KEY set): pass { apiKey } — sent as a
// bearer token on every request including stream(). Python: api_key=...
const session = await client.run({ document: "Rust is a systems language." });

const checkpoint = await session.checkpoint();
const replayed = await client.replay(checkpoint);

if (checkpoint.snapshotManifest) {
  console.log(checkpoint.snapshotManifest.pending?.kind);
}

const manifest = await client.getSnapshotManifest(session.id);
```

`Checkpoint` contains the replay call log and optional snapshot manifest
metadata. It does not contain raw VM snapshot bytes.

## Providers & Model Selection

Providers register from environment variables (all can coexist; requests
route by model name, first match wins):

- `ANTHROPIC_API_KEY` — Anthropic (`claude-*` models).
- `OPENAI_API_KEY` — OpenAI; `OPENAI_BASE_URL` redirects it at any
  OpenAI-compatible endpoint and widens it to match all model names.
- `CHIDORI_OPENAI_COMPAT_URL` + `CHIDORI_OPENAI_COMPAT_KEY` — any
  OpenAI-compatible endpoint (DeepSeek, Groq, Ollama, vLLM, LiteLLM…),
  matching all model names. `/v1` and bare hosts both work.
  (`LITELLM_API_URL`/`LITELLM_API_KEY` are legacy aliases.)
- `chidori model-login` — zero-setup OpenRouter fallback.

The default model for prompts that don't set `model` in code is
`CHIDORI_MODEL` (or `--model` on `run`/`resume`), falling back to
`claude-sonnet-4-6`. The resolved default is recorded in each run's
manifest, so `resume`, `branch-resume`/`branch-rerun`, and server
resume/replay routes re-run under the run's own model with no flags.
Detached agents likewise carry their model in their registry descriptor.

Cost estimation covers Anthropic/OpenAI models out of the box; teach it
other models with `CHIDORI_PRICING` (JSON, model prefix → USD per MTok,
consulted before the built-in table):

```bash
CHIDORI_PRICING='{"deepseek-v4-flash":{"input_per_mtok":0.28,"output_per_mtok":0.42,"cache_read_multiplier":0.1}}'
```

`chidori trace` and `chidori stats` display prompt-cache read/write token
totals alongside cost.

`chidori.workspace.*` roots at the project directory under `run`, `resume`,
and `serve`; `CHIDORI_WORKSPACE_ROOT` overrides the location.

## Runtime Policy

Durable TypeScript runs record policy in the snapshot manifest:

- `typescript_imports`: `none`, `relative`, or `project`.
- `date`: `disabled`, `fixed`, or `host`.
- `random`: `disabled`, `seeded`, or `host`.
- `maps_sets`: `reject` or `serialize`.

Environment overrides:

```bash
CHIDORI_TS_IMPORTS=relative
CHIDORI_TS_DATE=fixed
CHIDORI_TS_RANDOM=seeded
CHIDORI_SNAPSHOT_MAPS_SETS=reject
```

Durable snapshot runs reject host clock and host randomness.

For local smoke tests without provider credentials, set
`CHIDORI_TEST_LLM_RESPONSE` to a static response string. This registers a
catch-all test provider and avoids external network calls.

## Snapshot And Replay Notes

Current resume/replay uses the call log plus persisted host-promise metadata:
the agent re-executes and host calls return cached records. Persisted
TypeScript runs also write `runtime.snapshot` and `runtime.snapshot.json`
metadata. Replay **is** the resume mechanism by design — there is no direct VM
continuation from `runtime.snapshot`; the manifest carries journal/scaffold
metadata rather than serialized VM bytes.

Snapshot manifests record:

- ABI version and engine fork.
- Runtime policy.
- Entry source hash and imported module hashes.
- Pending host operation, if any.
- Host promise table records, including pending/resolved/rejected state.
- Call-log length and snapshot blob filename.

Resume rejects incompatible source hashes, policy, or ABI before trusting
snapshot metadata or raw VM snapshot bytes.
