A lightweight, terminal-native AI coding agent with built-in tools.
It dawned on me recently that it would be advantageous to have an intimiate understanding of how a modern "Agent" works. As the singularity approaches, I might find some respite in the fact that I have read at least most of the implementation that my personal singularity runs on.
I spend the occasional weekend on this project and, as I improve it, find myself using it for actual programming work more and more often. I attempt to reimplement from first principles any feature i find particularly interesting that Codex or Claude Code use. Most are fairly trivial to figure out, others more complex (like sub-agents). My long term vision is that this architecture supports sub-agents speaking with one another as they work. Currently, sub-agents vaguely work, to be clear.
The rest of this is fairly accurate AI slop. Feel free to read to understand your way around the repository.
Harness is a TUI-based AI assistant that lives in your terminal. Ask it to write code, debug issues, explore a codebase, or run commands — it has direct access to your filesystem and shell through a suite of built-in tools. A Rust backend handles agent logic and persistence while a React/Ink frontend provides a responsive terminal interface.
- Agentic tool-use loop — The AI calls tools iteratively to gather context and take action, then responds with a final answer.
- Built-in tools —
bash,read,write,edit,glob,grep,addition, andstart_subagent— everything an AI needs to navigate, edit, and automate within a codebase. - Subagents — Spawn parallel child agents to work on focused subtasks concurrently. Each subagent runs independently and reports back when complete.
- Conversation compaction — When conversations grow too long, a summarization step reclaims context budget by replacing old messages with a dense summary.
- Context budget tracking — Per-model token limits with automatic estimation to prevent context overflow. Supports GPT-4, Claude, Gemini, DeepSeek, Llama, and more.
- Conversation history — Conversations are automatically saved and can be browsed or resumed from the history screen.
- Configurable model — Use any model available on OpenRouter (default:
anthropic/claude-sonnet-4). - Custom instructions — Set persistent system-prompt instructions to tailor the agent's behavior.
- HUD (Heads Up Display) — Live git branch, diff stats, context token usage, and current model shown in the UI.
- Single command launch — One script builds the server (if needed) and starts the UI.
┌──────────────────┐ JSON-RPC 2.0 over stdio ┌──────────────────┐
│ Ink/React TUI │ ◀────────────────────────▸ │ Rust Server │
│ (TypeScript) │ │ (Tokio) │
└──────────────────┘ └──────────────────┘
├─ agent (LLM + tool loop)
│ ├─ tools (bash, read, write, edit, glob, grep, ...)
│ ├─ subagents (parallel child agents)
│ └─ compaction (conversation summarization)
├─ storage (settings + conversations)
└─ server (JSON-RPC dispatch)
The frontend spawns the Rust binary as a child process. All communication is JSON-RPC 2.0 — one JSON object per line, matched by id.
- Rust 1.87+
- Bun 1.0+
- An OpenRouter API key
# Clone the repo
git clone <repo-url> && cd harness
# Install frontend dependencies
cd packages/frontend && bun install && cd ../..
# Add your OpenRouter API key
echo 'OPENROUTER_API_KEY=sk-or-...' > .env
# Launch
./bin/harnessThe launch script automatically compiles the Rust server on first run (and whenever source files change).
ln -s "$(pwd)/bin/harness" ~/.local/bin/harness| Key | Action |
|---|---|
Enter |
Send message |
Shift+Enter |
New line |
Escape |
Back to home |
/help |
See available commands |
| Tool | Description |
|---|---|
| Bash | Execute shell commands and return stdout/stderr |
| Read | Read file contents with optional line offset & limit |
| Write | Create or overwrite files (creates parent directories) |
| Edit | Edit files by replacing exact strings (preferred for modifications) |
| Glob | Find files matching a glob pattern |
| Grep | Search file contents with regex, with optional path and file-type filters |
| Addition | Add two numbers together (utility tool) |
| Start Subagent | Spawn a parallel child agent to work on a focused subtask |
Settings are persisted at ~/.config/harness/settings.json and can be edited from the Settings screen inside the app.
| Setting | Default | Description |
|---|---|---|
model |
anthropic/claude-sonnet-4 |
Any model ID available on OpenRouter |
custom_instructions |
"" |
Extra instructions prepended to the system prompt |
Conversations are stored as JSON files under ~/.config/harness/conversations/.
harness/
├── bin/harness # Launch script (build + run)
├── crates/
│ ├── agent/ # LLM client, tool executor, agent loop
│ │ └── src/
│ │ ├── tools/ # bash, read, write, edit, glob, grep, addition, start_subagent
│ │ ├── subagents/ # Parallel child agent spawning & registry
│ │ ├── conversation/ # Compaction & summarization
│ │ └── context/ # Token budget tracking per model
│ ├── storage/ # Settings & conversation persistence
│ └── server/ # JSON-RPC stdio server binary
│ └── src/
│ ├── handlers/ # RPC method handlers (conversation, settings, HUD)
│ └── rpc/ # JSON-RPC dispatch & method registry
└── packages/
└── frontend/ # Ink (React) terminal UI
└── src/
├── rpc/ # Typed JSON-RPC client
└── screens/ # Home, Chat, Settings, History screens
Harness supports spawning parallel child agents via the start_subagent tool. This enables the AI to delegate focused subtasks and continue working while subagents execute concurrently.
-
Spawning — When the LLM calls
start_subagent, the tool spawns a tokio task that runs a fresh agent loop against its ownConversation(persisted under<parent_conv>/subagent/<subagent_id>/). -
Communication — Each agent owns a
SubagentInbox(anmpsc::UnboundedReceiverofSubagentResults). TheStartSubagentToolholds the matching sender. When a subagent finishes, it sends its result back via the channel and emits aSubagentCompletedevent. -
Parent loop — The parent agent drains the inbox at the top of every loop iteration, injecting results as user messages. When the LLM produces no tool calls, the parent waits on
inbox.recv().awaitif subagents are still pending — keeping the agent loop alive until every in-flight subagent reports back. -
Recursion — Subagents can spawn their own children. Each level creates its own inbox +
StartSubagentTool, so grandchildren's results funnel back to their direct parent, not the top-level caller. -
Registry — A process-wide
SubagentRegistrytracks running subagents, recording the spawn tree for observability.
MIT