Your first agent
Getting Started runs Chidori's pre-built examples. This tutorial has you write your own agent and walks it through the full durability loop: run it, trace its call log, pause it for a human, replay it with zero LLM calls, and turn the recording into a CI test. Plan for about fifteen minutes.
You need the chidori binary (install) and,
ideally, a provider key exported in your shell:
export ANTHROPIC_API_KEY=sk-ant-... # or another provider — see belowAny Anthropic, OpenAI, or OpenAI-compatible key works
(provider setup), and
chidori model-login gives you a zero-setup OpenRouter fallback.
No API key? Set
CHIDORI_TEST_LLM_RESPONSE="(test reply)"and every prompt call returns that static string instead of calling a provider — so the model won't actually exercise the tool, but the durability mechanics this tutorial is about (journaling, pause, replay, verify) behave identically.
1. Write the agent
An agent is one ordinary TypeScript file. Create a fresh directory and save
this as research.ts:
import { chidori, run, defineTool } from "chidori:agent";
const NOTES = [
"2026-05-04 standup: replay divergence bug traced to unseeded RNG in retry helper.",
"2026-05-11 standup: prompt cache hit rate at 87% after context() refactor.",
"2026-05-18 standup: actors supervision tree shipped; joins fold logs correctly.",
];
// A tool is just a function with a documented signature. `run` executes in
// the agent's own VM, so closures over NOTES work, and every invocation is
// journaled for the trace.
const searchNotes = defineTool({
name: "search_notes",
description: "Keyword search over the team's standup notes.",
parameters: {
type: "object",
properties: { query: { type: "string", description: "Search keyword" } },
required: ["query"],
},
run: async ({ query }: { query: string }) =>
NOTES.filter((n) => n.toLowerCase().includes(query.toLowerCase())),
});
run(async (input: { question: string }) => {
await chidori.log("Researching", { question: input.question });
// One call runs the whole provider tool-use loop: the model calls
// search_notes, the runtime executes it and feeds results back, up to
// maxTurns, then returns the final text.
const answer = await chidori.prompt(
`Answer from the standup notes, citing dates: ${input.question}`,
{ tools: [searchNotes], maxTurns: 4, type: "final" },
);
// Pause for a human. `details` carries the artifact under review, so the
// approval is never blind.
const ship = await chidori.input("Publish this answer?", {
type: "approval",
choices: ["yes", "no"],
default: "no",
details: answer,
});
return { answer, published: ship.toLowerCase() === "yes" };
});Three things to notice before running it:
- Every side effect goes through a host call.
chidori.log,chidori.prompt,chidori.input— each is recorded in the run's call log. Theifs,awaits, and string munging between them are plain TypeScript. - The tool needs no registration.
defineToolwraps a function in thename/description/parametersthe model reads; you pass the handle straight intoprompt(). - Type the input with an object type, not an
interface— interfaces fail the handler's JSON constraint with a confusing error.
2. Run it
chidori run research.ts --input question="what happened with the prompt cache?"The model runs the tool loop — calling search_notes, reading the results,
composing an answer — and then the run stops and asks you:
Publish this answer? [yes/no]The answer text prints above the prompt — that's details. Type yes. The
run completes and prints its JSON output, something like:
{
"answer": "…the 2026-05-11 note reports an 87% prompt cache hit rate…",
"published": true
}
chidori runasks y/a/N approval before powerful effects — network access,chidori.toolcalls, workspace writes. This agent's tool is pure in-VM compute, so the only pause you see is your owninput()gate. See Running Modes for postures and--trusted.
3. Read the record
Every run journals to .chidori/runs/<run_id>/ next to the agent file. Look
at what was recorded:
RUN_ID=$(ls -t .chidori/runs | head -1)
chidori trace "$RUN_ID"The trace is the run's complete story: the log call, each model turn and
search_notes invocation inside the prompt loop, your input() answer, and
the token counts and cost of every prompt. This call log — not a framework
abstraction — is what makes everything in the next two steps possible.
4. Replay it for $0
chidori resume research.ts "$RUN_ID"The agent code re-executes from the top — but every host call returns its recorded result from the call log instead of touching the world. No model is called, no tokens are billed, nobody is asked to approve anything, and the output is byte-identical to step 2. This is the same mechanism that powers crash recovery: a run that dies halfway replays to the frontier of its log and continues live from there (how replay works).
Two variants worth knowing now:
chidori resume research.ts <run_id> --allow-source-change— replay against edited agent code, divergence-checked. Fix a bug three runs deep without re-paying the first three runs.- A paused server-mode session resumes the same way, minutes or days later, in a fresh process (Signals).
5. Turn the recording into a test
chidori verify research.ts "$RUN_ID"verify replays the run with no provider configured and a deny-all
policy and asserts it completes with byte-identical output. Exit code 0
means the agent's behavior hasn't drifted from the recording. Commit
.chidori/runs/<run_id>/ to git and this is a full integration test of your
agent — prompts, tool loop, approval gate and all — that costs $0 and runs in
milliseconds in CI. See Value Checkpoints for
bounding replay cost as runs grow.
Where next
- Core Concepts — the full host-function surface and the mental model behind it.
- Common Patterns — approval gates, fan-out, multiplayer review, scheduled agents: which primitive fits which job.
- Host API Reference — every
chidori.*method, option by option. chidori serve research.ts --port 8080turns this same file into an HTTP session API whereinput()pauses become resumable sessions — Running Modes.