An open-source coding agent, written to be read

A coding agent you can read in an afternoon.

Tau is a small, working coding agent in Python. It talks to a model, reads and edits your files, and runs shell commands from your terminal — with no framework hiding how any of it works.

Use it as a tool. Read it as a textbook. Fork it as a starting point.

promptmodeltoolresultagent_starttext_deltatool_calltool_execution_end0τ/4τ/23τ/4θ = 0.00
Fig. 1 — one turn of the loop is τ
$ curl -LsSf https://twotimespi.dev/install.sh | sh
1

An agent is a loop

Definition 1.1A coding agent is a language model run in a loop, where each reply may ask for a tool — read a file, edit it, run a command — and each tool result is fed back as the next input. The loop stops when the model answers without asking for anything.

That's the whole idea. Everything else in Tau — sessions, skills, the terminal UI — is built around this loop, not inside it.

Read the loop →

your promptmodelstreams eventstool calls?noansweryesrun toolsread · edit · bashappend resultrepeat until the model stops asking.
Fig. 2 — the agent loop
2

Three layers, nested

Tau is three Python packages, nested like boxes. Each one controls its own job and only uses what's inside it — so you can learn, or replace, them one at a time.

youtau_codingthe app — touches your machine· reads & edits files· runs shell commands· saves sessions to disk· skills & slash commands· the terminal UItau_agentthe loop — pure logic· the transcript· tool calls & results· cancellation· queued promptstau_aithe model· provider adapters· token streaming· one event formatthe only way out to a modelmodel APIsOpenAI · Anthropic · localeach box only imports what’s inside it.
Fig. 3 — what each layer controls
3

Everything is an event

The agent doesn't print, draw, or save anything itself. Each layer emits small, typed events, and the layer above turns them into its own. Whoever listens at the top — a one-shot CLI, the terminal UI, your own frontend, or a JSONL session file — decides what to do with them.

tau_codingthe apptau_agentthe looptau_aithe modelbecame textrun read(README)savedsavedtimeprompt: “fix the typo in README”the TUI and session.jsonl listen up here ↑entry_appendedentry_appendedagent_settledmessage_updatetool_execution_starttool_execution_endturn_endresponse_starttext_deltatool_callresponse_end
Fig. 4 — one short run, as events. Each layer listens to the one below and speaks its own vocabulary.
4

A tool is just a function

Where do tool calls come from? A tool is half schema — a name, a description, and parameters the model can read — and half plain async function. This is a complete Tau extension, from examples/extensions/hello_tool.py:

from tau_agent.messages import TextContent
from tau_agent.tools import AgentTool, AgentToolResult


async def _run_hello(tool_call_id, arguments,
                     signal=None, on_update=None):
    who = str(arguments.get("who", "world"))
    return AgentToolResult(
        content=[TextContent(text=f"Hello, {who}!")])


def setup(tau):
    tau.register_tool(AgentTool(
        name="hello",
        label="hello",
        description="Greet someone by name.",
        parameters={
            "type": "object",
            "properties": {"who": {"type": "string"}},
        },
        execute_fn=_run_hello,
    ))
modelcalls itthe model seesyour code runsback into the transcriptname="hello"description="Greet someone…"parameters={"who": string}_run_hello(args)AgentToolResultAgentTool"Hello, Ada!"
Fig. 5 — half schema, half function
5

A frontend in a dozen lines

The terminal UI is one listener among many. Anything that can loop over the session's events is a frontend — here is one that prints to stdout, using the real event names from §3.

async for event in session.prompt(text):
    match event.type:
        case "message_update":
            e = event.assistant_message_event
            if e.type == "text_delta":
                print(e.delta, end="")
        case "tool_execution_start":
            print(f"\n→ {event.tool_name}({event.args})")
        case "tool_execution_end":
            print("  ✗" if event.is_error else "  ✓")
        case "agent_settled":
            break
session.prompt(…)your screenyou decide what each event looks like.message_updatemessage_updatetool_execution_starttool_execution_endmessage_updateagent_settledI'll open the README…→ read(README.md) ✓Fixed the typo.— done —
Fig. 6 — a frontend is a match on event types
6

A syllabus

Seven files, read in order, take you from a single event to a working coding agent. Line counts are from the current source.

187 lines293 lines3118 lines4376 lines5259 lines6171 lines71,218 lines≈ 2,300 lines in all — about an afternoon.circle size ∝ √linesevents.py_provider_events.pytools.pyloop.pyharness.pyjsonl.pytools.py
Fig. 7 — the reading route
§LessonFileLines
1Events, the contracttau_agent/events.py87
2Models become eventstau_ai/_provider_events.py93
3What a tool istau_agent/tools.py118
4The looptau_agent/loop.py376
5The harnesstau_agent/harness.py259
6Sessions on disktau_agent/session/jsonl.py171
7Real toolstau_coding/tools.py1,218
7

Pi, and why τ

Tau began as a way to understand Pi, a small coding agent written in TypeScript. Pi keeps three things apart: the loop, the machine it works on, and the screen you watch it on. Tau keeps that split and rebuilds it in Python, with notes on every step.

The name is a joke that stuck. π is half a turn; τ = 2π is the whole circle. Pi was the first half of the trip — Tau tries to go the rest of the way round, slowly, and show its work. More on τ than you asked for →

Pihalf a turn · TypeScriptTauthe whole turn · Python, with notessame three partsthe loopthe machinethe screen0πτ = 2π
Fig. 8 — π gets you halfway
τ

Start with events, add a loop, give it tools, wrap it in a UI. That's a coding agent.

$ curl -LsSf https://twotimespi.dev/install.sh | shGet started →Join the Discord →