harness-project 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harness_project-0.1.0/PKG-INFO +332 -0
- harness_project-0.1.0/README.md +320 -0
- harness_project-0.1.0/agent/__init__.py +0 -0
- harness_project-0.1.0/agent/compaction.py +195 -0
- harness_project-0.1.0/agent/loop.py +413 -0
- harness_project-0.1.0/agent/session.py +137 -0
- harness_project-0.1.0/agent/system_prompt.py +73 -0
- harness_project-0.1.0/cli.py +181 -0
- harness_project-0.1.0/config.py +56 -0
- harness_project-0.1.0/harness_project.egg-info/PKG-INFO +332 -0
- harness_project-0.1.0/harness_project.egg-info/SOURCES.txt +37 -0
- harness_project-0.1.0/harness_project.egg-info/dependency_links.txt +1 -0
- harness_project-0.1.0/harness_project.egg-info/entry_points.txt +2 -0
- harness_project-0.1.0/harness_project.egg-info/requires.txt +5 -0
- harness_project-0.1.0/harness_project.egg-info/top_level.txt +8 -0
- harness_project-0.1.0/main.py +5 -0
- harness_project-0.1.0/models.py +71 -0
- harness_project-0.1.0/providers/__init__.py +0 -0
- harness_project-0.1.0/providers/anthropic.py +187 -0
- harness_project-0.1.0/providers/base.py +31 -0
- harness_project-0.1.0/providers/gemini.py +191 -0
- harness_project-0.1.0/providers/openai.py +173 -0
- harness_project-0.1.0/providers/openrouter.py +175 -0
- harness_project-0.1.0/pyproject.toml +27 -0
- harness_project-0.1.0/setup.cfg +4 -0
- harness_project-0.1.0/tools/__init__.py +0 -0
- harness_project-0.1.0/tools/bash_tool.py +48 -0
- harness_project-0.1.0/tools/edit_tool.py +51 -0
- harness_project-0.1.0/tools/glob_tool.py +58 -0
- harness_project-0.1.0/tools/grep_tool.py +80 -0
- harness_project-0.1.0/tools/questions_tool.py +73 -0
- harness_project-0.1.0/tools/read_tool.py +30 -0
- harness_project-0.1.0/tools/registry.py +40 -0
- harness_project-0.1.0/tools/skill_reader.py +71 -0
- harness_project-0.1.0/tools/sub_agent_tool.py +88 -0
- harness_project-0.1.0/tools/todos_tool.py +113 -0
- harness_project-0.1.0/tools/web_search.py +55 -0
- harness_project-0.1.0/tools/write_tool.py +31 -0
- harness_project-0.1.0/tui.py +720 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harness-project
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-provider coding agent harness with memory, tools, and TUI
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
8
|
+
Requires-Dist: tavily-python>=0.5.0
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: httpx>=0.28.0
|
|
11
|
+
Requires-Dist: textual>=3.0.0
|
|
12
|
+
|
|
13
|
+
# Coding Agent Harness
|
|
14
|
+
|
|
15
|
+
A terminal coding agent with a real TUI — live streaming responses, rendered
|
|
16
|
+
markdown, a `/`-command palette, and persistent sessions you can walk away
|
|
17
|
+
from and resume later. Point it at Anthropic, OpenAI, Gemini, or anything
|
|
18
|
+
routed through OpenRouter, switch between them mid-conversation, and it never
|
|
19
|
+
asks you to type the same API key twice.
|
|
20
|
+
|
|
21
|
+
Think of it as a minimal, hackable "Claude Code style" agent that lives in a
|
|
22
|
+
proper terminal UI instead of a bare prompt.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why the TUI
|
|
27
|
+
|
|
28
|
+
This is the way the harness is meant to be run. Launch it, and:
|
|
29
|
+
|
|
30
|
+
- Your provider, model, and API key are remembered — `harness --tui` drops
|
|
31
|
+
you straight into a chat, no setup screen, as long as `.env` has what it
|
|
32
|
+
needs. Missing a key for the provider you want? A popup asks for it right
|
|
33
|
+
there, no restart.
|
|
34
|
+
- Responses **stream live**, token by token, and render as actual markdown —
|
|
35
|
+
headers, bold, code blocks, lists — not raw `**`/`` ``` `` syntax.
|
|
36
|
+
- Every command lives behind `/` — type it and get a live-filtered dropdown,
|
|
37
|
+
with second-level menus for anything that has a fixed set of choices
|
|
38
|
+
(`/provider`, for instance). No memorizing flags.
|
|
39
|
+
- Every conversation is a real, resumable session. Close the TUI, come back
|
|
40
|
+
tomorrow, `/resume` (or `--resume` at launch) and your entire history
|
|
41
|
+
replays back into the chat.
|
|
42
|
+
- Long sessions don't just die once they outgrow the model's context window —
|
|
43
|
+
compaction kicks in automatically, quietly, in the background.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
harness --tui
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's the whole onboarding.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- **A real TUI, not a form** — Textual-powered, with a persistent status bar
|
|
56
|
+
showing provider/model/status, a live streaming preview, and a scrollable,
|
|
57
|
+
markdown-rendered chat log.
|
|
58
|
+
- **`/` command palette** — `/provider`, `/model`, `/apikey`, `/resume`,
|
|
59
|
+
`/sessions`, `/remember`, `/tools`, `/clear`, `/help`, `/exit` — all
|
|
60
|
+
discoverable by typing `/` and reading the dropdown, no docs required.
|
|
61
|
+
- **Multi-provider** — `anthropic`, `openai`, `gemini`, `openrouter` behind
|
|
62
|
+
one interface. Switch any of them, live, without leaving the chat.
|
|
63
|
+
- **Live streaming** — real token-by-token SSE streaming, all four providers.
|
|
64
|
+
- **Markdown rendering** — what the model writes is what you see rendered.
|
|
65
|
+
- **Real session persistence** — append-only transcripts on disk, resumable
|
|
66
|
+
by id, never rewritten, so a crash never loses more than one in-flight
|
|
67
|
+
message.
|
|
68
|
+
- **Project memory** — `HARNESS.md`, a plain markdown file always loaded into
|
|
69
|
+
context, editable by hand or via `/remember`. No hidden retrieval — you can
|
|
70
|
+
always see exactly what the agent knows.
|
|
71
|
+
- **Context compaction** — old tool output gets elided first, and if that's
|
|
72
|
+
not enough, older turns get summarized, automatically, before a session
|
|
73
|
+
ever hits a hard context-window failure.
|
|
74
|
+
- **11 built-in tools** — file read/write/edit, bash, grep, glob, web search,
|
|
75
|
+
clarifying questions, todo tracking, skill loading, sub-agent spawning.
|
|
76
|
+
- **A plain-text mode too**, for scripts and one-shot piping — see
|
|
77
|
+
[Scripting / one-shot mode](#scripting--one-shot-mode) below if that's what
|
|
78
|
+
you actually need.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
Requires **Python ≥ 3.12**. Uses [`uv`](https://github.com/astral-sh/uv).
|
|
85
|
+
|
|
86
|
+
### Global install (recommended — gives you the `harness` command anywhere)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv tool install --editable .
|
|
90
|
+
harness --tui
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`--editable` runs against this actual source tree — changes to the code take
|
|
94
|
+
effect immediately, no reinstalling. It also means `harness --tui` works from
|
|
95
|
+
*any* directory, and treats wherever you're standing as the project root.
|
|
96
|
+
Uninstall any time with `uv tool uninstall harness-project`.
|
|
97
|
+
|
|
98
|
+
### Local (run from inside this repo only)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv sync
|
|
102
|
+
uv run harness --tui
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Configuration
|
|
106
|
+
|
|
107
|
+
Create a `.env` file in the project root:
|
|
108
|
+
|
|
109
|
+
```env
|
|
110
|
+
# pick a default provider
|
|
111
|
+
PROVIDER=anthropic
|
|
112
|
+
|
|
113
|
+
# provider API keys — only the one(s) you use are required
|
|
114
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
115
|
+
OPENAI_API_KEY=sk-...
|
|
116
|
+
GEMINI_API_KEY=...
|
|
117
|
+
OPENROUTER_API_KEY=sk-or-...
|
|
118
|
+
|
|
119
|
+
# web search (optional, for the web_search tool)
|
|
120
|
+
TAVILY_API_KEY=tvly-...
|
|
121
|
+
|
|
122
|
+
# optional model overrides
|
|
123
|
+
MODEL= # force a specific model for any provider
|
|
124
|
+
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
|
125
|
+
OPENAI_MODEL=gpt-4o
|
|
126
|
+
GEMINI_MODEL=gemini-2.5-flash
|
|
127
|
+
OPENROUTER_MODEL=deepseek/deepseek-v4-flash-0731
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`.env` is `.gitignore`'d — your keys never get committed, and once they're
|
|
131
|
+
set, `harness --tui` never asks for them again. Don't have a key for the
|
|
132
|
+
provider you want yet? Launch anyway — `/provider` pops up a prompt for it on
|
|
133
|
+
the spot.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Using the TUI
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
harness --tui # fresh session
|
|
141
|
+
harness --tui --resume <session-id> # pick up where you left off
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Type `/` in the input box at any point for the command dropdown:
|
|
145
|
+
|
|
146
|
+
| Command | Does |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `/provider <name>` | Switch provider. Pops up an API-key prompt on the spot if `.env` doesn't have one for it. |
|
|
149
|
+
| `/model <name>` | Switch the model for the current provider. |
|
|
150
|
+
| `/apikey <key>` | Set the API key for the current provider — this session only, never saved to `.env`. |
|
|
151
|
+
| `/resume <id>` | Load and replay a previous session's transcript. |
|
|
152
|
+
| `/sessions` | List every saved session. |
|
|
153
|
+
| `/remember <note>` | Append a note to `HARNESS.md`. |
|
|
154
|
+
| `/tools` | List every registered tool. |
|
|
155
|
+
| `/clear` | Clear the visible chat log. |
|
|
156
|
+
| `/help` | List all commands. |
|
|
157
|
+
| `/exit` | Quit. |
|
|
158
|
+
|
|
159
|
+
**Quitting**: `Ctrl+Q` quits instantly. `Ctrl+C`/`Cmd+C` copies selected text
|
|
160
|
+
if you've selected something in the chat log; otherwise the first press warns
|
|
161
|
+
you, the second (within 2s) quits.
|
|
162
|
+
|
|
163
|
+
**Copying text**: click-and-drag inside the chat log to select, then `Ctrl+C`
|
|
164
|
+
to copy. Not working in your terminal? Hold `Option (⌥)` on macOS while
|
|
165
|
+
dragging to force native terminal selection instead.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Scripting / one-shot mode
|
|
170
|
+
|
|
171
|
+
For CI, scripts, or anything that isn't an interactive chat, there's a plain
|
|
172
|
+
CLI underneath the TUI — same agent loop, same tools, no interface:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
harness "add a --version flag to cli.py" # one-shot, non-interactive
|
|
176
|
+
harness # plain interactive REPL, no TUI
|
|
177
|
+
harness -p openrouter -m "anthropic/claude-sonnet-4" "explain the memory system"
|
|
178
|
+
harness --resume <session-id> # resume without the TUI
|
|
179
|
+
harness --list-tools
|
|
180
|
+
harness --list-sessions
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Flag | Description |
|
|
184
|
+
|------|-------------|
|
|
185
|
+
| `message` (positional) | Initial task. If omitted, starts the plain REPL. |
|
|
186
|
+
| `-p, --provider` | `anthropic` \| `openai` \| `gemini` \| `openrouter` |
|
|
187
|
+
| `-m, --model` | Model name (defaults to the provider's default) |
|
|
188
|
+
| `-k, --api-key` | API key override (else read from `.env`) |
|
|
189
|
+
| `-r, --resume <id>` | Resume a previous session by id |
|
|
190
|
+
| `--tui` | Start the TUI instead |
|
|
191
|
+
| `--list-tools` | Print the tool registry and exit |
|
|
192
|
+
| `--list-sessions` | Print every saved session and exit |
|
|
193
|
+
|
|
194
|
+
The plain REPL also understands `/resume` and `/remember`, plus `exit`,
|
|
195
|
+
`quit`, `q` to stop — just without the dropdown, since there's no widget to
|
|
196
|
+
draw it in.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Session & memory model
|
|
201
|
+
|
|
202
|
+
Three independent, deliberately simple mechanisms — no vector DB, no hidden
|
|
203
|
+
retrieval, nothing the model decides to remember on your behalf:
|
|
204
|
+
|
|
205
|
+
1. **Session transcripts** (`agent/session.py`) — every message, append-only,
|
|
206
|
+
one `.jsonl` file per session under `.harness/sessions/`. Resuming replays
|
|
207
|
+
the exact original history; nothing is ever rewritten, so a crash mid-turn
|
|
208
|
+
only ever loses the one in-flight message.
|
|
209
|
+
2. **Project memory** (`HARNESS.md`) — a plain markdown file at the project
|
|
210
|
+
root, loaded in full into the system prompt every turn. You write to it
|
|
211
|
+
directly, or via `/remember` — deterministic, no LLM involved in the write.
|
|
212
|
+
3. **Context compaction** (`agent/compaction.py`) — triggers once a session
|
|
213
|
+
crosses ~75% of the active model's context window. Elides old, bulky tool
|
|
214
|
+
results first (free); if that's still not enough, summarizes what's left
|
|
215
|
+
with one extra LLM call. The full transcript on disk is untouched either
|
|
216
|
+
way — a resumed session just re-compacts on its next turn if it's still long.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Architecture
|
|
221
|
+
|
|
222
|
+
```mermaid
|
|
223
|
+
flowchart TD
|
|
224
|
+
TUI["tui.py — the main entry point"]
|
|
225
|
+
CLI["cli.py — scripting / one-shot"]
|
|
226
|
+
|
|
227
|
+
subgraph Core["agent/"]
|
|
228
|
+
LOOP["loop.py — agent_loop / run_agent_once"]
|
|
229
|
+
SESS["session.py — transcripts"]
|
|
230
|
+
COMPACT["compaction.py"]
|
|
231
|
+
SP["system_prompt.py"]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
CONFIG["config.py"]
|
|
235
|
+
MODELS["models.py"]
|
|
236
|
+
HMD["HARNESS.md"]
|
|
237
|
+
|
|
238
|
+
subgraph Providers["providers/"]
|
|
239
|
+
P["anthropic · openai · gemini · openrouter"]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
subgraph Tools["tools/"]
|
|
243
|
+
REG["registry.py"]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
TUI --> LOOP
|
|
247
|
+
CLI --> LOOP
|
|
248
|
+
LOOP --> SP
|
|
249
|
+
LOOP --> CONFIG
|
|
250
|
+
LOOP -- generate_stream --> Providers
|
|
251
|
+
LOOP -- execute --> REG
|
|
252
|
+
LOOP --> SESS
|
|
253
|
+
LOOP --> COMPACT
|
|
254
|
+
SP -. reads .-> HMD
|
|
255
|
+
Providers --> MODELS
|
|
256
|
+
REG --> MODELS
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**The loop, in one sentence:** the TUI (or the plain CLI, for scripting) hands
|
|
260
|
+
a message to `agent_loop`, which builds a system prompt (tool docs +
|
|
261
|
+
`HARNESS.md` if present), streams a response from the selected provider, and
|
|
262
|
+
— while the provider keeps returning `tool_call` — executes tools via the
|
|
263
|
+
registry and feeds results back, until it returns `text`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Project layout
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
main.py # entrypoint → cli.run_cli()
|
|
271
|
+
tui.py # the TUI — screens, slash commands, streaming preview
|
|
272
|
+
cli.py # argparse, tool registration, dispatch to loop / TUI
|
|
273
|
+
config.py # .env loading, paths, per-provider model/key resolution
|
|
274
|
+
models.py # Message, ToolDefinition, LLMResponse, StreamChunk
|
|
275
|
+
|
|
276
|
+
agent/
|
|
277
|
+
loop.py # agent_loop (interactive) + run_agent_once (sub-agents)
|
|
278
|
+
session.py # session transcript read/write
|
|
279
|
+
compaction.py # context-window compaction
|
|
280
|
+
system_prompt.py # DEFAULT_SYSTEM_PROMPT template
|
|
281
|
+
|
|
282
|
+
providers/
|
|
283
|
+
base.py # BaseProvider ABC — generate() + generate_stream()
|
|
284
|
+
anthropic.py openai.py gemini.py openrouter.py
|
|
285
|
+
|
|
286
|
+
tools/
|
|
287
|
+
registry.py # ToolRegistry (register / execute)
|
|
288
|
+
read_tool.py write_tool.py edit_tool.py
|
|
289
|
+
bash_tool.py grep_tool.py glob_tool.py
|
|
290
|
+
web_search.py questions_tool.py todos_tool.py
|
|
291
|
+
skill_reader.py sub_agent_tool.py
|
|
292
|
+
|
|
293
|
+
.skills/ # skill instruction files (discovered via read_skill)
|
|
294
|
+
.harness/ # runtime state: sessions, todos
|
|
295
|
+
HARNESS.md # project memory (created on first /remember)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Extending the harness
|
|
301
|
+
|
|
302
|
+
**Add a tool**
|
|
303
|
+
1. Create `tools/my_tool.py` exporting `TOOL_DEF` (name / description /
|
|
304
|
+
JSON-schema `parameters`) and a `*_handler` function.
|
|
305
|
+
2. Import it and add the `(TOOL_DEF, handler)` pair to the `tool_map` list in
|
|
306
|
+
`cli.py:setup_tools()`.
|
|
307
|
+
|
|
308
|
+
**Add a provider**
|
|
309
|
+
1. Subclass `BaseProvider` in `providers/my_provider.py` and implement
|
|
310
|
+
`generate()` and `generate_stream()`, converting to/from the shared
|
|
311
|
+
`Message` / `LLMResponse` / `StreamChunk` types.
|
|
312
|
+
2. Wire it into `_get_provider()` in `agent/loop.py` and add it to the CLI's
|
|
313
|
+
`--provider` choices and the TUI's `PROVIDER_MODELS` dict.
|
|
314
|
+
|
|
315
|
+
**Add a skill** — drop a text/markdown file into `.skills/`; the agent finds
|
|
316
|
+
it via the `read_skill` tool.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Known limitations
|
|
321
|
+
|
|
322
|
+
Being upfront about what this doesn't do yet:
|
|
323
|
+
|
|
324
|
+
- **No approval gates** — tool calls (including `bash` and file writes) run
|
|
325
|
+
immediately, no confirmation step. No path sandboxing either.
|
|
326
|
+
- **One tool call per model turn** — if a model requests several tool calls
|
|
327
|
+
at once, only the first is used; the rest are silently dropped.
|
|
328
|
+
- **Sub-agents aren't truly parallel** — `spawn_sub_agent` blocks synchronously
|
|
329
|
+
while running, despite the "up to 3 in parallel" framing in the system prompt.
|
|
330
|
+
- **No retry/backoff** on transient API errors (rate limits, 5xx).
|
|
331
|
+
|
|
332
|
+
None of these are hidden — they're just not built yet.
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Coding Agent Harness
|
|
2
|
+
|
|
3
|
+
A terminal coding agent with a real TUI — live streaming responses, rendered
|
|
4
|
+
markdown, a `/`-command palette, and persistent sessions you can walk away
|
|
5
|
+
from and resume later. Point it at Anthropic, OpenAI, Gemini, or anything
|
|
6
|
+
routed through OpenRouter, switch between them mid-conversation, and it never
|
|
7
|
+
asks you to type the same API key twice.
|
|
8
|
+
|
|
9
|
+
Think of it as a minimal, hackable "Claude Code style" agent that lives in a
|
|
10
|
+
proper terminal UI instead of a bare prompt.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why the TUI
|
|
15
|
+
|
|
16
|
+
This is the way the harness is meant to be run. Launch it, and:
|
|
17
|
+
|
|
18
|
+
- Your provider, model, and API key are remembered — `harness --tui` drops
|
|
19
|
+
you straight into a chat, no setup screen, as long as `.env` has what it
|
|
20
|
+
needs. Missing a key for the provider you want? A popup asks for it right
|
|
21
|
+
there, no restart.
|
|
22
|
+
- Responses **stream live**, token by token, and render as actual markdown —
|
|
23
|
+
headers, bold, code blocks, lists — not raw `**`/`` ``` `` syntax.
|
|
24
|
+
- Every command lives behind `/` — type it and get a live-filtered dropdown,
|
|
25
|
+
with second-level menus for anything that has a fixed set of choices
|
|
26
|
+
(`/provider`, for instance). No memorizing flags.
|
|
27
|
+
- Every conversation is a real, resumable session. Close the TUI, come back
|
|
28
|
+
tomorrow, `/resume` (or `--resume` at launch) and your entire history
|
|
29
|
+
replays back into the chat.
|
|
30
|
+
- Long sessions don't just die once they outgrow the model's context window —
|
|
31
|
+
compaction kicks in automatically, quietly, in the background.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
harness --tui
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That's the whole onboarding.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **A real TUI, not a form** — Textual-powered, with a persistent status bar
|
|
44
|
+
showing provider/model/status, a live streaming preview, and a scrollable,
|
|
45
|
+
markdown-rendered chat log.
|
|
46
|
+
- **`/` command palette** — `/provider`, `/model`, `/apikey`, `/resume`,
|
|
47
|
+
`/sessions`, `/remember`, `/tools`, `/clear`, `/help`, `/exit` — all
|
|
48
|
+
discoverable by typing `/` and reading the dropdown, no docs required.
|
|
49
|
+
- **Multi-provider** — `anthropic`, `openai`, `gemini`, `openrouter` behind
|
|
50
|
+
one interface. Switch any of them, live, without leaving the chat.
|
|
51
|
+
- **Live streaming** — real token-by-token SSE streaming, all four providers.
|
|
52
|
+
- **Markdown rendering** — what the model writes is what you see rendered.
|
|
53
|
+
- **Real session persistence** — append-only transcripts on disk, resumable
|
|
54
|
+
by id, never rewritten, so a crash never loses more than one in-flight
|
|
55
|
+
message.
|
|
56
|
+
- **Project memory** — `HARNESS.md`, a plain markdown file always loaded into
|
|
57
|
+
context, editable by hand or via `/remember`. No hidden retrieval — you can
|
|
58
|
+
always see exactly what the agent knows.
|
|
59
|
+
- **Context compaction** — old tool output gets elided first, and if that's
|
|
60
|
+
not enough, older turns get summarized, automatically, before a session
|
|
61
|
+
ever hits a hard context-window failure.
|
|
62
|
+
- **11 built-in tools** — file read/write/edit, bash, grep, glob, web search,
|
|
63
|
+
clarifying questions, todo tracking, skill loading, sub-agent spawning.
|
|
64
|
+
- **A plain-text mode too**, for scripts and one-shot piping — see
|
|
65
|
+
[Scripting / one-shot mode](#scripting--one-shot-mode) below if that's what
|
|
66
|
+
you actually need.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Requires **Python ≥ 3.12**. Uses [`uv`](https://github.com/astral-sh/uv).
|
|
73
|
+
|
|
74
|
+
### Global install (recommended — gives you the `harness` command anywhere)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv tool install --editable .
|
|
78
|
+
harness --tui
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`--editable` runs against this actual source tree — changes to the code take
|
|
82
|
+
effect immediately, no reinstalling. It also means `harness --tui` works from
|
|
83
|
+
*any* directory, and treats wherever you're standing as the project root.
|
|
84
|
+
Uninstall any time with `uv tool uninstall harness-project`.
|
|
85
|
+
|
|
86
|
+
### Local (run from inside this repo only)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv sync
|
|
90
|
+
uv run harness --tui
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Configuration
|
|
94
|
+
|
|
95
|
+
Create a `.env` file in the project root:
|
|
96
|
+
|
|
97
|
+
```env
|
|
98
|
+
# pick a default provider
|
|
99
|
+
PROVIDER=anthropic
|
|
100
|
+
|
|
101
|
+
# provider API keys — only the one(s) you use are required
|
|
102
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
103
|
+
OPENAI_API_KEY=sk-...
|
|
104
|
+
GEMINI_API_KEY=...
|
|
105
|
+
OPENROUTER_API_KEY=sk-or-...
|
|
106
|
+
|
|
107
|
+
# web search (optional, for the web_search tool)
|
|
108
|
+
TAVILY_API_KEY=tvly-...
|
|
109
|
+
|
|
110
|
+
# optional model overrides
|
|
111
|
+
MODEL= # force a specific model for any provider
|
|
112
|
+
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
|
113
|
+
OPENAI_MODEL=gpt-4o
|
|
114
|
+
GEMINI_MODEL=gemini-2.5-flash
|
|
115
|
+
OPENROUTER_MODEL=deepseek/deepseek-v4-flash-0731
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`.env` is `.gitignore`'d — your keys never get committed, and once they're
|
|
119
|
+
set, `harness --tui` never asks for them again. Don't have a key for the
|
|
120
|
+
provider you want yet? Launch anyway — `/provider` pops up a prompt for it on
|
|
121
|
+
the spot.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Using the TUI
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
harness --tui # fresh session
|
|
129
|
+
harness --tui --resume <session-id> # pick up where you left off
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Type `/` in the input box at any point for the command dropdown:
|
|
133
|
+
|
|
134
|
+
| Command | Does |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `/provider <name>` | Switch provider. Pops up an API-key prompt on the spot if `.env` doesn't have one for it. |
|
|
137
|
+
| `/model <name>` | Switch the model for the current provider. |
|
|
138
|
+
| `/apikey <key>` | Set the API key for the current provider — this session only, never saved to `.env`. |
|
|
139
|
+
| `/resume <id>` | Load and replay a previous session's transcript. |
|
|
140
|
+
| `/sessions` | List every saved session. |
|
|
141
|
+
| `/remember <note>` | Append a note to `HARNESS.md`. |
|
|
142
|
+
| `/tools` | List every registered tool. |
|
|
143
|
+
| `/clear` | Clear the visible chat log. |
|
|
144
|
+
| `/help` | List all commands. |
|
|
145
|
+
| `/exit` | Quit. |
|
|
146
|
+
|
|
147
|
+
**Quitting**: `Ctrl+Q` quits instantly. `Ctrl+C`/`Cmd+C` copies selected text
|
|
148
|
+
if you've selected something in the chat log; otherwise the first press warns
|
|
149
|
+
you, the second (within 2s) quits.
|
|
150
|
+
|
|
151
|
+
**Copying text**: click-and-drag inside the chat log to select, then `Ctrl+C`
|
|
152
|
+
to copy. Not working in your terminal? Hold `Option (⌥)` on macOS while
|
|
153
|
+
dragging to force native terminal selection instead.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Scripting / one-shot mode
|
|
158
|
+
|
|
159
|
+
For CI, scripts, or anything that isn't an interactive chat, there's a plain
|
|
160
|
+
CLI underneath the TUI — same agent loop, same tools, no interface:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
harness "add a --version flag to cli.py" # one-shot, non-interactive
|
|
164
|
+
harness # plain interactive REPL, no TUI
|
|
165
|
+
harness -p openrouter -m "anthropic/claude-sonnet-4" "explain the memory system"
|
|
166
|
+
harness --resume <session-id> # resume without the TUI
|
|
167
|
+
harness --list-tools
|
|
168
|
+
harness --list-sessions
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Flag | Description |
|
|
172
|
+
|------|-------------|
|
|
173
|
+
| `message` (positional) | Initial task. If omitted, starts the plain REPL. |
|
|
174
|
+
| `-p, --provider` | `anthropic` \| `openai` \| `gemini` \| `openrouter` |
|
|
175
|
+
| `-m, --model` | Model name (defaults to the provider's default) |
|
|
176
|
+
| `-k, --api-key` | API key override (else read from `.env`) |
|
|
177
|
+
| `-r, --resume <id>` | Resume a previous session by id |
|
|
178
|
+
| `--tui` | Start the TUI instead |
|
|
179
|
+
| `--list-tools` | Print the tool registry and exit |
|
|
180
|
+
| `--list-sessions` | Print every saved session and exit |
|
|
181
|
+
|
|
182
|
+
The plain REPL also understands `/resume` and `/remember`, plus `exit`,
|
|
183
|
+
`quit`, `q` to stop — just without the dropdown, since there's no widget to
|
|
184
|
+
draw it in.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Session & memory model
|
|
189
|
+
|
|
190
|
+
Three independent, deliberately simple mechanisms — no vector DB, no hidden
|
|
191
|
+
retrieval, nothing the model decides to remember on your behalf:
|
|
192
|
+
|
|
193
|
+
1. **Session transcripts** (`agent/session.py`) — every message, append-only,
|
|
194
|
+
one `.jsonl` file per session under `.harness/sessions/`. Resuming replays
|
|
195
|
+
the exact original history; nothing is ever rewritten, so a crash mid-turn
|
|
196
|
+
only ever loses the one in-flight message.
|
|
197
|
+
2. **Project memory** (`HARNESS.md`) — a plain markdown file at the project
|
|
198
|
+
root, loaded in full into the system prompt every turn. You write to it
|
|
199
|
+
directly, or via `/remember` — deterministic, no LLM involved in the write.
|
|
200
|
+
3. **Context compaction** (`agent/compaction.py`) — triggers once a session
|
|
201
|
+
crosses ~75% of the active model's context window. Elides old, bulky tool
|
|
202
|
+
results first (free); if that's still not enough, summarizes what's left
|
|
203
|
+
with one extra LLM call. The full transcript on disk is untouched either
|
|
204
|
+
way — a resumed session just re-compacts on its next turn if it's still long.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Architecture
|
|
209
|
+
|
|
210
|
+
```mermaid
|
|
211
|
+
flowchart TD
|
|
212
|
+
TUI["tui.py — the main entry point"]
|
|
213
|
+
CLI["cli.py — scripting / one-shot"]
|
|
214
|
+
|
|
215
|
+
subgraph Core["agent/"]
|
|
216
|
+
LOOP["loop.py — agent_loop / run_agent_once"]
|
|
217
|
+
SESS["session.py — transcripts"]
|
|
218
|
+
COMPACT["compaction.py"]
|
|
219
|
+
SP["system_prompt.py"]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
CONFIG["config.py"]
|
|
223
|
+
MODELS["models.py"]
|
|
224
|
+
HMD["HARNESS.md"]
|
|
225
|
+
|
|
226
|
+
subgraph Providers["providers/"]
|
|
227
|
+
P["anthropic · openai · gemini · openrouter"]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
subgraph Tools["tools/"]
|
|
231
|
+
REG["registry.py"]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
TUI --> LOOP
|
|
235
|
+
CLI --> LOOP
|
|
236
|
+
LOOP --> SP
|
|
237
|
+
LOOP --> CONFIG
|
|
238
|
+
LOOP -- generate_stream --> Providers
|
|
239
|
+
LOOP -- execute --> REG
|
|
240
|
+
LOOP --> SESS
|
|
241
|
+
LOOP --> COMPACT
|
|
242
|
+
SP -. reads .-> HMD
|
|
243
|
+
Providers --> MODELS
|
|
244
|
+
REG --> MODELS
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**The loop, in one sentence:** the TUI (or the plain CLI, for scripting) hands
|
|
248
|
+
a message to `agent_loop`, which builds a system prompt (tool docs +
|
|
249
|
+
`HARNESS.md` if present), streams a response from the selected provider, and
|
|
250
|
+
— while the provider keeps returning `tool_call` — executes tools via the
|
|
251
|
+
registry and feeds results back, until it returns `text`.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Project layout
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
main.py # entrypoint → cli.run_cli()
|
|
259
|
+
tui.py # the TUI — screens, slash commands, streaming preview
|
|
260
|
+
cli.py # argparse, tool registration, dispatch to loop / TUI
|
|
261
|
+
config.py # .env loading, paths, per-provider model/key resolution
|
|
262
|
+
models.py # Message, ToolDefinition, LLMResponse, StreamChunk
|
|
263
|
+
|
|
264
|
+
agent/
|
|
265
|
+
loop.py # agent_loop (interactive) + run_agent_once (sub-agents)
|
|
266
|
+
session.py # session transcript read/write
|
|
267
|
+
compaction.py # context-window compaction
|
|
268
|
+
system_prompt.py # DEFAULT_SYSTEM_PROMPT template
|
|
269
|
+
|
|
270
|
+
providers/
|
|
271
|
+
base.py # BaseProvider ABC — generate() + generate_stream()
|
|
272
|
+
anthropic.py openai.py gemini.py openrouter.py
|
|
273
|
+
|
|
274
|
+
tools/
|
|
275
|
+
registry.py # ToolRegistry (register / execute)
|
|
276
|
+
read_tool.py write_tool.py edit_tool.py
|
|
277
|
+
bash_tool.py grep_tool.py glob_tool.py
|
|
278
|
+
web_search.py questions_tool.py todos_tool.py
|
|
279
|
+
skill_reader.py sub_agent_tool.py
|
|
280
|
+
|
|
281
|
+
.skills/ # skill instruction files (discovered via read_skill)
|
|
282
|
+
.harness/ # runtime state: sessions, todos
|
|
283
|
+
HARNESS.md # project memory (created on first /remember)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Extending the harness
|
|
289
|
+
|
|
290
|
+
**Add a tool**
|
|
291
|
+
1. Create `tools/my_tool.py` exporting `TOOL_DEF` (name / description /
|
|
292
|
+
JSON-schema `parameters`) and a `*_handler` function.
|
|
293
|
+
2. Import it and add the `(TOOL_DEF, handler)` pair to the `tool_map` list in
|
|
294
|
+
`cli.py:setup_tools()`.
|
|
295
|
+
|
|
296
|
+
**Add a provider**
|
|
297
|
+
1. Subclass `BaseProvider` in `providers/my_provider.py` and implement
|
|
298
|
+
`generate()` and `generate_stream()`, converting to/from the shared
|
|
299
|
+
`Message` / `LLMResponse` / `StreamChunk` types.
|
|
300
|
+
2. Wire it into `_get_provider()` in `agent/loop.py` and add it to the CLI's
|
|
301
|
+
`--provider` choices and the TUI's `PROVIDER_MODELS` dict.
|
|
302
|
+
|
|
303
|
+
**Add a skill** — drop a text/markdown file into `.skills/`; the agent finds
|
|
304
|
+
it via the `read_skill` tool.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Known limitations
|
|
309
|
+
|
|
310
|
+
Being upfront about what this doesn't do yet:
|
|
311
|
+
|
|
312
|
+
- **No approval gates** — tool calls (including `bash` and file writes) run
|
|
313
|
+
immediately, no confirmation step. No path sandboxing either.
|
|
314
|
+
- **One tool call per model turn** — if a model requests several tool calls
|
|
315
|
+
at once, only the first is used; the rest are silently dropped.
|
|
316
|
+
- **Sub-agents aren't truly parallel** — `spawn_sub_agent` blocks synchronously
|
|
317
|
+
while running, despite the "up to 3 in parallel" framing in the system prompt.
|
|
318
|
+
- **No retry/backoff** on transient API errors (rate limits, 5xx).
|
|
319
|
+
|
|
320
|
+
None of these are hidden — they're just not built yet.
|
|
File without changes
|