Common patterns
Chidori has a small number of primitives that compose into most agent shapes. This page maps jobs to primitives; each recipe links to the doc that covers it in depth, and to a runnable example where one exists.
| I want to… | Reach for | Details |
|---|---|---|
| Ask an LLM, maybe with tools | chidori.prompt + defineTool | Host API |
| Build a chat assistant | chidori.conversation | Core Concepts |
| Answer webhooks / arbitrary HTTP | chidori serve catch-all route | below |
| Gate an action on human approval | chidori.input with details | below |
| Wait for an outside party (human or agent) | chidori.signal | Signals |
| Run several prompts concurrently | chidori.util.parallel | below |
| Compare whole strategies, not just prompts | chidori.branch | Branching Execution |
| Run long-lived concurrent workers with supervision | chidori.actors | Actors |
| Run a service that outlives any one run | chidori.agents + chidori.alarm | Detached Agents · Host API |
| Remember things across runs | chidori.memory | Memory |
| Avoid re-paying expensive compute on resume | chidori.step | Value Checkpoints |
| Pin agent behavior in CI | chidori verify | below |
Approval gates that show their work
Never make a human approve blind: pass the artifact under review as
details (Core Concepts covers how gates surface
it).
const draft = await chidori.prompt("Draft the announcement.", { type: "draft" });
const verdict = await chidori.input("Ship this announcement?", {
type: "approval",
choices: ["yes", "no"],
default: "no",
details: draft,
});
if (verdict.toLowerCase() !== "yes") return { shipped: false };Under chidori serve, the input() suspends the session to disk — no
process waits while the human decides. Days later,
POST /sessions/{id}/resume picks the run up exactly where it paused.
Example: examples/agents/input_pause.ts.
An agent behind a webhook
Under chidori serve, any route other than the /sessions/* API is folded
into { event: { method, path, headers, query, body } } and run as the
agent's input; returning { status, body, headers? } shapes the HTTP
response:
import { chidori, run } from "chidori:agent";
run(async (input: { event: { method: string; path: string; body?: unknown } }) => {
const { event } = input;
if (event.method !== "POST" || event.path !== "/hooks/pr") {
return { status: 404, body: { error: "not found" } };
}
const triage = await chidori.prompt(
`Triage this pull request event:\n${JSON.stringify(event.body)}`,
{ type: "final" },
);
return { status: 200, body: { triage } };
});Branch on input.event early, as above: every request runs the whole
agent, so return a cheap 404 for non-events before any model call. A run
that pauses (on input(), a signal, or a policy approval) is stored as
a real session and answered 202 with the session view, so the caller can
resume or signal it later — a webhook can open a long-lived, durable
workflow. Deep doc: Running Modes (which also covers
outbound requests from a handler —
examples/agents/webhook.ts).
Give the model tools, keep the loop
prompt() with tools runs the whole provider tool-use loop for you —
Core Concepts covers the loop and when to hand-roll
it with context():
const answer = await chidori.prompt(input.question, {
tools: [searchNotes, wikiSearch], // defineTool handles and/or registry names
maxTurns: 6,
});Every model turn and tool invocation is journaled, so the loop shows up in
chidori trace — and replays for $0. Examples:
examples/agents/tool_use.ts (built-in
loop), examples/agents/worker.ts
(author-driven loop).
Fan out drafts, concurrently
const [a, b, c] = await chidori.util.parallel(
[
() => chidori.prompt("Draft: crisp and technical", { type: "draft" }),
() => chidori.prompt("Draft: warm and story-led", { type: "draft" }),
() => chidori.prompt("Draft: contrarian angle", { type: "draft" }),
],
{ concurrency: 3 },
);
const winner = await chidori.prompt(
`Pick the strongest draft and say why:\n\nA:\n${a}\n\nB:\n${b}\n\nC:\n${c}`,
{ type: "final" },
);util.parallel is in-VM control flow — only the prompts inside are durable
calls. When the strategies differ (different code, not just different
prompts), use chidori.branch instead: each variant runs its own module
from the current state, and the whole fan-out replays as one recorded call.
Example: examples/branching/.
Multiplayer review
A run can wait on named signals from several parties — humans or other agents — with a durable mailbox absorbing whatever arrives early:
// Wait for either reviewer; the result's `name` says who fired.
const first = await chidori.signal(["design-review", "security-review"], {
timeoutMs: 24 * 60 * 60 * 1000,
});
if (first.timedOut) return { escalated: true };Note on timeoutMs: the deadline is enforced by the serving process —
under chidori serve sessions, and in-process for actors and detached
agents; on a bare top-level chidori run pause it is inert (the CLI prints
the pause and exits).
Deliver with POST /sessions/{id}/signal — the server resolves a matching
pause, pushes to a live streaming run, or queues into the mailbox. Example:
examples/multiplayer-review/, deep doc:
Signals.
A service that sleeps between events
A detached agent owns its own run, journal, and mailbox, and outlives the
run that spawned it. Waiting costs nothing: signal() hibernates with no
thread and no VM, and alarm() is the durable "wake me every N hours"
timer:
// services/inbox-triager.ts
import { chidori, run } from "chidori:agent";
run(async () => {
for (;;) {
const msg = await chidori.signal(["email", "shutdown"], {
timeoutMs: 6 * 60 * 60 * 1000, // or use chidori.alarm for pure maintenance ticks
});
if (msg.timedOut) { await runMaintenance(); continue; }
if (msg.name === "shutdown") return { stopped: true };
await triage(msg.payload);
}
});Spawn it once with chidori.agents.spawn(..., { name: "inbox-triager" });
send to it from any run or via POST /agents/detached/{name}/send. The
fleet re-arms at boot, so a server restart loses nothing. Deep doc:
Detached Agents.
Don't re-pay expensive compute
Replay re-executes your TypeScript. That's usually free — but a genuinely expensive pure computation (parsing a big corpus, building an index) would re-run on every resume. Journal its value once:
const index = await chidori.step("build-index", () => buildIndex(corpus));Resume returns the recorded value without re-running the callback. The
callback must be pure, synchronous compute — host effects inside a step
throw. Deep doc: Value Checkpoints, example:
examples/agents/value_checkpoint.ts.
Pin behavior with a recorded run
A recorded run is a complete, deterministic specification of your agent's behavior. Export it as a fixture (the raw run directory is heavy — don't commit that), commit the fixture, and assert against it in CI:
chidori run agent.ts --input question="smoke test" # record once
chidori export <run_id> --fixture tests/fixtures # the four artifacts verify reads
git add tests/fixtures # commit the fixture
chidori verify agent.ts <run_id> --runs-dir tests/fixtures # in CI: exit 0 = no driftverify replays with no providers, no tools, and the untrusted policy
profile — if the agent's prompts, tool calls, or control flow drift from
the recording, it fails. A full integration test that costs $0 and runs in
milliseconds. Full contract: Replay as test.
Observing with Tael builds its golden regression
cases on the same mechanism.
Keep long conversations inside the window
Compaction is explicit — it changes what the model sees, so it's an author decision, and it's a recorded prompt call, so it replays deterministically:
const chat = chidori.conversation({
system: PERSONA,
compact: { budgetTokens: 8000 }, // no-op until the tail exceeds budget
});Or on a raw context: ctx = await ctx.compact({ budgetTokens: 8000 }).
Deep doc: Context Management.