Releases: jmccardle/tau
Release list
v0.9.4
v0.9.4 - the session that got long
0.9.3 asked whether a stranger could install tau and get a working
conversation. This release is about the hour after that: the things that work
on a five-message chat and stop working on a five-hundred-message one. An
800-message transcript took over four minutes to redraw, streaming throttled
to a few tokens a second, Esc discarded the turn, and the tree editor became
unreadable at exactly the size that makes it necessary.
Three things nobody planned turned up while building that, and each is a case
of tau being quietly wrong rather than slow: the system prompt tau composes
had never reached a model through the TUI, the token counter re-counted every
earlier turn on every turn, and a reasoning model's thinking was streamed into
a widget that was never on screen.
What the model is told
- THE PROMPT TAU BUILDS IS NOW THE ONE THE MODEL GETS. TauBackend composes
the real prompt - base text, the project's AGENTS.md / CLAUDE.md, and the
tool list - and then the TUI and tau -p each composed a SECOND string from
config["system_prompt"] and stored it as the session's first message,
which is what goes on the wire. The TUI also substituted "You are a
helpful assistant." when no prompt was configured, so the stored message
was never absent and the built one was never used: no project context and
no tool list had ever reached a model throughtau. This is the other
half of 0.9.3's context-files work, which built the prompt and then let
the frontends write over it. - Every frontend folds the configured prompt onto the MODEL ENTRY now, where
the backend reads it as custom_prompt and composes the context files and
the tool list around it. rpc_mode already did this and is the precedent. - {{fields}} in a system prompt: {{base_prompt}}, {{project_context}},
{{tools}}, {{tool_names}}, {{cwd}}, {{model}}. Naming a section MOVES it
rather than copying it, so a custom prompt can wrap tau's voice instead of
replacing it and choose where the tool list goes. Substitution runs on the
template only, never on the assembled prompt, so a {{tools}} inside a
project's AGENTS.md reaches the model as written. An unknown field raises;
a misspelled {{tols}} rendered literally would look exactly like a prompt
that worked.
Losing work
- ESC NO LONGER THROWS THE TURN AWAY. Aborting while a tool call's arguments
were mid-stream discarded the whole turn - the user's own prompt, the
assistant message, and every completed tool result - and reported the loss
as a JSONDecodeError traceback. The traceback was not a side effect of the
loss; it was the cause, because the session persists a turn as one batch
after the loop returns and the raise killed the local list. - The finalizer reads stop_reason. On an abort, a tool call that cannot be
finished is DROPPED - not repaired and not defaulted to {} - because a
half-streamed {"path": "/etc/pas turned into something is a call the model
never issued. usage.extra["dropped_partial_tool_calls"] records how many.
The same unfinishable buffer on a COMPLETE stream still errors. - A failed run carries what it finished, and the session persists it before
re-raising. Every outstanding tool_call_id is answered "Operation
aborted", above both executors, because the parallel one had no abort
check of its own and would have run the whole batch. - Verified against a real llama.cpp, which emitted 12 argument fragments for
one two-key tool call - the fragmentation the bug needed, and the thing a
single-chunk cloud response hides.
Long conversations
- Loading a saved chat was quadratic in its length: an 800-message session
took 251s to redraw, and it redrew on every resume, /compact, tree
navigation and elide. It is 0.24s now. The chat view mounts the last 4
user turns or 50 messages, whichever comes first, and writes a
"... N earlier - click to show them" row above them. This bounds what is
DRAWN and nothing else: the whole conversation is loaded, in the log, and
is what the model is sent. - The same mounted tree throttled the next turn's streaming to a couple of
tokens per second, which is what "text accumulates but doesn't display"
looked like from outside. Streaming after a reload is 22.4 tok/s
regardless of transcript length. - A running turn says what it is doing. Between Enter and the first answer
text nothing on screen changed, so a turn spent reasoning or waiting on a
thirty-second tool looked exactly like a turn that had died. The exchange
line now counts: "Working... 812 out ~143 chunks 0:47". The two numbers
are never added - "812 out" is the real output_tokens the server reported,
summed over finished completions; "~143 chunks" is the completion still in
flight, where no usage figure exists yet. - TOKEN COUNTS STOPPED COUNTING THE SAME PROMPT AGAIN EVERY TURN.
total_tokens is one completion's prompt plus its completion, and the
prompt is the whole conversation; two surfaces summed it across the
conversation anyway. A real 17-message session read 192.9k tokens for a
conversation that was 22.6k long. There are two numbers now, never their
sum: ctx (the prompt last sent, carried as a replace) and out (which sums). - Underneath it, the cached span was billed twice. OpenAI and Google both
report cached tokens INSIDE the prompt count, and tau copied them to
cache_read_tokens without taking them back out of input_tokens, so
compute_cost_usd charged them at the input rate and again at the
cache-read rate. Both providers subtract now. - REASONING TEXT REACHES THE SCREEN. A reasoning model's thinking
accumulated into a widget that had never been mounted: the region was
assigned before the mount that raised, so every later call took the
"already exists" branch and returned a widget nobody could see. Measured
at 0 of 28 tokens visible. Tool-call boxes had the same bug, and a tool
result written before its box mounted was silently dropped by
Markdown.update. The test suite could not see any of it, because it paused
after every event on the assumption that a real backend never bursts.
The tree browser
- Indentation counts forks, not messages. One parentId level used to be one
widget level, so a 25-message conversation drove the label width to zero. - A turn folds. Your message owns the turn it started - the reply, every
tool call and every result are its children, starting folded - and the
next thing you asked is its SIBLING, so a hundred turns is a hundred rows
rather than a hundred levels of indent. - navigate rows are gone. They record that the cursor moved and carry no
message. Two exceptions keep their row: the current node, and one with
more than one child, which is a real branch point. - Enter on one of your own messages forks from its parent and brings the
message back in the input box to edit, because continuing from below a
user message would put two user turns in a row. - Only a row with something under it has an expand arrow, and those rows get
the two cells back for their preview. - Rows say what they are, in colour: the user: / assistant: / toolResult:
tag is painted in the hue that role wears in the transcript, and
bookkeeping entries take a quiet grey. - A branch summary is drawn as a pair with the branch it looks back on, and
hovering a row traces where it leaves your path. - The detail pane folds away with ctrl+D or a double-click on its border.
- Every row was one or two cells too wide, because the labels were sized
against a width that did not subtract the vertical scrollbar. - ELIDING IS ONE KEY IN THE BROWSER. It was a button on the action chooser
that re-opened the whole tree to ask for the second node, and reported an
illegal pair after both screens had closed. It is ctrl+E now. The two ends
bracket what is KEPT: given [1,2,3,4,5,6], pairing 2 with 4 leaves
[2,3,4]. You do not have to remember which end you picked first - the two
ends are always an ancestor and a descendant, so the tree decides. While
one row is marked, every row that cannot be the other end is greyed out. - The offer line says what will happen before you press anything:
"ctrl+E: keep this span, drop the other 3 entries, and move back to it".
It used to read "elide 37 messages", which invites the opposite reading
and undercounted besides. - docs/TREE-EDITOR-MANUAL.md is the manual, and matches.
Keys
- ctrl+C was bound straight to quit, so one mistimed press ended a session
with a draft still in the input. It now stops a running turn, or clears
the input, or offers "press ctrl+C again to exit", or quits - in that
order, depending on where you are. - Esc with nothing generating used to do nothing at all. It offers the tree
browser, and a second press within three seconds opens it. Pressing either
key withdraws the other's offer: there is one header line, and an offer
you can no longer see should not still be answerable.
Appearance
- Four themes, swappable live: mocha (default, unchanged from 0.9.3), latte
(light), gruvbox, and ansi - which uses your terminal's own 16 colours and
paints no backgrounds, because it cannot know whether the terminal is
light or dark. parley.tcss names 25 role variables and contains zero
colour literals, and a test fails on any that appear. - Choose one with "theme" in ~/.tau/config.json, --theme NAME for one run,
or ctrl+p in session, which saves the choice. Write your own as
~/.tau/themes/.json with extends/palette/textual keys. - A broken theme file no longer stops tau from starting: it raises one error
toast naming the file and runs in mocha. A broken file named after a
built-in leaves the built-in standing...
v0.9.3
v0.9.3 - three vendors, context files that arrive, and --resume
0.9.2 asked what a stranger's first hour looks like. This release is the
answer, plus two things nobody planned: tau's own system prompt had never
reached a model, and every model was being served over the OpenAI wire
whatever it declared.
Vendors
- tau speaks three wire protocols now: openai-completions (unchanged, still
the default), anthropic-messages, and google-generative-ai. The two vendor
SDKs are optional extras — ffwf-tau-llm[anthropic] and ffwf-tau-llm[google]
— imported lazily on first request, so a plain install pulls neither. - tau dispatches on Model.api. It used to construct OpenAICompletionsProvider
unconditionally and read model.api not at all, so a model declaring a
protocol tau has never implemented was served over the completions wire in
silence. An unknown api now raises, naming what is registered. - Model.api, AssistantMessage.api and .provider widen to str. The last was
pinned to "openai", so a legal Model naming any other vendor raised a
validation error when the vendor was copied onto the message. - The Google vendor registers as
gemini, matching the backend value config
entries already carry. - Reasoning signatures are namespaced by vendor, and the OpenAI writer
REFUSES a foreign one rather than forwarding a token another vendor will
reject. A Gemini 3 function-call signature is replayed on every
reasoning_replay setting including "off" — it is protocol, not
chain-of-thought, and omitting it is a 400. - Three capability questions were settled by measurement against the live
API rather than by reading. Records are in docs/probe-results/.
The system prompt and project context files
- BEFORE THIS RELEASE NEITHER HAD EVER REACHED A MODEL on the TUI or
headless path. The backend passed a config key straight through and never
called the loader behind it. - Discovery now walks the agent dir, then every ancestor of cwd, root-most
first, one file per directory, first match among AGENTS.override.md,
AGENTS.md, AGENTS.MD, CLAUDE.md, CLAUDE.MD. CLAUDE.md was not a name tau
knew at all. A worktree nested in its own repo shadows the repo's copy. - Context files COMPOSE with system_prompt instead of being switched off by
it. --no-context-files / -nc turns discovery off. - A found-but-unreadable file raises, naming the path. Every block is
wrapped in <project_instructions path="..."> so a prompt cannot carry
instructions whose origin it does not state. - A real coding-agent prompt ships, and tau_default_config.json no longer
carries a system_prompt key.
Sessions
- --resume works. It had been rejected in both modes since it was added,
with help text saying "TUI only" and an error blaming headless mode. - A session picker, with fuzzy filter and a Tab that widens the scope from
this directory to all. --resume, /resume and the command palette are one
handler with three bindings. - The sidebar starts closed.
- The branchOf lane tag is gone. Three of its four consumers used it to ask
"does this belong to the conversation being looked at", which is ancestry
from the cursor — so a three-way fork returned three mutually exclusive
alternatives as one conversation.
Backends that are not quite OpenAI-shaped
- Model.stream (default true) — a gateway with no SSE was unreachable
through any config path. - Model.request_timeout — 300s read and 10s connect were fixed at client
construction with no override anywhere. - context_window and max_tokens are reachable; they were hardcoded at 128000
and 4096 for every model in existence. python -m tau_llm.catalogfills a config entry from models.dev. Nothing
is vendored; --base-url is required and never guessed.- Four failures that used to arrive unattributable: a tool call with no name
was built and executed (now raises, naming the call id, model and base
URL); an error could carry no content at all; a keepalive frame crashed
the turn; the timeout was unreachable.
SDK
- create_agent_session takes no_tools ("all" | "builtin" | None) and
no_context_files. Passing tools= and no_tools= together raises.
JMFTS store
- Stop using a character count as a proxy for a token window; ask the
server. 1800 characters of base64 is about 1350 tokens against a 512-token
limit, so dense content took the "short enough to embed whole" path and
was refused.
Packaging and release
- Python 3.11, 3.12, 3.13 and 3.14 are measured, not claimed.
- dist/ is gitignored.
- The missing-SDK error hint named tau-llm[google], which does not resolve —
tau imports as tau_llm and publishes as ffwf-tau-llm. Fixed, with a test
that reads the four distribution names out of the pyproject.toml files.
Upgrade notes
- SessionLog implementors: lane= is gone from the Protocol and every shipped
store. resolve_cursor is "last entry wins" again. One contract test is
inverted — a store must not reintroduce a cursor filter. - The default system prompt changed, and system_prompt is no longer in the
shipped config. - A CLAUDE.md or AGENTS.md above your working directory is read now, on
every run, all the way to /. -nc turns discovery off. - A gateway that omits function.name now raises on the first call instead of
burning up to max_turns. - A model config naming an api tau does not implement now raises.
- create_agent_session(tools=..., no_tools=...) now raises.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
v0.9.2
v0.9.2 - installable from PyPI, and a TUI that fits the terminal
Packaging
- tau_ai is now tau_llm. The four distributions carry an ffwf- prefix:
ffwf-tau-llm, ffwf-tau-agent-core, ffwf-tau-coding-agent, ffwf-tau-jmfts.
tau-llm and tau-ai on PyPI are other people's projects. - textual, the JMFTS store and the NATS bus are optional extras. Plain
pip install ffwf-tau-coding-agentgives a headless tau; [tui] adds the
interactive one. typer is gone. - Both
tauandffwf-tauland on PATH. They are the same entry point. - MIT per package, held by Fight Fire with Fire Robotics, LLC.
- A GitHub Actions pipeline builds and publishes all four through PyPI
Trusted Publishing (OIDC, no stored token). bump-version.sh writes the
eleven places a version literal lives, and a test fails until they agree. - tau also ships as a container.
Command line
- --no-tools and --no-builtin-tools are now different flags. --no-tools
offers the model nothing at all; -nbt drops only the built-in set and
keeps extension-registered tools. -nbt used to degenerate to --no-tools,
so a caller that registers its capability through extensions and passes
--no-tools now gets zero tools and wants -nbt instead. - --tools is run-level policy, not a per-model override.
Tools
- One tool hierarchy across the three layers, and every shape emits a JSON
schema. define_tool() builds and validates a real ToolDefinition. - The assistant's tool-call message is sent on the wire.
TUI
- A density pass: markdown by source, zero block margins, the /tree session
browser with a detail pane, an empty chat pane that states the config. - Dialogs fit the terminal and sit in the middle of it. The sidebar and the
extension panel lay out to the width they are given. - Deterministic snapshot tests, and headless visual-inspection tooling to
produce them.
Examples and docs
- Five shipped examples did not run. They run now, and a contract test loads
and registers every numbered example on each suite run. - The README absorbs ARCHITECTURE.md and MONOREPO-STRUCTURE.md. The package
docs were rewritten from a code audit; tau-api-reference.md is retired.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
v0.9.1 - database, RPC, and extension overhaul
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0.9.0, initial public release
v0.9.0 - initial commit Co-Authored-By: Qwen 3.6 35B-A3B (256k context) <qwen36-35B-IQ4_XS.gguf@ffwf.net> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>