Host API reference
Reference for the chidori host object and the agent module surface. Each
method here is a durable host call unless marked as an in-VM helper:
recorded in the journal (the run's call log) live, returned from the journal
on replay. The
LLM-optimized rendition of this reference ships in the repo as
llm.txt — point your codegen tools at that.
For orientation first, read Core Concepts.
Agent shape
An agent is a .ts file that imports from the virtual chidori:agent module
and registers its handler with run(...):
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 }fromchidori:agentand callrun(handler)at the top level. (Legacy fallback:export async function agent(input, chidori)is still accepted whenrun(...)wasn't called.) - Optionally validate the input before the handler executes with
run(handler, { inputSchema }): 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. Validation is deterministic and runs before any host call; a failure throwsInputValidationErrorlisting every issue, andchidori serveanswers 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
typealias, never aninterface— interfaces have no implicit index signature, so they fail the handler'sAgentJsonconstraint 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 standardfetch, which the runtime captures. - Prefer deterministic code. Durable runs use fixed
Dateand seededMath.randompolicies by default. - Local TypeScript imports are governed by runtime policy. Dynamic imports are rejected.
Try the validation live — this example runs in your browser, and the input is
editable: delete topic (or set it to a number, or add an extra key) and the
run refuses with the full issue list before the handler executes:
import { chidori, run } from "chidori:agent";
run(
async (input: { topic: string }) => {
await chidori.log("handler entered — input passed the schema", {
topic: input.topic,
});
return { topic: input.topic };
},
{
inputSchema: {
type: "object",
properties: { topic: { type: "string", minLength: 1 } },
required: ["topic"],
additionalProperties: false,
},
},
);LLM calls
chidori.prompt(text, options?)
const text = await chidori.prompt("Write a concise answer", {
type: "final",
model: "claude-sonnet-4-6",
maxTokens: 500,
temperature: 0.2,
});Returns the reply text. 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.
| Option | Meaning |
|---|---|
type | Label for streamed prompt output — "progress", "draft", "subagent", "final", … |
model | Provider model override. Unset prompts use the run's default model (--model / CHIDORI_MODEL, falling back to claude-sonnet-4-6); the resolved default is recorded in the run manifest so resume/branch-rerun re-run under the same model automatically. Short aliases like claude-sonnet resolve to the current canonical id; samples here use canonical ids. |
system | System prompt for this call. |
maxTokens | Output token cap. Reasoning models spend the same budget on hidden reasoning first — budget generously. A truncated reply prints a warning to stderr. |
maxTurns | Cap on provider tool-use turns for the built-in tool loop. |
temperature | Sampling temperature. |
tools | Tools available to the loop: registered tool names (MCP/native registry) and/or defineTool(...) handles, freely mixed. Handle bodies run in the agent's own VM; each invocation is journaled as a mark("tool:<name>") record. |
format | "json" parses the reply as JSON (a single wrapping markdown fence is tolerated). Unparseable output throws by default so truncation can't masquerade as a structured result. |
strict | Applies to format: "json". true (default) throws on unparseable output; false falls back to the raw string. |
cache | Prompt-cache posture. Defaults to on ("5m"): the stable request head (system, tools, conversation prefix) is marked so providers bill repeated prefixes at the cached rate. false disables for this call; "1h" requests the extended TTL. Caching never changes a response. |
Use context().respond() instead when you need the structured stopReason,
token counts, or reasoning yourself, or per-step control of a tool loop.
When streaming is enabled, prompt events carry prompt_type, stream_id,
and seq so UIs can filter progress streams from final-answer streams — see
Streaming.
chidori.context()
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.
| Method | Returns | Purpose |
|---|---|---|
prompt(options?) | { text, context } | Send; get 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() | string | Stable content hash of the assembled request; also recorded in each prompt's journal args as request_digest. |
estimateTokens() | number | Rough local size estimate for window budgeting. |
compact(options?) | Promise<Context> | Explicit, opt-in window compaction — see below. |
compact() 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) verbatim.
budgetTokens makes it a pure no-op 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 and
Context Management.
chidori.conversation(options?)
const chat = chidori.conversation({
system: "You are a concise, friendly assistant.",
tools: [search], // defineTool handles, 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 ContextA stateful chat-assistant wrapper over context() — the most common agent
shape. The system/tools head is frozen once as a cacheable prefix; each
say(message) appends the user turn, makes one durable prompt host call,
and threads the assistant turn back in. The whole conversation is recorded,
replays for $0, and reads the shared prefix at the cached rate each turn.
| Method | Purpose |
|---|---|
say(message, options?) | Send a user message, return the assistant reply text; the dialogue advances in place. options are per-turn PromptOptions. |
respond(message, options?) | Like say() but returns the structured response (toolCalls, blocks) for author-driven tool loops; append results with chat.context.toolResult(...), then say(). |
loop(options?) | 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 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. The
cache is plain files on disk, shared by every run and process pointed at the
same directory: an exact repeat of a prompt — even from a different run or a
concurrent process — is served locally without calling the provider, then
recorded as a normal journal entry with the identical result and no token
usage. Live-path only: replay always short-circuits to the journal first.
Humans and other agents
chidori.input(prompt, options?)
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
});Pause for a 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 — approval gates
are never blind. It is display-only and never part of the durable record.
- Under
chidori serve,input()pauses the session; resume withPOST /sessions/{id}/resumeorAgentClient.resume(id, response). - Under
chidori run,input()reads one line from stdin. An empty answer — blank enter, or EOF in a non-interactive run — resolves to the declareddefault; EOF with nodefaultfails the run rather than silently returning an empty string.
chidori.signal(name | names[], options?) / chidori.pollSignal(name)
// 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: pause until ANY listed name fires; result.name says which.
const fired = await chidori.signal(["review", "steer"]);timeoutMs records the deadline on the pause; enforcing it is the
supervising server's job — chidori serve arms a timer per paused session
and re-arms them all at startup. Inside an actor or a detached agent the
deadline is enforced in-process. Under a bare top-level chidori run, a
signal pause prints "paused, awaiting signal" and the process exits, so
timeoutMs is inert there.
Every consumed signal is recorded in the journal, 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 Signals.
chidori.alarm(ms)
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.
Tools and sub-agents
defineTool(...) and the tools option
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.
import { chidori, run, defineTool } from "chidori:agent";
// `fetch` inside a tool body is the captured fetch: policy-gated,
// journaled, and replayed 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 run body executes in the agent's own VM: closures over agent state
work, and its side effects are the same captured effects the agent already
has. Each invocation is journaled as a mark("tool:<name>") record.
chidori.tool(name, args)
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:
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 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.
chidori.callAgent(path, input)
const child = await chidori.callAgent("child.ts", { topic: "snapshots" });Sub-agents share the parent runtime context and journal. Runtime dispatch
accepts TypeScript .ts sub-agents only.
Concurrency and multi-agent
chidori.util.parallel(fns, options?) — in-VM helper
const [a, b] = await chidori.util.parallel([
() => chidori.prompt("Draft option A", { type: "draft" }),
() => chidori.prompt("Draft option B", { type: "draft" }),
]);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.
chidori.branch(variants, options?)
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 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 journal 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.
Persisted branches are independently operable after the parent moves on:
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.tsSee Branching Execution.
chidori.actors.* — supervised concurrent processes
const worker = await chidori.actors.spawn("workers/researcher.ts", { topic }, {
name: "researcher", // optional registry name for lookup / send
// (reserved: "parent" and "actor-*")
restart: "resume", // "never" (default) | "clean" | "resume"
maxRestarts: 3, // default 3
backoffMs: 500, // default 0; doubles per attempt
idleTimeoutMs: 300000, // default 300000 — reclaim an idle actor
intercept: { model: "claude-haiku-4-5" }, // narrow the child's scope
});
await worker.send("focus", { region: "EU" }); // never blocks
await chidori.actors.send("researcher", "focus", { region: "EU" });
const msg = await chidori.receive("draft"); // { name, payload, from }
const any = await chidori.receive(["draft", "cancel"], { timeoutMs: 60000 });
const outcome = await worker.join(); // fold records into this run's journal
await worker.stop(); // cooperative stop, then join
await worker.status(); // { pid, status, restarts, mailbox, waitingFor? }
await chidori.actors.lookup("researcher"); // a handle, or nullActors run their own source module on an isolated VM, concurrently on their
own thread, with a durable mailbox. Restart strategies: clean re-runs from
scratch; resume replays the actor's accumulated journal minus the trailing
failed records, so completed work returns from cache and only the failing
call retries. idleTimeoutMs reclaims an actor that sits idle past the
deadline. intercept ({ model, tools, workspace }) scopes the child down —
it can only narrow what the parent already has, never widen it; the actor's
model is set via intercept.model (there is no top-level model option).
Actor death is observable via a "__chidori.down__" message;
actors form supervision trees (three generations of actors below the run,
≤ 128 actors per run); join/stop
are owner-only. Full semantics, including the intercept discussion:
Actors.
chidori.agents.* — detached durable agents
const svc = await chidori.agents.spawn("services/inbox-triager.ts", {}, {
name: "inbox-triager", // registry name that outlives this run
restart: "resume", // "never" | "clean" | "resume" (default)
model: "claude-haiku-4-5",
});
await svc.send("email", { from: "a@x.com" }); // durable delivery; wakes a hibernating agent
await svc.status();
await svc.join({ timeoutMs: 30000 });
await svc.stop();
await chidori.agents.lookup("inbox-triager");A detached agent is its own durable run and outlives the spawner: its own
run id and journal, a registered name, a durable mailbox, and a
hibernate/wake lifecycle — chidori.signal(name) inside the agent holds no
thread and no VM while waiting. The fleet survives process restarts:
chidori serve re-arms every registered agent at boot. Requires
persistence. See Detached Agents.
The spawn options are not the actor set — the two shapes genuinely differ:
| Option | actors.spawn | agents.spawn |
|---|---|---|
name | optional; "parent" and "actor-*" are reserved | optional registry name (letters, digits, - _ .) |
restart default | "never" | "resume" |
maxRestarts / backoffMs | 3 / 0 | 3 / 0 |
idleTimeoutMs | yes — default 300000 | not available |
intercept | yes — narrow-only child scoping | not available |
model | via intercept.model only | top-level option; defaults to the spawner's model and is persisted in the agent's registry descriptor |
State and effects
fetch / node:http — there is no chidori.http
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();The runtime replaces the standard networking APIs with captured versions backed by one policy-gated host call. 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 journal when available.
chidori.template(strOrPath, vars)
const prompt = await chidori.template("prompts/summary.jinja", {
document: input.document,
});Jinja rendering for reusable prompt text; inline templates are also supported; undefined variables fail loudly. See Prompt Templates.
chidori.memory.*
await chidori.memory.set("draft", { text: "..." });
const draft = await chidori.memory.get("draft");
const entries = await chidori.memory.list(); // [{ key, value }, ...]
const drafts = await chidori.memory.list({ prefix: "draft" });
const existed = await chidori.memory.delete("draft"); // true if the key existed
await chidori.memory.clear();Persistent, namespaced key-value storage across runs, anchored to the
agent's workspace root at .chidori/memory/<namespace>.json
(CHIDORI_MEMORY_DIR overrides). list(options?) returns an array of
{ key, value } entries; its full option set is namespace (selects
.chidori/memory/<ns>.json under the workspace root; default "default")
and prefix (a list-only key filter). delete resolves to whether the
key existed. Logged and replay-aware. See Memory.
chidori.workspace.*
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. Entries carry { path, status, sha256, bytes }. Every action is policy-gated (workspace:write /
workspace:delete are refused under untrusted) and recorded. remove is
an alias for delete.
chidori.step(name, fn)
const plan = await chidori.step("plan", () => buildPlanDeterministically(input));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 Value Checkpoints.
chidori.compensation.register(name, agent, input?) — saga rollback
const server = await chidori.tool("provision_server", { size: "large" });
await chidori.compensation.register("deprovision", "comp/deprovision.ts", {
serverId: server.id,
});The journal runs forward; compensations let it run backward. Each registration durably records an inverse action — an agent module plus its input — for a side effect the run just performed. Registration itself does nothing; on a successful run the registrations are void history. When a run stops short (cancelled, failed, or abandoned mid-flight), roll it back:
chidori rollback <run_id>or POST /sessions/{id}/cancel with {"compensate": true} (deferred with a
note when the session is still live — roll back after it settles). Registered
compensations execute newest-first, each as its own ordinary run —
journaled, replayable, visible in chidori trace. A failed compensation is
reported and rollback continues past it (the remaining inverse actions are
independent obligations). A completed rollback writes rollback.json into
the run directory and a second rollback refuses — inverse actions are not
re-fired. Rollback is explicit, never automatic: compensations perform real
side effects, and re-firing them is an operator decision.
The agent path resolves like callAgent (relative to the project root) and
must exist at registration — a compensation that can't resolve is useless
exactly when it's needed.
Watch the ledger arm itself — this example registers two compensations and
then fails; the runner reports the armed inverse actions newest-first,
exactly the plan chidori rollback <run_id> would execute (delete the
throw and the registrations become void history instead):
import { chidori, run } from "chidori:agent";
run(async () => {
await chidori.compensation.register("deprovision", "comp/deprovision.ts", {
serverId: "srv-7",
});
await chidori.compensation.register("notify-oncall", "comp/notify.ts");
throw new Error("provisioning failed halfway");
});chidori.log(msg, data?) / chidori.mark(label, data?)
await chidori.log("Fetched candidates", { count: 3 });
await chidori.mark("after-draft", { tokens: 120 });log records structured progress for debugging. mark records a labelled
journal marker — an annotation for the trace, nothing more (the durable
value checkpoint is chidori.step).
chidori.util.retry(fn, options?) / chidori.util.tryCall(fn) — in-VM helpers
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 });
}Only attempts (default 3) is honored, and retries are immediate.
RetryOptions also accepts delayMs and backoff, but they are not
applied — no delay occurs between attempts.
chidori.appData.*
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", ... } }.
chidori.renderDOM()
Experimental. Installed in every native agent but absent from the
TypeScript types — under the agent-env types, call it as
(chidori as any).renderDOM().
document.body.appendChild(document.createElement("div"));
const batch = (chidori as any).renderDOM();Agents get a virtual document / window. renderDOM() flushes the
pending DOM mutation batch as a journaled dom_render effect. It belongs to
the DOM-runtime prototype — engineering note (GitHub):
DOM Runtime Prototype.
Streaming
chidori run examples/agents/streaming_progress.ts --stream--stream changes only how progress is reported (NDJSON events on stdout);
the final done event carries run_id and status. Over HTTP, use
POST /sessions/stream (SSE) — here consumed through the
TypeScript SDK's client.stream():
for await (const event of client.stream({ topic: "snapshots" })) {
if (event.type === "prompt_delta" && event.prompt_type === "progress") {
process.stdout.write(event.delta);
}
}| Event | Meaning |
|---|---|
call | A host call record. |
prompt_start / prompt_delta / prompt_end | Prompt stream lifecycle; deltas carry incremental token text. |
paused | The run paused at a signal() listen point and stays live; a delivered signal (or the timeout) 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.
HTTP session API
chidori serve exposes runs as sessions — create, pause/resume, signal,
approve, replay, stream, plus a catch-all route that turns any other HTTP
request into an agent event. The endpoint reference lives in
Running Modes; production posture and recipes in
Deployment; typed clients in the
TypeScript SDK and
Python SDK.
Providers & model selection
Providers register from environment variables (all can coexist; requests route by model name, first match wins):
| Variable | Provider |
|---|---|
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. |
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):
CHIDORI_PRICING='{"deepseek-v4-flash":{"input_per_mtok":0.28,"output_per_mtok":0.42,"cache_read_multiplier":0.1}}'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.
Runtime policy
Durable TypeScript runs record policy in the snapshot manifest:
| Policy | Values |
|---|---|
typescript_imports | none, relative, or project |
date | disabled, fixed, or host |
random | disabled, seeded, or host |
maps_sets | reject or serialize |
Environment overrides:
CHIDORI_TS_IMPORTS=relative
CHIDORI_TS_DATE=fixed
CHIDORI_TS_RANDOM=seeded
CHIDORI_SNAPSHOT_MAPS_SETS=rejectDurable snapshot runs reject host clock and host randomness. Resume rejects incompatible source hashes, policy, or ABI before trusting snapshot metadata — see Replay & Resume and the Sandbox Model.