thousand birds / products / chidori

Chidori agent framework

A YAML-free agent framework. Agents are written in Starlark — a deterministic Python dialect — so every run can be checkpointed, replayed, and traced for free. Below: the host function surface, the session file format, the runtime modes, and the language choices behind them.

● V3 STABLEGitHub ↗Docs ↗
status
v3 · stable
license
Apache 2.0
runtime
Rust binary
language
Starlark
sdk
Python (stdlib)
session
JSON, content-addressed
§ mental model

A Starlark VM plus a fixed set of host functions.

Chidori isn't a DSL or a graph builder. It's a Starlark interpreter wrapped around a small set of host functions — prompt, tool, parallel, input, exec, agent. Everything else is ordinary Starlark: control flow, comprehensions, helpers.

Determinism falls out of two facts: Starlark itself has no I/O, and every host function is logged with content-addressed args and results. Replay is just walking the log and skipping the live calls.

§ host functions

Eight calls. Everything else is Starlark.

config(model=…, temperature=…)
Module-level defaults. Pure: no I/O, no side effects.
prompt(text, *, model?, format?, schema?)
Single LLM call. Returns text or structured output. Logged span.
tool(name, **kwargs)
Invoke a registered tool by name. Args and result are content-addressed.
parallel(callables) → list
Fan out independent calls. Order preserved. One span per branch.
input(prompt, *, schema)
Suspend, checkpoint, wait for human reply. Resume at the same seq.
exec(code, *, runtime, limits)
Run agent-written code in a WASI sandbox. No fs, no net, hard limits.
agent(name, **kwargs)
Invoke another .star agent. Fresh VM, shared session, own span tree.
template(path, **vars) → str
Render a Jinja template from disk. Pure transform, no LLM.
§ session file

The single artifact you commit.

A session is a JSON document: the agent path, the config, an ordered list of host function calls keyed by sequence number, and content hashes for every argument and result. Two runs with the same prefix share their cache; replay walks the list and skips live calls.

// session.json — abbreviated
{
  "agent": "agents/researcher.star",
  "config": { "model": "claude-sonnet" },
  "calls": [
    { "seq": 0, "fn": "config",
      "args_hash": "0x1a…", "result_hash": "0x00…" },
    { "seq": 1, "fn": "prompt",
      "args_hash": "0x4f…", "result_hash": "0xb2…",
      "tokens_in": 412, "tokens_out": 84 },
    { "seq": 2, "fn": "parallel",
      "branches": [
        { "fn": "tool", "args_hash": "0xc1…", "result_hash": "0x9e…" },
        { "fn": "tool", "args_hash": "0xc2…", "result_hash": "0x9f…" }
      ]
    }
  ],
  "return_hash": "0xfe…",
  "spans": 6
}
§ anatomy of an agent

One file. One agent() entry point.

A Chidori agent is a .star file with a top-level def agent(...). Its parameters are the inputs; the return value is the JSON output. Module-level config() sets defaults. Helper functions can live alongside it freely.

# agents/researcher.star
config(model = "claude-sonnet")

def agent(question, depth = "standard"):
    queries = prompt(
        "3 search queries for: " + question,
        format = "json",
    )
    results = parallel([
        lambda q = q: tool("web_search", query = q)
        for q in queries
    ])
    return prompt(
        template("prompts/research.jinja",
                 question = question,
                 results  = results),
        model = "claude-opus",
    )
§ runtime topology

Four ways to drive the same binary.

one-shot
$ chidori run agents/x.star --input k=v
Execute once. Writes session.json on exit. Best for scripts and CI.
serve
$ chidori serve agents/x.star --port 8080
HTTP server. Each POST /sessions becomes an event dict → agent(event).
replay
$ chidori replay session.json
Re-run from a saved log. Cache hits for every call. $0.00, identical output.
resume
$ chidori resume session.json --input answer.json
Continue a session paused on input(). Resumes at the exact seq.

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 Starlark, not Python?
Starlark is Python-shaped but has no clocks, no random, no I/O. That eliminates the entire class of nondeterminism that breaks replay. Anything you would reach for in Python is either expressible in Starlark or available as a host function.
Why content-addressed?
Caching by argument hash means a replayed call with the same inputs returns the same output regardless of when or where it ran. Two sessions that share a prefix share their cache.
Why no async?
Replayability requires a total order over side effects. parallel() handles fan-out without surfacing event-loop primitives the user has to reason about.
Why a separate language for prompts?
It isn't separate. A prompt is a string in Starlark. template() is a pure renderer. There's no DSL, no YAML, no graph builder.
Pair Chidori with Tael so every prompt, tool, and human-input step lands as a queryable span.