A small, headless-first harness that lets small local models (and frontier ones) do real work with a tiny prompt.
uv tool install tether-sdk
tether run --model ollama/qwen3.5:9b --tools bash,edit --verify "pytest -q" "fix the failing tests"Most agent frameworks treat the context window as a dumping ground. Before your task is read, they inject 3,000 to 8,000 tokens of JSON tool schemas and system prompt, then append every tool output forever. On a frontier model that is money. On a 9B model running on your laptop it is fatal: attention is diluted, small models fumble nested JSON, and changing the tool list between turns throws away the inference server's KV cache.
Tether is built the other way round:
- One tool, not thirty. The model sees a single
act(cmd, stdin)schema. Everything else (bash,edit,web,mem, your own functions) is a command behind it, off by default. Fixed framing is under 150 tokens. - Old output folds away. Tool results stay in full while there is room; once the history passes half the context, results older than two turns become a one-line tombstone. Everything lives in SQLite and comes back by search.
- The envelope is deterministic. Sandboxing, state, step limits, verification gates and termination are plain code. The model only reasons; it is never trusted with safety.
Tether answers to one number: how many real coding exercises a small local model finishes. On the Polyglot-Python set (Aider's 34 Python exercises, graded by each exercise's own tests), qwen3.5:9b through Ollama, default config, 2 trials each (68 runs). The context was 8192 tokens up to v0.2.15 and is 32768 (the default since v0.2.16) for v0.2.18:
| before v0.2.10 | v0.2.10 | v0.2.13 | v0.2.15 | v0.2.18 | |
|---|---|---|---|---|---|
| Passes | 35 of 68 (51.5%) | 35 of 68 (51.5%) | 37 of 68 (54.4%) | 40 of 68 (58.8%) | 45 of 68 (66.2%) |
| Mean score | 0.718 | 0.763 | 0.783 | 0.756 | 0.830 |
| Runs killed by the wall cap | 11 | 14 | 13 | 12 | 17 |
The target is 74% (50 of 68) by v0.4.0, so this is the number every change is ranked by. It is what qwen3.5:9b can already do: 27 of the 34 exercises have passed at least once from scratch, so the work is making each of them pass every run. For comparison, the best published harness for this model, little-coder, reports 52.9% on the same 34 Python exercises (its own setup). v0.2.18 passes 5 more runs than v0.2.15, and this time the paired score delta clears the noise: +0.074, 95% interval +0.003 to +0.147 (pass flips +7/-2). It comes with the larger default context, so the gain belongs to both. connect and hangman passed for the first time; more runs now end at the wall cap (17, against 12) instead of running out of steps. One run is noisy (about 0.1), so read the pass count and the failure breakdown, not one decimal. The second yardstick is the Coding Ladder: 24 tasks, 6 tiers of difficulty times write, bugfix, plan and review, which shows how far up a model climbs and what stops it. Method, how to reproduce both and fair-comparison rules: Benchmarks.
flowchart LR
You["CLI · SDK · MCP"] --> Loop
subgraph Envelope["Deterministic envelope"]
Loop["bounded loop<br/>steps · stall guard · gates"] --> Mux["act(cmd, stdin)<br/>command bus"]
Mux --> Sand["sandbox<br/>bwrap · workspace jail"]
Sand --> Tools["bash · edit · web · mem · agent · your @tool"]
Tools --> Fold["fold old output"]
Fold --> DB[("SQLite WAL + FTS5<br/>.tether/tether.db")]
DB --> Loop
end
Loop <-->|"messages"| Model["LLM<br/>ollama · openai · anthropic"]
A session is a row in SQLite, so it survives crashes, runs detached (--headless), takes new instructions while running (tether send), and can be read by another agent as compact JSON. A task ends only when the model calls done, after your --verify commands pass. To re-run a finished session's goal and compare the two runs, use tether replay <session-id>.
| Decision | Instead of | Why |
|---|---|---|
One act tool, commands behind it |
A JSON schema per tool | CodeAct and SWE-agent results: one executable channel beats many schemas, most for 7-9B models; and the prompt prefix never changes, so the KV cache survives. |
Progressive --help |
Full usage up front | A tool costs about a dozen tokens until the model asks or gets it wrong. |
| SQLite (WAL, FTS5) as memory | An ever-growing transcript | Context stays a bounded working set; the rest is searchable. State is ACID, so sessions resume and agents share results. |
Kernel sandbox (bwrap), env scrubbing, SSRF guard |
"Please be careful" in the prompt | Safety that depends on the model obeying is not safety. |
Explicit done plus verification gates |
Letting the model declare success | Small models stop early or never stop. The gate is a command you wrote. |
Thin httpx adapters, 2 runtime dependencies |
Vendor SDKs, LangChain-style frameworks | Easy to read, easy to add a backend (providers.yaml, no code), nothing to upgrade around. |
The research behind it: Maia et al. (2026) on probabilistic core and deterministic envelope, Wang et al. (2024, CodeAct), Yang et al. (2024, SWE-agent) and Packer et al. (2023, MemGPT); see References and Architecture.
uv tool install tether-sdk # or: pip install tether-sdk
ollama pull qwen3.5:9b # any model works; see the providers page
tether run --model ollama/qwen3.5:9b --tools bash,edit "create hello.txt containing hi"
tether run --worktree --setup "uv sync" --verify "uv run pytest -q" "fix the failing test"On Linux install bubblewrap for write sessions; on macOS/Windows pass --allow-unsandboxed, in Docker set TETHER_SANDBOX=container.
An empty Ollama reply is resent once, unchanged; if three empty replies come in a row, the session stops and suggests think=false. See the providers page.
Resume a failed pipeline from its first unfinished stage with tether pipeline --resume <pipeline_session_id>.
Everything else (headless sessions, skills, pipelines, MCP, the CLI reference, writing your own tool or provider) is in the docs: igormcsouza.github.io/tether. Start with the guides: fix a failing test, use it from Claude Code or Codex.
import asyncio
from tether import Harness, tool
@tool(name="price", description="Look up the unit price of a fruit")
def price(fruit: str) -> str:
return f"{fruit}: 0.50 EUR"
async def main() -> None:
harness = Harness(model="ollama/qwen3.5:9b", tools=[price])
session = await harness.run("What does an apple cost?")
print(session.result)
asyncio.run(main())Harness, Pipeline, Skill, SandboxPolicy, @tool and register_provider are the public surface. The agent-facing JSON and the optional tether mcp server (tether-sdk[mcp]) are specified in the reference.
Tether stays lightweight, reliable, extensible, model-agnostic, easy to use, resourceful and a token saver; a change that fails one of those lines is reworked. Read AGENTS.md (the rules and the module map) and CONTRIBUTING.md (setup, checks, golden traces, e2e, the bench, releasing). Open an issue before a large change; small fixes with a test and a docs line are welcome as they are.
tether/ harness · multiplexer · context · store · sandbox · providers/ · tools/
tests/ unit, golden traces (what the model sees), 100% coverage
e2e/ real CLI and real Ollama behind a fault-injecting proxy (local)
bench/ Polyglot-Python and model checks (developers)
docs/ the user site (MkDocs)
- Maia, P. H. M., et al. (2026). LLM-Native Applications as a Paradigm for AI-Driven Software Architecture.
- Wang, X., Chen, Y., Yuan, L., Zhang, Y., Li, Y., Peng, H., & Ji, H. (2024). Executable Code Actions Elicit Better LLM Agents (CodeAct). In Proceedings of the 41st International Conference on Machine Learning (ICML 2024). arXiv:2402.01030
- Yang, J., Jimenez, C. E., Wettig, A., Lieret, K., Yao, S., Narasimhan, K., & Press, O. (2024). SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering. In Advances in Neural Information Processing Systems (NeurIPS 2024). arXiv:2405.15793
- Packer, C., Wooders, S., Lin, K., Fang, V., Patil, S. G., Stoica, I., & Gonzalez, J. E. (2023). MemGPT: Towards LLMs as Operating Systems. arXiv:2310.08560
- Gumaan, E. (2026). Feedback That Backfires: Why Small Language Model Agents Repeat the Call They Just Watched Fail. Source of the restart rule: after repeated failures, start a context that describes the state instead of replaying the failed attempts. arXiv:2608.23651
- Kim, J., et al. (2026). Scaling Test-Time Compute for Agentic Coding. A restart carries a summary of what was tried, not the transcript. arXiv:2604.16529
- Yang, Z. (2026). Why Retrying Fails: Context Contamination in LLM Agent Pipelines. Why a retry should not replay the failed context. arXiv:2605.08563
- Olausson, T. X., Inala, J. P., Wang, C., Gao, J., & Solar-Lezama, A. (2024). Is Self-Repair a Silver Bullet for Code Generation? In ICLR 2024. Repair is limited by feedback quality, the reason a failing test run shows the failing test's source. arXiv:2306.09896
- Zhong, L., Wang, Z., & Shang, J. (2024). Debug like a Human: A Large Language Model Debugger via Verifying Runtime Execution Step-by-step (LDB). In Findings of ACL 2024. Showing the values behind a failure improves repair. arXiv:2402.16906
- Huang, Y., et al. (2026). Regression Accumulation in Multi-Turn LLM Programming Conversations. In ASE 2026. Later edits break earlier passing tests; checking against them helps every model, the reason a test run names the tests the last change broke. arXiv:2607.01855
- Wang-Lin, X., Isopoussu, A., & Mahon, L. (2026). If It's Not Buggy, Don't Fix It: On the Dynamics of Iterative Bug-fixing with LLMs. Repeated fixing falls into cycles that undo and redo the same change, the reason a rewrite that left the code as it was is named. arXiv:2609.10123