§ mental model
A TypeScript runtime plus a fixed set of host functions.
Chidori isn't a DSL or a graph builder. It's a Rust runtime that runs your TypeScript agents and injects a chidori object with a small set of host functions — prompt, tool, parallel, input, callAgent, http. Everything else is ordinary TypeScript: async control flow, helpers, imports.
Determinism comes from runtime policy: durable runs use a fixed Date and seeded Math.random, and every side effect is a logged host call. Replay is just walking the log and returning cached results instead of running the live calls.
§ host functions
A handful of calls. Everything else is TypeScript.
chidori.prompt(text, { type?, model?, … })Single LLM call. Returns text or parsed JSON. Stream events carry the type label. Logged.
chidori.tool(name, args)Invoke a registered tool by name. Inputs and result are logged for replay.
chidori.parallel(fns)Run independent functions concurrently. Order preserved. One labelled span per branch.
chidori.callAgent(path, input)Invoke another .ts agent. Shares the parent runtime context and call log.
chidori.input(msg, options)Pause and persist the run, wait for a human reply. Resume from the saved state.
chidori.http(url, options)Make an HTTP request. Policy-checked, logged, and replayed from the call log.
chidori.execJs / execPython / execWasm(code, opts)Run agent-written code in a sandbox with strict time and memory limits.
chidori.template(strOrPath, vars)Render a Jinja2 template with minijinja. Pure transform, no LLM.
chidori.memory(action, …)Persistent key-value + vector storage. Actions are logged and replay-aware.
chidori.retry / tryCall / log / env / checkpointRetry with backoff, capture errors, structured logs, env reads, explicit call-log markers.
§ session file
The single artifact you commit.
A session is a JSON document: the agent path, the input, an ordered list of host function calls keyed by sequence number with their results, and the final output. Commit it to git, diff it in CI, or ship it as an artifact; replay walks the list and returns cached results instead of live calls.
// session.json — abbreviated
{
"agent": "agents/researcher.ts",
"input": { "question": "OTel SDK choice" },
"calls": [
{ "seq": 0, "fn": "prompt",
"type": "json", "model": "claude-sonnet",
"tokens_in": 412, "tokens_out": 84 },
{ "seq": 1, "fn": "parallel",
"branches": [
{ "fn": "tool", "name": "web_search", "result": "…" },
{ "fn": "tool", "name": "web_search", "result": "…" }
]
},
{ "seq": 2, "fn": "prompt",
"type": "final", "model": "claude-opus" }
],
"output": { "answer": "…", "queries": ["…"] }
}§ anatomy of an agent
One file. One agent() export.
A Chidori agent is a .ts file that exports an async agent(input, chidori) function. The input is your typed payload; the return value is the JSON output. The injected chidori object is how you reach the outside world. Helper functions and imports live alongside it freely.
// agents/researcher.ts
import type { Chidori } from "chidori";
export async function agent(
input: { question: string },
chidori: Chidori,
) {
const queries = await chidori.prompt(
"3 search queries for: " + input.question,
{ type: "json" },
);
const results = await chidori.parallel(
queries.map((q) => () =>
chidori.tool("web_search", { query: q })),
);
return chidori.prompt(
await chidori.template("prompts/research.jinja", {
question: input.question,
results,
}),
{ model: "claude-opus", type: "final" },
);
}§ runtime topology
Four ways to drive the same binary.
one-shot
$ chidori run agents/x.ts --input k=vExecute once. Writes a run checkpoint on exit. Best for scripts and CI.
serve
$ chidori serve agents/x.ts --port 8080HTTP server. Each POST /sessions runs the agent; ANY /* is passed as an event.
replay
$ chidori run agents/x.ts --replay session.jsonRe-run from a saved call log. Cache hits for every call. $0.00, identical output.
resume
$ curl …/sessions/{id}/resume -d @answer.jsonContinue a session paused on chidori.input(). Resumes from the saved state.
In every mode, host function calls are emitted as OpenTelemetry spans. Set OTEL_EXPORTER_OTLP_ENDPOINT to point them at Tael or any OTLP collector.
§ design notes
Why these choices, in case you're wondering.
Why TypeScript?
Agents are real programs: native async control flow, typed inputs, imports, and the editor tooling your team already runs. No template DSL to learn, no YAML schema, no graph builder — just a function.
If TypeScript can do I/O, how is it deterministic?
Durable runs execute under a runtime policy: fixed Date, seeded Math.random, no host clock, and every side effect routed through a chidori host function. Given the same inputs and cached host results, control flow reproduces the same output.
What does replay actually cache?
Every prompt, tool, http, and input call is logged with a sequence number and its result. On replay each call checks the log for its seq — a hit returns the cached result instantly, a miss executes normally.
How does parallel stay replayable?
chidori.parallel() fans out independent functions concurrently while preserving deterministic sequence numbers, so branches and the prompts inside them replay in a stable order.
Pair Chidori with
Tael so every prompt, tool, and human-input step lands as a queryable span.