Tau — Reference¶
Cognition × Application
Extension API, session tree format, configuration keys, and CLI flags.
Draft
Built for lookup, not narrative reading. Written from the source repo's own
docs/tau-llm.md, docs/tau-agent-core.md, docs/tau-coding-agent.md,
docs/extensions.md, and docs/REMOTE-CONTROL.md — each of those is
itself checked against running code, not aspirational. tau --help is the
exact contract for the CLI table below; treat a disagreement as this page
being stale.
Packages¶
Four distributions, each installable on its own. The ffwf- prefix is not
decoration: tau-ai and tau-llm on PyPI are unrelated third-party projects,
so pip install tau-llm fetches someone else's code.
| Distribution | Imports as | What it is |
|---|---|---|
ffwf-tau-llm |
tau_llm |
Provider and streaming layer. |
ffwf-tau-agent-core |
tau_agent_core |
Agent loop, tools, sessions, extensions. Headless. |
ffwf-tau-coding-agent |
tau_coding_agent |
The tau command and the Textual TUI. |
ffwf-tau-jmfts |
tau_jmfts |
JMFTS-backed session store. |
Installing the top of the stack pulls the rest:
pip install 'ffwf-tau-coding-agent[tui]'
Everything past the headless core is an extra, and each one reports its own absence with the install command rather than a traceback:
| Extra | Adds | Needed for |
|---|---|---|
ffwf-tau-coding-agent[tui] |
textual, rich |
the interactive TUI. tau -p and tau --mode rpc run a full turn without it. |
ffwf-tau-coding-agent[jmfts] |
ffwf-tau-jmfts |
--store jmfts. |
ffwf-tau-agent-core[bus] |
nats-py |
the built-in nats_bus extension. |
ffwf-tau-agent-core[testing] |
pytest |
importing tau_agent_core.testing. |
The install puts two console scripts on PATH — tau and ffwf-tau, the
same entry point behind each. Type tau; write ffwf-tau in scripts, systemd
units and Dockerfiles. See the
DevOps Manual for why.
Provider layer (tau_llm)¶
τ speaks the OpenAI-compatible chat-completions API. Model carries the
usual fields (id, provider, base_url, context_window, max_tokens)
plus four τ-specific ones:
| Field | Meaning |
|---|---|
reasoning_replay |
"turn" (default) or "all". Whether a prior turn's chain-of-thought is resent on every later call. τ defaults away from pi's always-resend behavior — measured 72%→28% of payload size on a real transcript. |
grammar_dialect |
Which constrained-decoding grammar flavor the target server speaks. |
extra_body |
Arbitrary per-model JSON merged into every request body — the escape hatch for a server-specific field (e.g. chat_template_kwargs) the provider has no first-class knob for. |
server_features |
Declares which optional server behaviors (e.g. native structured output) this endpoint actually supports, rather than τ probing for them. |
A Provider is one method: stream_chat(messages, tools, model, options) ->
AsyncIterator[StreamEvent]. There is no separate api/name property on
the interface — a provider is identified by the Model.provider string that
selects it.
Providers are pooled, not registered. An earlier registry design was
built, found to construct a fresh empty registry on every call (so it never
actually cached anything) and measured at 42ms of overhead per call, then
deleted outright. tau_llm.client's connection pool (keyed on
provider+base_url+api_key) replaced it.
Tool argument validation is hand-rolled, not jsonschema/pydantic —
validate_tool_arguments checks type and required from a plain-dict
schema and nothing else: no minLength, no minimum, no enum. Writing one
of those into a tool schema looks enforced and is silently ignored.
define_tool() builds the definition and validates it, raising on a malformed
one rather than handing back a tool that fails later:
from tau_llm import define_tool
word_count = define_tool(
name="word_count",
label="Word count",
description="Count the words in a string.",
parameters={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
execute=lambda text: {"words": len(text.split())},
)
It returns a ToolDefinition. A single mapping may be passed positionally
instead of keywords.
There is one tool hierarchy, three layers deep, each layer adding what that layer owns:
| Class | Package | Adds |
|---|---|---|
ToolDefinition |
tau_llm |
the wire-facing shape — name, label, description, parameters, execute |
ToolDefinition |
tau_agent_core |
identity by name, so the loop can dedupe and shadow tools |
ExtensionToolDefinition |
tau_agent_core |
source, recording which extension registered it |
AgentTool.definition names the base, so any of the three fits. What is
not interchangeable is execute: the tau_llm form is called with the
tool's own arguments, and the extension form is called
execute(tool_call_id, params, signal, on_update, ctx). AgentSession
adapts between them when it resolves an extension tool. Do not pass a
define_tool() result to api.register_tool() — the shape validates and
the call signature does not.
ToolSpec is a Protocol in tau_llm naming the three members a provider
actually reads (name, description, parameters). It is what the
provider layer annotates against, so anything with those three may be sent
regardless of which package built it.
Agent loop & sessions (tau_agent_core)¶
AgentLoop.__init__(self, config: AgentLoopConfig, emit=None, tools=None,
model=None, abort_signal=None, hook_dispatcher=None, steer_queue=None) —
config/tools/model/abort_signal are constructor arguments, not
fields on one shared object. AgentLoopConfig is deliberately small:
model, system_prompt, tool_execution_mode, max_retries, max_turns,
temperature, api_key, reasoning. Everything an earlier design put on
the config as callbacks (before_tool_call, get_steering_messages, …) is
now injected instead — extension hooks go through hook_dispatcher
(gated on has_hook_handlers(), so a zero-extension session pays nothing
for it), mid-turn steering goes through a shared steer_queue.
Sessions are a tree, not a chat log. Append-only JSONL; a
ConversationTree walks parent_id chains to build model input for the
active leaf — an ephemeral frame (system prompt, tool schemas) plus the
exact linear path to that leaf, no hidden channel either side of that. A
SessionLog Protocol (append_message, append_navigate,
append_branch_summary, entries, cursor, …) is the minimal contract
both the SDK's in-memory log and the TUI's file-backed one satisfy.
The one door. Every input source — TUI keystrokes, tau -p, the SDK, an
extension, an RPC client — funnels through AgentSession.submit().
multitask_strategy decides what happens when a submission arrives while a
turn is already running:
| Strategy | Effect |
|---|---|
reject |
Refuse the new submission outright (rejection_reason explains why). |
enqueue |
Queue it; it runs after the current turn ends. |
steer |
Injected into the running turn without waiting for it to end. |
rollback |
Navigate back to the pre-turn leaf, then run as if from there — the running turn's output becomes an abandoned sibling branch, not a deletion. |
fork |
Branch the conversation instead of extending the active leaf. |
prompt() is a thin wrapper that builds a Submission and calls submit()
— not a second door.
SDK entry point:
def create_agent_session(
model: str | Model = "gpt-4o", provider: str = "openai",
base_url: str | None = None, api_key: str | None = None,
tools: list[str] | None = None, session_log: SessionLog | None = None,
extensions: list[Callable] | None = None, system_prompt: str | None = None,
thinking_level: str = "off", cwd: str | None = None,
tool_execution_mode: Literal["sequential", "parallel"] = "parallel",
compaction_policy: CompactionPolicy | None = None,
bus_available: bool = False,
) -> AgentSession
tools takes built-in name strings only (read, write, edit, bash,
grep, find, ls) — a custom AgentTool instance needs the
AgentSession constructor directly. There is no settings= parameter: an
earlier version had one that was silently ignored, so it was removed rather
than kept as a no-op — passing it raises TypeError.
Compaction is LLM-backed with no fabricated-summary fallback:
should_compact(context_tokens, context_window, settings) -> bool;
prepare_compaction(path_entries, settings) -> CompactionPreparation | None;
async def compact(preparation, model, api_key, *, custom_instructions=None,
thinking_level=None) -> CompactionResult. A compaction error raises rather
than silently truncating the conversation.
Extension API¶
Extensions are plain Python modules — importlib, no compile step, no
manifest language. A module exposes def register(api): ...; api is an
ExtensionAPI.
# Events — subscribe a mutating hook or a notify-only event through the
# same call; api.on returns an unsubscribe callable.
api.on(event: str, handler: Callable) -> Callable[[], None]
# Tools
api.register_tool(definition: dict | ExtensionToolDefinition) -> None
api.get_all_tools() -> list[Any]
api.set_active_tools(names: list[str]) -> None
# Commands and shortcuts
api.register_command(name: str, command: dict) -> None
api.register_shortcut(...)
# Session state
api.append_entry(custom_type: str, data: dict) -> None # durable, not RAM-only
api.set_session_name(name: str) -> None
api.get_session_name() -> str | None
# Messaging and turn origination — the "one door" for extensions
api.send_user_message(content: str, deliver_as: str = "followUp") -> None
await api.submit(...)
api.submit_threadsafe(...)
# Inter-extension pub/sub
await api.emit(topic: str, payload: Any) -> None # ext:<name>:<topic> channels
# Per-extension config
api.config -> dict[str, Any] # ~/.tau/config.json → "extensions.<name>", or --ext-config
api.ui -> ExtensionUI
api.context -> ExtensionContext
Mutating hooks (handler's return value is applied) — tool_call
(veto/patch args), tool_result (modify content/terminate), before_agent_start
(contribute to the system prompt), input (transform the user prompt),
turn_end, user_turn_end, session_before_switch (veto a session
new/fork/switch operation).
Lifecycle hooks — session_start, session_shutdown (carries a
reason, e.g. "reload").
Notify-only events — agent_start, agent_end, turn_start,
message_start/message_update/message_end,
tool_execution_start/tool_execution_update/tool_execution_end, plus the
wildcard "all".
One sharp edge: api.on("turn_end", ...) always resolves to the
mutating hook — there is no way to observe a plain notify-grade
turn_end via api.on. Use "all" or AgentSession.subscribe() for pure
observation.
Discovery — the global directory ~/.tau/extensions/ (loaded
alphabetically), then every explicit -e/--extension PATH (repeatable).
No project-local <cwd>/.tau/extensions/ discovery, and no
dependency-manifest convention inside an extension directory. Collision
handling differs by kind: a duplicate tool name raises ValueError at
load time; commands are silent last-write-wins; shortcuts are
last-write-wins with a logged warning.
Headless dialogs raise, not guess. In headless mode, ctx.ui.confirm /
select / input / form raise HeadlessDialogError unless a policy is
configured (--ui-defaults METHOD=ANSWER,... or config.json's
ui_defaults).
RPC¶
--mode rpc speaks JSON-RPC 2.0 over stdio (newline-delimited). 20 verbs
are implemented, 7 are formally declined with a stated reason rather than
silently absent:
| Tier | Verbs |
|---|---|
| A/C — turn + state | submit, prompt, abort, get_state, get_messages, get_commands, get_tools, get_capabilities, new_session, fork, switch_session |
| B — session management | compact, set_model, get_models, set_session_name, get_session_name, get_session_stats, list_sessions, set_auto_compaction, get_last_assistant_text |
submit/prompt answer twice: an immediate acceptance response, then a
later agent_end notification when the turn finishes. A rejected submission
errors on the response instead (-32000 SUBMISSION_REJECTED), with no later
event.
Seven verbs are declined, each reachable via get_capabilities().declined[]:
| Verb | Why declined |
|---|---|
send_tool_result |
A second, unauthenticated path into the tool executor a host never drove the call for. |
bash |
Same reasoning — an out-of-band shell verb bypasses the loop's own admission rules. |
cycle_model |
A TUI keybinding affordance, not a protocol verb — a host names a model via set_model instead. |
cycle_thinking_level |
Same judgment; τ also has no thinkingLevel concept on AgentSession for a set_* verb to act on. |
set_steering_mode |
multitask_strategy is already a per-submission parameter, not a session-wide mode to toggle. |
set_follow_up_mode |
Same — multitask_strategy="enqueue" already covers it per-call. |
export_html |
Rendering is the host's job; τ hands back get_messages and events. |
The source repo's docs/RPC-PROTOCOL.md is the exact, generated, test-locked
wire reference (a drift test asserts the checked-in file equals its own
render() output) — treat the table above as an index into it, not a
replacement.
CLI flags¶
| Flag | Short | Notes |
|---|---|---|
--print |
-p |
run one turn headlessly, print, exit |
--mode {text,json,rpc} |
headless output format; rpc doesn't combine with --print |
|
--model |
-m |
config key or provider/id shorthand |
--provider |
long-only | |
--tools/--no-tools |
-t/-nt |
allowlist / offer the model no tools at all, built-in or extension-registered |
--exclude-tools |
-xt |
denylist |
--no-builtin-tools |
-nbt |
drops the built-in set only; extension-registered tools survive and are still offered |
--extension PATH (repeatable) |
-e |
explicit load, always runs even under --no-extensions |
--no-extensions |
-ne |
disables discovery only |
--bus |
declare this run may reach a message bus, so TOUCHES_BUS extensions (e.g. nats_bus) are allowed to load |
|
--ext-config NAME.KEY=VALUE (repeatable) |
per-extension config override, CLI wins over config.json |
|
--ui-defaults METHOD=ANSWER,... |
headless dialog auto-answers; --print only |
|
--system-prompt / --append-system-prompt (repeatable) |
||
--continue / --resume / --session REF / --fork REF |
-c / -r |
mutually exclusive. --resume is TUI-sidebar-only — it raises at the CLI |
--name |
-n |
session display title |
--no-session |
ephemeral, no persistence | |
--thinking {off,minimal,low,medium,high,xhigh} |
requires a reasoning-capable model | |
--store {file,jmfts} |
session backend for this run | |
--session-dir DIR |
file store only; default differs by mode (~/.tau/sessions vs. a private RPC temp dir) |
|
--import-session PATH / --export-session REF PATH |
JMFTS store transfer, then exit | |
--verbose |
long-only — -v is --version, not verbose |
|
--help / --version |
-h / -v |
See the DevOps Manual for how these fit into a deployment, and the Cookbook for worked extension examples.