Skip to content
Markdown

Tau: a minimal, readable coding agent

Scope: Tau (huggingface/tau, PyPI tau-ai, Apache-2.0), a small terminal coding agent designed to be read as an example of how coding agents are built. This page covers its three-layer split (portable brain, coding app, TUI), the provider-neutral agent loop and its turn and tool semantics, and how to use it as a reference harness. It is the readable-reference entry among the harness pages in agentic systems, a minimal counterpart to the OpenHands platform and harness architecture, and it shares the Pi lineage with Shannon and Prime Agent.

Examined at commit 0217633f (2026-08-07) of huggingface/tau, version 0.3.8, Apache-2.0. Tau is "a Python port of Pi's minimalist coding agent." The turn-and-tool state machine below is executed and asserted as standalone Python (a faithful model of src/tau_agent/loop.py); separately, the real upstream run_agent_loop was driven directly with a scripted provider and a fake tool, confirming turn accounting, the max_turns hard stop, unknown-tool handling, and the empty-error-turn filter. No live model was called.

What it is

Tau is a coding agent that lives in your terminal: you ask it to explain a repo, add tests, or fix a stack trace, and it reads files, edits code, runs commands, and streams what it is doing while keeping a durable session history. Its stated second purpose is pedagogy: it is meant to be read, a teaching project for understanding the shape of a coding-agent system without starting from a giant production codebase.

The design is a clean three-layer split:

  • tau_ai translates model providers (Anthropic, OpenAI, Google, Mistral, OpenAI Codex) into Tau's provider-neutral stream.
  • tau_agent is the portable brain: messages, tools, events, the loop, the harness, and session primitives. It knows nothing about Textual, Rich, config paths, or slash commands.
  • tau_coding wraps the brain as a real coding app: CLI, TUI, file and shell tools, provider config, project instructions, skills, and on-disk sessions.

The boundary the project emphasizes: AgentHarness is the reusable brain, CodingSession is the coding-agent environment, and the TUI is one possible frontend that consumes events. This is the separation most production harnesses blur, which is what makes Tau useful to read.

Why use it

  • A loop you can actually read. src/tau_agent/loop.py is around 320 lines and captures the whole perceive-act-observe cycle: model call, tool dispatch, turn accounting, termination. It is a faithful reference for how an agent loop is structured.
  • Provider-neutral by construction. The tau_ai layer normalizes every provider into one event stream, so the brain never branches on vendor. That is the right shape for a multi-provider harness.
  • Events, not rendering. Frontends consume a typed event stream (AgentStartEvent, TurnStartEvent, ToolExecutionEndEvent, and so on), so you can build a different UI or a headless driver without touching the brain.
  • Durable, resumable sessions. History persists, including terminal failures for diagnostics, and the loop is careful to keep those failures out of the next model request.
  • Small and Apache-2.0. It installs a tau command from PyPI and is easy to fork as a starting point.

When to use it (and when not)

Use it to learn how a coding agent is built, to prototype a harness idea against a clean brain, or as a lightweight terminal coding agent when you want something readable and hackable. The layer split makes it a good base for a custom frontend or a headless automation that drives the loop directly.

Do not expect production feature depth: it is a minimalist port at 0.3.x, not a hardened platform (for that, see OpenHands). Do not run it against repositories or with tools you do not trust without a sandbox: like any coding agent, it executes model-chosen shell commands and edits, so it needs the same sandboxing and permission gating as any other. Do not treat the API as stable at this version; the internals are meant to be read and will evolve.

Architecture

flowchart TB
  subgraph AI["tau_ai"]
    PROV["Provider adapters -> neutral stream"]
  end
  subgraph AGENT["tau_agent (portable brain)"]
    LOOP["run_agent_loop"]
    TOOLS["Tool dispatch"]
    MSG["Messages + events"]
  end
  subgraph CODING["tau_coding (app)"]
    CLI["CLI + TUI"]
    FT["File + shell tools"]
    SESS["On-disk sessions"]
  end
  PROV --> LOOP
  LOOP --> TOOLS
  TOOLS --> MSG
  AGENT --> CODING

The agent loop, executed

A turn is one model response plus any tool executions it requests. The loop repeats while the assistant keeps calling tools, stops at a final text turn, and enforces max_turns as a hard stop. It also filters empty error or aborted turns out of the provider context so a failed turn does not poison the next request, while keeping it in durable history. The model below reproduces that state machine.

def run_loop(scripted, tools, max_turns=None):
    """scripted: list of ('tool', name) or ('text', reply) per model call."""
    history = [("user", "start")]
    model_calls = tool_execs = turn = 0
    while True:
        turn += 1
        if max_turns is not None and turn > max_turns:
            history.append(("assistant", f"error: stopped after max_turns={max_turns}"))
            return {"calls": model_calls, "tools": tool_execs, "stop": "max_turns",
                    "history": history}
        kind, payload = scripted[min(model_calls, len(scripted) - 1)]
        model_calls += 1
        if kind == "text":
            history.append(("assistant", payload))
            return {"calls": model_calls, "tools": tool_execs, "stop": "final",
                    "history": history}
        history.append(("assistant", f"call:{payload}"))       # tool-using turn
        history.append(("tool", tools.get(payload, f"error: tool {payload} not found")))
        tool_execs += 1


def provider_context(history):
    """Mirror _provider_context: drop empty error/aborted assistant turns."""
    return [(r, c) for (r, c) in history
            if not (r == "assistant" and c == "error:")]


tools = {"echo": "ran echo"}

# 1. Two tool turns then a final text turn: 3 model calls, 2 tool execs.
r = run_loop([("tool", "echo"), ("tool", "echo"), ("text", "done")], tools)
print(f"case1: calls={r['calls']} tools={r['tools']} stop={r['stop']}")
assert r["calls"] == 3 and r["tools"] == 2 and r["stop"] == "final"

# 2. max_turns hard stop when the model never stops asking for tools.
r2 = run_loop([("tool", "echo")], tools, max_turns=2)
print(f"case2: calls={r2['calls']} stop={r2['stop']}")
assert r2["calls"] == 2 and r2["stop"] == "max_turns"

# 3. Unknown tool: the loop records an error result and continues to a final turn.
r3 = run_loop([("tool", "nope"), ("text", "ok")], tools)
assert any("not found" in c for _, c in r3["history"]) and r3["stop"] == "final"
print(f"case3: unknown tool handled, stop={r3['stop']}")

# 4. Empty error turn is filtered from provider context (kept in history).
poisoned = [("user", "hi"), ("assistant", "error:"), ("assistant", "real answer")]
assert provider_context(poisoned) == [("user", "hi"), ("assistant", "real answer")]
print("case4: empty error turn filtered from provider context")
print("OK: a turn is model-response + its tool execs; the loop halts on a text turn "
      "or max_turns, and empty failures stay in history but out of the next request")

Executed output:

case1: calls=3 tools=2 stop=final
case2: calls=2 stop=max_turns
case3: unknown tool handled, stop=final
case4: empty error turn filtered from provider context
OK: a turn is model-response + its tool execs; the loop halts on a text turn or max_turns, and empty failures stay in history but out of the next request

Driving the real run_agent_loop from tau_agent gave the same results: a scripted provider that requested two tool calls then returned text produced three model calls and two tool executions; max_turns=2 against a provider that always asked for a tool terminated with the assistant message "Agent stopped after max_turns=2"; an unknown tool name yielded is_error=True and the loop continued; and an empty error assistant turn terminated in a single turn. The loop's _provider_context filter is what keeps that empty failure out of the next request.

How to use it

Tau installs from PyPI as tau-ai (Python 3.12-plus), preferably via uv:

uv tool install tau-ai      # installs the `tau` command
tau                          # start the terminal agent in the current directory

It reads project instructions and skills from the working directory, streams tool activity in the TUI, and persists sessions on disk so you can resume. Provider selection and keys are configured through tau_coding.

How to develop with it

The point of the layer split is that you can consume the brain without the app. Import tau_agent and drive run_agent_loop with your own ModelProvider and AgentTool implementations to build a headless agent or a different frontend; the loop emits the typed event stream and returns the accumulated messages. Add a tool by implementing the AgentTool execute contract (id, arguments, cancellation signal, progress callback). Read loop.py, harness.py, and messages.py together to see how a turn, a tool result, and the durable transcript relate. The dev-notes and docs/internals/architecture are the project's own guide to the internals.

How to maintain it

Pin the tau-ai version; at 0.3.x the internals are explicitly a moving teaching target. When you fork it as a base, track upstream's tau_agent changes separately from your tau_coding customizations, since the brain is where the load-bearing semantics (turn accounting, context filtering) live. The provider adapters in tau_ai are the surface most likely to need updates as provider APIs change.

How to run it in production

Tau is a reference and a lightweight tool, not a production platform, so "production" here means embedding its brain in your own automation. Whatever you build, keep the coding-agent safety basics: run the file and shell tools inside a sandbox, gate destructive actions behind approval, and restrict egress with a runtime enforcement layer. The loop's max_turns is your bound on runaway iteration; set it. For a fuller production harness, this knowledge base's OpenHands platform and harness architecture pages cover the additional machinery.

Failure modes

  • Executes model-chosen actions. Like any coding agent, it runs shell commands and edits files. Untrusted repos or tools without a sandbox are a risk.
  • Early-version API churn. At 0.3.x the internals change; code against a pinned version.
  • Minimalist by design. It lacks the guardrails, policy hooks, and orchestration of a production platform; those are your responsibility if you deploy the brain.
  • Provider drift. The tau_ai adapters track external provider APIs and can lag a provider change.

References

  • Tau repository (Hugging Face), pinned commit 0217633f: https://github.com/huggingface/tau
  • Agent loop (src/tau_agent/loop.py): https://github.com/huggingface/tau/blob/main/src/tau_agent/loop.py
  • Documentation and architecture: https://twotimespi.dev/internals/architecture/
  • PyPI: https://pypi.org/project/tau-ai/
  • Pi (the project Tau ports): https://github.com/badlogic/pi-mono

Related: Agentic systems index · Harness architecture · OpenHands agent platform · The agent loop · Self-improving RLM agent (Prime Agent) · Tools and function calling