Skip to content

Releases: jmccardle/tau

v0.9.4

Choose a tag to compare

@jmccardle jmccardle released this 25 Aug 01:52

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 through tau. 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...
Read more

v0.9.3

Choose a tag to compare

@jmccardle jmccardle released this 22 Aug 21:24

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.catalog fills 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

Choose a tag to compare

@jmccardle jmccardle released this 21 Aug 10:43

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-agent gives a headless tau; [tui] adds the
    interactive one. typer is gone.
  • Both tau and ffwf-tau land 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

Choose a tag to compare

@jmccardle jmccardle released this 09 Aug 14:45
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

Choose a tag to compare

@jmccardle jmccardle released this 09 Aug 14:31
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>