tinyharness is the loop that turns an LLM into an agent: it reads, searches and edits your code, runs commands in a sandbox, asks before it changes things, and keeps long sessions inside the context window. It works as a coding agent in your terminal and as a Python SDK for building your own. The agent core is about 9k lines of code with no framework underneath, so you can read the whole thing in an afternoon.
Why tinyharness
- Readable. About 9k lines of code for the agent core (12k with the terminal UI),
commented for people learning how agents work. Start with
agent/loop.py. - Any model, even yours. Anthropic, OpenAI, Gemini, xAI, OpenRouter, DeepSeek, Mistral and Groq, each through its own current API, plus any OpenAI-compatible server: vLLM, Ollama, llama.cpp, SGLang.
- Robust with small models. Cut-off replies, empty replies, tool calls written as text, malformed arguments and repeated failures are handled by the harness, so mid-size open models get work done too. How.
- Safe without the friction. Commands run with filesystem and network isolation;
routine contained work doesn't ask, while a newly required exact host can be approved
without dropping containment. Downloading new executable code asks once; cached tools
then run normally, and bare Python installs use a project
.venv. API keys never reach the model. Once outside content is in the conversation, anything that could send data out asks a person first. - Small and fast. Seven core tools and a short prompt (about 3k tokens before your instructions) that stays the same from call to call, so the provider's cache hits.
- Extensible. One Python file with
setup(th)adds tools, prompt sections, commands and hooks. Web search, PDFs, subagents and parallel teams are built-in extensions you turn on.
Quickstart
Requires Python 3.11+. Install the command with uv (or pipx):
uv tool install tinyharness-lm # or: pipx install tinyharness-lm
cd your-project && tinyharness
The first run asks which provider to use and for its API key; one key for any supported
provider is enough. uv tool upgrade tinyharness-lm updates it.
In the session, type / to autocomplete commands. The ones you'll use most:
| Command | |
|---|---|
/model |
pick a model from any provider you have a key for |
/mode |
change the permission mode (or press Shift+Tab) |
/effort |
how hard the model reasons, low … max, the same for every model |
/skills, /<skill> |
find or run skills |
/extensions |
what's on and what it costs per request; /extensions on web |
/sandbox |
what commands can write, read and reach |
/btw <question> |
a quick side question, even while the agent works; not added to the conversation |
/sessions, /context, /clear |
switch conversations, inspect context, or start fresh |
/key [provider] |
add or replace a provider key saved in auth.json |
Useful flags: -c (continue the latest conversation), -r (pick one), -p "prompt"
(run one prompt and exit), --model provider:model, --mode ask, --cwd <dir>.
Use it from Python
uv add tinyharness-lm # or: pip install tinyharness-lm
import asyncio
from tinyharness import AssistantText, Session, SessionOptions, ToolUse
async def main():
async with Session(SessionOptions()) as session:
async for event in session.run("write fib.py with a fib(n) function"):
match event:
case AssistantText(text=text):
print(text, end="")
case ToolUse(summary=summary):
print(f"\n● {summary}")
asyncio.run(main())
The CLI and the SDK consume the same typed event stream. For headless runs, pass
permission_callback= to decide what would otherwise prompt. Extensions can be passed in
code (SessionOptions(extensions=[...])) and tested against a scripted model
(tinyharness.testing). Subagents can be defined in
code (AgentSpec) or Markdown, each on any provider's model, and conversations can be
resumed from code (SessionOptions(resume="latest")). Post-turn evaluators can be passed
with SessionOptions(evaluators=[...], evaluator_timeout=30.0); their bounded results are
appended to the versioned trace without changing the completed conversation. See examples/ for a
one-shot query, a multi-turn session, resuming, a custom tool, a permission policy with
hooks, skills, subagents on different models, an extension, and a post-turn evaluator.
What's inside
| Agent loop | Streaming, parallel read-only tools, argument validation and repair, recovery from cut-off and empty replies, retries with backoff |
| Tools | Core: Read Write Edit Glob Grep Bash Open. Built-in extensions you turn on: web (WebSearch, WebFetch), pdf, todo, agents (Agent, Team) |
| Extensions | One Python file with setup(th) adds tools, prompt sections, slash commands and event handlers (docs/extension-api.md); `/extensions on |
| Permissions | Routine work in the project and the sandbox doesn't ask; anything beyond it asks, with a diff for edits. Modes default, auto (an LLM classifier for unclear cases), plan, ask (every edit and command) and bypassPermissions; persistent allow/deny rules |
| Sandbox | Native Seatbelt on macOS; optional Sandbox Runtime on Linux, Windows and as a fallback. Commands have narrow writes, blocked credential reads (SRT covers the home and project neighborhoods) and allowlisted network access; /sandbox shows it |
| Context | Cap oversized messages, stub old tool output, then summarize, without rebuilding the prompt cache every turn |
| Subagents and teams | Isolated contexts on any model; Team runs a dependency graph of tasks; the model picks the team's size, up to 20 subagents at once (max_parallel_agents) |
| Skills, hooks, plugins | Portable SKILL.md skills, shell or Python hooks on 5 events, and plugins that bundle them |
| Resume & traces | Lossless, schema-versioned JSONL logs outside your repo with correlated main-loop model/tool decisions and latency; on resume, the agent is told what changed while you were away |
Each of these in detail: docs/features.md.
How it works
CLI (cli/) ──┐
├──► Session (session.py) ──► agent loop (agent/loop.py) ──► LLM (llm/client.py ─ llm/adapters/)
SDK ─────────┘ wires components │ retries, model lists
├─ compaction (agent/compaction.py)
├─ runaway / empty-reply recovery (agent/runaway.py)
├─ permission gate (permissions/gate.py) ── hooks
└─ tools (tools/*.py) ── workspace, sandbox + allowlist proxy
│
└─ extensions (ext.py, extensions/) add tools,
prompt sections, commands and hooks; the agents
extension's Agent / Team ──► subagents
(subagents/runner.py): the same loop, fresh
history, any provider:model
Suggested reading order:
| # | File | What you'll learn |
|---|---|---|
| 1 | agent/loop.py |
The core loop: stream the model, validate calls, gate them, run them, repeat |
| 2 | tools/base.py, tools/read.py |
How tools are defined and exposed to the model |
| 3 | permissions/gate.py |
The layered permission check |
| 4 | agent/compaction.py |
Staying inside the context window without breaking the prompt cache |
| 5 | agent/history.py |
The conversation, and the normalization every request goes through |
| 6 | session.py |
Wiring it all together; state that persists between prompts |
| 7 | llm/adapters/ |
One small adapter per provider API; the rest of the code never sees a provider's format |
| 8 | ext.py, extensions/ |
How a feature plugs in: the built-in extensions use the same API as yours |
| 9 | subagents/runner.py |
Subagents are the same loop with a fresh history |
| 10 | cli/ |
Rendering events (render.py), the REPL, slash commands; colors in theme.py, Scout in scout.py |
Every prompt sent to the model is a Markdown file in tinyharness/prompts/.
Docs
- docs/features.md: everything above, in detail
- docs/configuration.md: API keys, providers, gateways, self-hosted models, where files live
- docs/extending.md: extensions, custom tools, hooks, subagents and plugins
- docs/extension-api.md: the extension API in full
- docs/skills.md: writing and sharing skills
- CHANGELOG.md and AGENTS.md (how to work on tinyharness, for people and agents)
Development
git clone https://github.com/anujdutt9/tinyharness && cd tinyharness
uv sync # installs dev tools too
uv run tinyharness
uv run pytest # tests use a scripted fake LLM; no API key needed
uv run pytest --terminal # also the screen-layout tests, in an emulated terminal (slower)
uv run ruff check . && uv run ruff format --check . && uv run mypy
uv build && uv run python scripts/check_dist.py # build and smoke-test the package
Releasing: bump __version__ in tinyharness/version.py, add a
CHANGELOG entry, then push a matching tag (git tag v0.2.0 && git push --tags).
The release workflow tests, builds and publishes to PyPI.
License
Metadata
Release files for tinyharness-lm 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tinyharness_lm-0.1.0.tar.gz | 3.0 MB | Without provenance | Details for tinyharness_lm-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tinyharness_lm-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for tinyharness_lm-0.1.0-py3-none-any.whl |
Total release size: 3.3 MB
Release files / tinyharness_lm-0.1.0.tar.gz
| Download URL | tinyharness_lm-0.1.0.tar.gz |
|---|---|
| Size | 3.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
37e1b20aa787d8232baaac454ce75451fef862963ccc576d62009619a0a6cac2
|
|
BLAKE2b-256 checksum How to use checksums |
8f8ec72fcc87df3c62e06ff45ed14d3de01827c16be6bb4e3467105f808a0294
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / tinyharness_lm-0.1.0-py3-none-any.whl
| Download URL | tinyharness_lm-0.1.0-py3-none-any.whl |
|---|---|
| Size | 314.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cac0f3f5338c81753ddadd95b0bb40844efc5e9ffa3431dc4a1c079b850ccd61
|
|
BLAKE2b-256 checksum How to use checksums |
1bb5b1c7755c0a054aababe5adc33c14ca5097ba7e9052708a6a0b609b5f34aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for tinyharness-lm 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.