Skip to main content

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 --tui drops you straight into a chat, no setup screen, as long as .env has 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 --resume at 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-provideranthropic, openai, gemini, openrouter behind 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 memoryHARNESS.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:

  1. Session transcripts (agent/session.py) — every message, append-only, one .jsonl file 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.
  2. 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.
  3. 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

  1. Create tools/my_tool.py exporting TOOL_DEF (name / description / JSON-schema parameters) and a *_handler function.
  2. Import it and add the (TOOL_DEF, handler) pair to the tool_map list in cli.py:setup_tools().

Add a provider

  1. Subclass BaseProvider in providers/my_provider.py and implement generate() and generate_stream(), converting to/from the shared Message / LLMResponse / StreamChunk types.
  2. Wire it into _get_provider() in agent/loop.py and add it to the CLI's --provider choices and the TUI's PROVIDER_MODELS dict.

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 bash and 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 parallelspawn_sub_agent blocks 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

harness_project-0.1.0.tar.gz (42.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

harness_project-0.1.0-py3-none-any.whl (48.7 kB view details)

Uploaded Python 3

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

Hashes for harness_project-0.1.0.tar.gz
Algorithm Hash digest
SHA256 14162ffe827d893ae6399d4519d7455d936230f5393f9d40e261ccfe3ab5a4c8
MD5 bb39c7f117e93aa9d9e583d57200e5e9
BLAKE2b-256 1b8522bedb1970c9939173b25b0303de37d2b849170b870ac448a35d50e9e24b

See more details on using hashes here.

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

Hashes for harness_project-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f01addec333889090674ad0ea4bc4a6e80dc79aabe7f6b095f604009cb2c34c8
MD5 88225ca5ef97b9cd3f4613434c8eab60
BLAKE2b-256 8bea4a8335eabad85203dec864fdb55147bc85685912cca1274d8d3d75545a60

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page