Agents as TypeScript

A Chidori agent is a .ts file that exports an agent function. Its input parameter is the typed JSON payload; its return value, serialized to JSON, is the agent's output. The runtime injects a chidori host object — every side effect goes through one of its methods.

Anatomy of an agent

agents/summarizer.ts

import type { Chidori } from "chidori";

export async function agent(
  input: { document: string; depth?: string },
  chidori: Chidori,
) {
  const summary = await chidori.prompt(
    "Summarize in 3 bullets:\n" + input.document,
    { type: "summary" },
  );
  const actions = await chidori.prompt("Extract action items:\n" + summary, {
    type: "json",
  });
  return { summary, action_items: actions };
}
  1. export async function agent(input, chidori) is the entry point — input holds the agent's typed inputs.
  2. The injected chidori object exposes every host function.
  3. The return value becomes the agent's JSON output.

Per-call options like model, temperature, and maxTokens go in the options object passed to each chidori.prompt(...) call — there is no module-level config.

TypeScript you already know

If you know TypeScript, you can read any Chidori agent. Control flow, array methods, objects, async/await, and helper functions all work exactly as you'd expect:

// Conditionals
const sources = input.depth === "deep" ? await fetchSources(urls) : [];

// Loops
const facts: string[] = [];
for (const url of urls) {
  const page = await chidori.tool("fetch_url", { url });
  facts.push(await chidori.prompt("Extract facts:\n" + page));
}

// Array transforms
const topUrls = results.flatMap((batch) => batch.slice(0, 3).map((r) => r.url));

// Helper functions
async function summarizeFor(audience: string, doc: string) {
  return chidori.prompt("Summarize for " + audience + ":\n" + doc);
}

Side effects go through chidori

TypeScript can do arbitrary I/O, so Chidori draws the line at the host object: anything that touches the outside world — call an LLM, hit an HTTP endpoint, read memory, ask a human, execute generated code — goes through a chidori.* method. Errors can be turned into values with chidori.tryCall(fn), which returns { ok, error } instead of throwing.

The side-effect boundary

Every interaction with the world is a chidori host call:

const answer = await chidori.prompt("What is 2+2?");          // LLM call
const page   = await chidori.tool("fetch_url", { url });      // registered tool
const resp   = await chidori.http("https://api.example.com"); // raw HTTP
const pref   = await chidori.memory("get", "user_pref");      // persistent KV + vector store
const ok     = await chidori.input("Proceed?", { default: plan }); // human-in-the-loop

Each call is a checkpoint boundary: the runtime logs the name, arguments, and result before returning. See the Host Functions page for the full API.

Why this design matters

Because the only way an agent touches the outside world is through chidori host functions, the runtime can enforce determinism by policy. A durable run uses a fixed Date, a seeded Math.random, no access to the host clock, and routes every side effect through a logged, cached host call. That lets Chidori:

  1. Log every side effect with zero instrumentation from you.
  2. Replay any past run to the same output by returning cached results for each logged call — zero LLM calls on replay.
  3. Suspend and resume mid-execution — chidori.input(...) saves a checkpoint, the human responds, the agent picks up exactly where it stopped.
  4. Branch alternative histories — edit a recorded result in the debugger, and the rest of the run takes a different path.

None of this requires special framework magic. It's just "TypeScript where the only I/O is a documented host-object call, run under a deterministic policy."

Was this page helpful?