Coding Agent Harness
A terminal coding agent with a real TUI — live streaming responses, rendered
markdown, a /-command palette, and persistent sessions you can walk away
from and resume later. Point it at Anthropic, OpenAI, Gemini, or anything
routed through OpenRouter, switch between them mid-conversation, and it never
asks you to type the same API key twice.
Think of it as a minimal, hackable "Claude Code style" agent that lives in a proper terminal UI instead of a bare prompt.
Why the TUI
This is the way the harness is meant to be run. Launch it, and:
- Your provider, model, and API key are remembered —
harness --tuidrops you straight into a chat, no setup screen, as long as.envhas what it needs. Missing a key for the provider you want? A popup asks for it right there, no restart. - Responses stream live, token by token, and render as actual markdown —
headers, bold, code blocks, lists — not raw
**/```syntax. - Every command lives behind
/— type it and get a live-filtered dropdown, with second-level menus for anything that has a fixed set of choices (/provider, for instance). No memorizing flags. - Every conversation is a real, resumable session. Close the TUI, come back
tomorrow,
/resume(or--resumeat launch) and your entire history replays back into the chat. - Long sessions don't just die once they outgrow the model's context window — compaction kicks in automatically, quietly, in the background.
harness --tui
That's the whole onboarding.
Features
- A real TUI, not a form — Textual-powered, with a persistent status bar showing provider/model/status, a live streaming preview, and a scrollable, markdown-rendered chat log.
/command palette —/provider,/model,/apikey,/resume,/sessions,/remember,/tools,/clear,/help,/exit— all discoverable by typing/and reading the dropdown, no docs required.- Multi-provider —
anthropic,openai,gemini,openrouterbehind one interface. Switch any of them, live, without leaving the chat. - Live streaming — real token-by-token SSE streaming, all four providers.
- Markdown rendering — what the model writes is what you see rendered.
- Real session persistence — append-only transcripts on disk, resumable by id, never rewritten, so a crash never loses more than one in-flight message.
- Project memory —
HARNESS.md, a plain markdown file always loaded into context, editable by hand or via/remember. No hidden retrieval — you can always see exactly what the agent knows. - Context compaction — old tool output gets elided first, and if that's not enough, older turns get summarized, automatically, before a session ever hits a hard context-window failure.
- 11 built-in tools — file read/write/edit, bash, grep, glob, web search, clarifying questions, todo tracking, skill loading, sub-agent spawning.
- A plain-text mode too, for scripts and one-shot piping — see Scripting / one-shot mode below if that's what you actually need.
Installation
Requires Python ≥ 3.12. Uses uv.
Global install (recommended — gives you the harness command anywhere)
uv tool install --editable .
harness --tui
--editable runs against this actual source tree — changes to the code take
effect immediately, no reinstalling. It also means harness --tui works from
any directory, and treats wherever you're standing as the project root.
Uninstall any time with uv tool uninstall harness-project.
Local (run from inside this repo only)
uv sync
uv run harness --tui
Configuration
Create a .env file in the project root:
# pick a default provider
PROVIDER=anthropic
# provider API keys — only the one(s) you use are required
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=...
OPENROUTER_API_KEY=sk-or-...
# web search (optional, for the web_search tool)
TAVILY_API_KEY=tvly-...
# optional model overrides
MODEL= # force a specific model for any provider
ANTHROPIC_MODEL=claude-sonnet-4-20250514
OPENAI_MODEL=gpt-4o
GEMINI_MODEL=gemini-2.5-flash
OPENROUTER_MODEL=deepseek/deepseek-v4-flash-0731
.env is .gitignore'd — your keys never get committed, and once they're
set, harness --tui never asks for them again. Don't have a key for the
provider you want yet? Launch anyway — /provider pops up a prompt for it on
the spot.
Using the TUI
harness --tui # fresh session
harness --tui --resume <session-id> # pick up where you left off
Type / in the input box at any point for the command dropdown:
| Command | Does |
|---|---|
/provider <name> |
Switch provider. Pops up an API-key prompt on the spot if .env doesn't have one for it. |
/model <name> |
Switch the model for the current provider. |
/apikey <key> |
Set the API key for the current provider — this session only, never saved to .env. |
/resume <id> |
Load and replay a previous session's transcript. |
/sessions |
List every saved session. |
/remember <note> |
Append a note to HARNESS.md. |
/tools |
List every registered tool. |
/clear |
Clear the visible chat log. |
/help |
List all commands. |
/exit |
Quit. |
Quitting: Ctrl+Q quits instantly. Ctrl+C/Cmd+C copies selected text
if you've selected something in the chat log; otherwise the first press warns
you, the second (within 2s) quits.
Copying text: click-and-drag inside the chat log to select, then Ctrl+C
to copy. Not working in your terminal? Hold Option (⌥) on macOS while
dragging to force native terminal selection instead.
Scripting / one-shot mode
For CI, scripts, or anything that isn't an interactive chat, there's a plain CLI underneath the TUI — same agent loop, same tools, no interface:
harness "add a --version flag to cli.py" # one-shot, non-interactive
harness # plain interactive REPL, no TUI
harness -p openrouter -m "anthropic/claude-sonnet-4" "explain the memory system"
harness --resume <session-id> # resume without the TUI
harness --list-tools
harness --list-sessions
| Flag | Description |
|---|---|
message (positional) |
Initial task. If omitted, starts the plain REPL. |
-p, --provider |
anthropic | openai | gemini | openrouter |
-m, --model |
Model name (defaults to the provider's default) |
-k, --api-key |
API key override (else read from .env) |
-r, --resume <id> |
Resume a previous session by id |
--tui |
Start the TUI instead |
--list-tools |
Print the tool registry and exit |
--list-sessions |
Print every saved session and exit |
The plain REPL also understands /resume and /remember, plus exit,
quit, q to stop — just without the dropdown, since there's no widget to
draw it in.
Session & memory model
Three independent, deliberately simple mechanisms — no vector DB, no hidden retrieval, nothing the model decides to remember on your behalf:
- Session transcripts (
agent/session.py) — every message, append-only, one.jsonlfile per session under.harness/sessions/. Resuming replays the exact original history; nothing is ever rewritten, so a crash mid-turn only ever loses the one in-flight message. - Project memory (
HARNESS.md) — a plain markdown file at the project root, loaded in full into the system prompt every turn. You write to it directly, or via/remember— deterministic, no LLM involved in the write. - Context compaction (
agent/compaction.py) — triggers once a session crosses ~75% of the active model's context window. Elides old, bulky tool results first (free); if that's still not enough, summarizes what's left with one extra LLM call. The full transcript on disk is untouched either way — a resumed session just re-compacts on its next turn if it's still long.
Architecture
flowchart TD
TUI["tui.py — the main entry point"]
CLI["cli.py — scripting / one-shot"]
subgraph Core["agent/"]
LOOP["loop.py — agent_loop / run_agent_once"]
SESS["session.py — transcripts"]
COMPACT["compaction.py"]
SP["system_prompt.py"]
end
CONFIG["config.py"]
MODELS["models.py"]
HMD["HARNESS.md"]
subgraph Providers["providers/"]
P["anthropic · openai · gemini · openrouter"]
end
subgraph Tools["tools/"]
REG["registry.py"]
end
TUI --> LOOP
CLI --> LOOP
LOOP --> SP
LOOP --> CONFIG
LOOP -- generate_stream --> Providers
LOOP -- execute --> REG
LOOP --> SESS
LOOP --> COMPACT
SP -. reads .-> HMD
Providers --> MODELS
REG --> MODELS
The loop, in one sentence: the TUI (or the plain CLI, for scripting) hands
a message to agent_loop, which builds a system prompt (tool docs +
HARNESS.md if present), streams a response from the selected provider, and
— while the provider keeps returning tool_call — executes tools via the
registry and feeds results back, until it returns text.
Project layout
main.py # entrypoint → cli.run_cli()
tui.py # the TUI — screens, slash commands, streaming preview
cli.py # argparse, tool registration, dispatch to loop / TUI
config.py # .env loading, paths, per-provider model/key resolution
models.py # Message, ToolDefinition, LLMResponse, StreamChunk
agent/
loop.py # agent_loop (interactive) + run_agent_once (sub-agents)
session.py # session transcript read/write
compaction.py # context-window compaction
system_prompt.py # DEFAULT_SYSTEM_PROMPT template
providers/
base.py # BaseProvider ABC — generate() + generate_stream()
anthropic.py openai.py gemini.py openrouter.py
tools/
registry.py # ToolRegistry (register / execute)
read_tool.py write_tool.py edit_tool.py
bash_tool.py grep_tool.py glob_tool.py
web_search.py questions_tool.py todos_tool.py
skill_reader.py sub_agent_tool.py
.skills/ # skill instruction files (discovered via read_skill)
.harness/ # runtime state: sessions, todos
HARNESS.md # project memory (created on first /remember)
Extending the harness
Add a tool
- Create
tools/my_tool.pyexportingTOOL_DEF(name / description / JSON-schemaparameters) and a*_handlerfunction. - Import it and add the
(TOOL_DEF, handler)pair to thetool_maplist incli.py:setup_tools().
Add a provider
- Subclass
BaseProviderinproviders/my_provider.pyand implementgenerate()andgenerate_stream(), converting to/from the sharedMessage/LLMResponse/StreamChunktypes. - Wire it into
_get_provider()inagent/loop.pyand add it to the CLI's--providerchoices and the TUI'sPROVIDER_MODELSdict.
Add a skill — drop a text/markdown file into .skills/; the agent finds
it via the read_skill tool.
Known limitations
Being upfront about what this doesn't do yet:
- No approval gates — tool calls (including
bashand file writes) run immediately, no confirmation step. No path sandboxing either. - One tool call per model turn — if a model requests several tool calls at once, only the first is used; the rest are silently dropped.
- Sub-agents aren't truly parallel —
spawn_sub_agentblocks synchronously while running, despite the "up to 3 in parallel" framing in the system prompt. - No retry/backoff on transient API errors (rate limits, 5xx).
None of these are hidden — they're just not built yet.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file harness_project-0.1.0.tar.gz.
File metadata
- Download URL: harness_project-0.1.0.tar.gz
- Upload date:
- Size: 42.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14162ffe827d893ae6399d4519d7455d936230f5393f9d40e261ccfe3ab5a4c8
|
|
| MD5 |
bb39c7f117e93aa9d9e583d57200e5e9
|
|
| BLAKE2b-256 |
1b8522bedb1970c9939173b25b0303de37d2b849170b870ac448a35d50e9e24b
|
File details
Details for the file harness_project-0.1.0-py3-none-any.whl.
File metadata
- Download URL: harness_project-0.1.0-py3-none-any.whl
- Upload date:
- Size: 48.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f01addec333889090674ad0ea4bc4a6e80dc79aabe7f6b095f604009cb2c34c8
|
|
| MD5 |
88225ca5ef97b9cd3f4613434c8eab60
|
|
| BLAKE2b-256 |
8bea4a8335eabad85203dec864fdb55147bc85685912cca1274d8d3d75545a60
|