Repository navigation
Releases: Eltarras/thunc
Release list
v0.5.0 (beta)
thunc 0.5.0 makes code that uses thunc testable and extensible. thunc.testing gives your tests a scripted fake model and pytest fixtures, thunc.register_backend plugs in a backend of your own, and the docs cover Ollama. With record and replay from 0.4, you can test code that uses thunc with no model at all. A program that uses none of these behaves as in 0.4.0.
pip install --upgrade thuncA fake model for your tests: thunc.testing
from thunc import testing
def test_triage():
with testing.fake_model({"urgency": 4, "category": "bug"}) as model:
assert triage("I was charged twice") == ("bug", 4)
assert model.calls[0].inputs == {"ticket": "I was charged twice"}
def test_triage_with_pytest(thunc_model): # the same, as a fixture that comes with thunc
thunc_model.reply({"urgency": 4, "category": "bug"})
assert triage("I was charged twice") == ("bug", 4)- Every call and agent run goes to the fake model while it's active, whatever
backend=it names, and the recordings are left alone. - Replies go by function name, in order, or come from a function of the request. A string is the model's text as it is, so
"high"for anintmakes the call retry as a real one would. Anything else is sent as JSON, so4,Priority.HIGHor a dataclass work as written. model.callskeeps every request: its function, inputs, return type, the prompt sent, and which attempt or step it was.- Agents:
[testing.act("read", path="config.py"), testing.finish(60)]. The tools run for real. testing.isolated(), or thethunc_isolatedfixture, puts thunc's settings back after a block.
Your own backend: thunc.register_backend
def local(text, *, system, model, api_key, timeout):
return my_client.complete(system=system, prompt=text, model=model or "small", timeout=timeout)
thunc.register_backend("local", local, default_model="small")
thunc.configure(backend="local")- Usable as
backend=,configure(backend=...)andTHUNC_BACKEND. Agents run on it through the JSON text protocol. - Its signature is checked when it's registered. An agent's
effort=is only passed to a backend that takes it. typed=Trueregisters a backend that answers typed questions, asjevdoes.thunc.TextBackendandthunc.TypedBackendare the two shapes, for type hints. Raisethunc.TransientErrorfor errors worth asking again.
Ollama
- Ollama 0.13.3 and later serve the OpenAI Responses API the
openaibackend uses: setOPENAI_BASE_URL=http://localhost:11434/v1andOPENAI_API_KEY=ollama, thenconfigure(backend="openai", model="qwen3:8b"). live_tests/test_ollama.pychecks a model end to end: text, typed answers, and an agent on both protocols. It hasn't been run for this release; results are welcome in issues.
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows, with new tests for the fake model, its fixtures (loaded through the entry point, in a separate pytest run, as yours are) and
register_backend. The type checks intests/test_typing.pycover the new API under mypy and basedpyright.
Behavior changes
- None for existing programs. thunc now registers a pytest plugin (the
thunc_modelandthunc_isolatedfixtures), loaded by pytest when thunc is installed; it adds nothing else to your test runs.
Full changelog: v0.4.0...v0.5.0
v0.4.0 (beta)
thunc 0.4.0 completes functions and makes them testable without a model. Return an Enum; get the precise type of thunc.call(..., returns=Literal[...]) from your type checker; await calls with thunc.acall and thunc.amap; and record a test suite's answers once, then replay them in CI with no model, backend or API key. A program that uses none of these behaves as in 0.3.2.
pip install --upgrade thunc# conftest.py
thunc.configure(recordings="tests/recordings") # or THUNC_RECORDINGS=tests/recordingsTHUNC_RECORD=1 pytest # ask the model, and save every answer in tests/recordings
pytest # replay: no model is asked, and a call that wasn't recorded fails
THUNC_RECORD=missing pytest # replay what's there; ask for, and save, only what's missing- Every
thunc.call,@thunc.functionand agent run is answered from the folder. One that wasn't recorded raisesThuncErrorsaying how to record it, instead of spending money or waiting on a login. - A call is matched on its function, system prompt and request. The backend and model are saved with the answer but not matched, so CI needs no backend or key.
- A recorded answer still goes through the return type and
ensure=; one that no longer fits says to record it again. Failed calls are never recorded. - Agent runs replay the model's replies in order while the tools run for real, so files are written and commands run as when recorded.
- While replaying, anything that would still reach a model (a durable run on Temporal) fails instead.
- Recordings are one JSON file per call, in the cache's format:
thunc cache list --cache-dir tests/recordingslists them.
Enum return types (#6)
class Priority(Enum):
LOW = 1
HIGH = 3
@thunc.function
def priority(ticket: str) -> Priority:
"""How urgent is this ticket?"""
... # the model sees: exactly one of these JSON values: 1 (LOW), 3 (HIGH)- The member comes back, matched on its value like a
Literaloption. A name sent in place of a value is asked again, not guessed. - An enum's values must be strings, numbers or booleans;
Flagenums aren't supported. Onjev, an enum of strings is a choice and one of integers a score.
Async
summary = await thunc.acall("Summarize this.", {"text": note}) # thunc.call, awaited
labels = await thunc.amap(category, tickets, workers=8) # thunc.map, awaitedthunc.amapkeeps the input order, takesasync defand plain functions, and raises the first error, cancelling the calls that haven't started.- Calls run in worker threads, so the event loop stays free while the model answers.
Typing (#10)
returns=is typed as a type form (PEP 747), sothunc.call(..., returns=Literal["a", "b"])is aLiteral["a", "b"]to current mypy and Pyright, notAny. The same goes for unions,list[Item]andagent.call. Nothing is added at run time: thunc still has no dependencies.
Deprecations
thunc.ThuncDeprecationWarning, and a policy: a part of the API that's going away keeps working for at least two minor releases and warns where it's used, naming the release that removes it. It's shown by default, once per place. Nothing is deprecated yet; this is for the changes ahead of 1.0.
Measured (python -m benchmarks, stand-in models that answer at once)
| per call | |
|---|---|
A replayed call (record.call_replayed) |
37 µs, the same as a cache hit |
A call while recording every answer (record.call_recording) |
0.3 ms, the file write |
A plain call with no recordings folder (call.str) |
7.1 µs in 0.3.2, 9.8 µs now: two more environment reads, under 0.01% of a model's reply |
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows, with 81 new tests: record and replay of calls and agent runs (the text protocol and the Claude API), enums, the async API, deprecations, and the inferred types, which
tests/test_typing.pychecks with mypy and basedpyright. - Live: a call and an agent run recorded on Claude Code (native tool calls) and replayed with no backend configured.
Behavior changes
thunc.mapover anasync deffunction returns its values instead of coroutines that were never awaited.- The trace has a
"replayed"field, and thecall.endandagent.startevents havereplayed. thunc-watch 0.1.0 shows replayed calls as cached; the next thunc-watch release shows them as replayed.
Full changelog: v0.3.2...v0.4.0
v0.3.2 (beta)
thunc 0.3.2 makes agent runs on the Claude API steadier, and a trace that can't be written no longer costs the answer. Two fixes from a stability review of 0.3.1; nothing else is new.
pip install --upgrade "thunc[watch]"Fixed
- An error the Claude API sends mid-reply is retried when asking again may fix it. An overloaded, rate-limit, timeout or server error that arrives while a reply streams comes with the stream's HTTP status, 200, so thunc took it for a permanent error and the agent run failed. It's now retried like the same error arriving before the reply: twice, with a pause, as the docs say. A bad request, a bad key or a billing error still fails at once (#68).
- A trace file that can't be written warns instead of failing the call. With
trace=orTHUNC_TRACEnaming a folder that doesn't exist or a read-only file, every call and agent run raised the file's error: a successful call lost its answer, already paid for, and a failed one hid its own error. Now thunc warns once per path, and the call returns its value or raises its own error (#68).
Measured (the same harness on 0.3.1 and 0.3.2, with only the API client or backend scripted)
| 0.3.1 | 0.3.2 | |
|---|---|---|
| 10-step agent runs that finish, 5% of requests failing mid-reply | 58.9% | 99.9% |
| The same, 10% failing | 33.4% | 98.8% |
| Calls and runs with an unwritable trace that end as with tracing off | 0/20 | 20/20 |
| Time per call with a working trace | 42.5–43.4 µs | 40.9–44.5 µs (no change) |
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows, with 12 new tests for these fixes: errors sent mid-reply, in the shape the SDK raises them, retried or not by their type, and calls and agent runs with a trace path that can't be written.
Behavior changes
- An agent run on the Claude API that met an overloaded, rate-limit, timeout or server error mid-reply now retries the step, so it can take a little longer instead of failing.
- A trace file that can't be written is a
RuntimeWarning, once per path, instead of anOSErrorfrom the call. To make it an error again, turn the warning into one withwarnings.filterwarnings("error", "thunc could not write the trace"). - thunc-watch stays at 0.1.0.
Full changelog: v0.3.1...v0.3.2
v0.3.1 (beta)
thunc 0.3.1 ships the two changes that missed 0.3.0's package. The v0.3.0 tag was made on an earlier commit than intended, so these, merged before the release, didn't reach PyPI. Nothing else is new: see v0.3.0 for thunc watch and thunc write (experimental).
pip install --upgrade "thunc[watch]"Fixed
- thunc write's timing message lists its parts in a fixed order: answer, draft, test calls. The three run side by side and were listed in the order they finished, so the message changed from run to run (#65).
Docs
- The README on PyPI says beta v0.3 and its intro mentions
thunc watchand thunc write (#66). The website already had these from main.
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows, including a test that finishes thunc write's parts out of order. CI runs the dashboard's tests on Linux, macOS and Windows.
Behavior changes
- None. thunc-watch stays at 0.1.0.
Full changelog: v0.3.0...v0.3.1
v0.3.0 (beta)
thunc 0.3.0 lets you watch a program's calls and agent runs live. thunc watch app.py runs your program with a dashboard in the terminal: the calls waiting on a model, retries and why each reply was rejected, timings for each function, each agent's steps as they happen, and the --profile report when the program ends. A program needs nothing added to be watched.
It also brings an experiment, thunc write: a function declared with @thunc.function(write=True) writes its own body into your file on its first call, and runs as plain Python from then on. A program that uses neither behaves exactly as in 0.2.3.
pip install --upgrade "thunc[watch]"thunc watch support_inbox.py --limit 20 # a script and its arguments, as with thunc run
thunc watch -m myapp.triage # a module
thunc watch -- uv run app.py # any command
thunc watch --agents # agent runs in ./.thunc_agents, from any process
thunc watch --plain app.py # one line per event, for CI logs and pipes thunc watch support_inbox.py claude-code/sonnet 00:18.0 ● running
1 Overview 2 Agents 3 Calls 4 Summary
────────────────────────────────────────────────────────────────────────────────────────────
IN FLIGHT 4 running
⠙ draft_reply ticket="Password reset email never… attempt 1 4.2s ████████████
⠙ urgency ticket="Refund still not showing a… attempt 2 4.1s ██████████░░
FUNCTION CALLS CACHED RETRIES FAILED MEAN P95 MODEL RECENT
category 8 0 0 0 1.5s 2.5s 11.7s ▃▄▅█▆▇▃▄
urgency 7 0 1 0 2.4s 4.7s 16.9s ▂▄▄▆▃█▅
AGENTS 0 running
✓ repo-guide tests_for(feature="caching") finished · 6 steps 12.4s
EVENTS
17:38:13 ✓ find_order → Order(id='A-1043') 1.8s
17:38:15 ↻ urgency attempt 1: not valid JSON: 'high' 2.8s
The dashboard (#59)
- Five screens: Overview (calls in flight, a table per function, running agents, results per second, recent events), Agents (each run's steps, time split, changed files and denials), Calls (each attempt of a call: the reply and why it was rejected), Summary (the
thunc run --profiletables, opened when the program ends) and Output (what the program printed). - Mouse or keys. Click a row to select it and again to open it, or use
↑↓and⏎.fshows only retries, failures and denials;ppauses the display while the program keeps running;?lists every key. --agents [DIR]follows the agent runs recorded in a folder, from another terminal, a web app or a worker. A run whose process died partway through shows as interrupted.--save-eventsand--replaykeep a run and play it back, which is a good thing to attach to a bug report.--replayalso plays one agent run's session record.--plainprints one line per result, retry and agent step to stderr, then the report. It's what you get when the output isn't a terminal.
How it's packaged
- The dashboard is a Rust binary in its own package, thunc-watch 0.1, with wheels for macOS, Linux (glibc and musl) and Windows.
thunc[watch]installs it, so thunc itself stays pure Python with no dependencies. It's also on yourPATHasthunc-watch. - Without it,
thunc watchsays how to install it.
THUNC_EVENTS
- When
THUNC_EVENTSnames a file, thunc appends one JSON line to it per call start, model reply, call end, agent tool call and agent end;thunc watchsets it for the program it runs. To watch a program started some other way, set it yourself and runthunc watch --events FILE. - Inputs, replies and values are cut to 120-character previews;
--capture(orTHUNC_EVENTS_CAPTURE=1) sends them whole. Like a trace, they can contain personal data. - When it isn't set, nothing is written.
thunc write, experimental (#63, #64)
Experimental. Its behavior, options and the code it writes may change, or it may be removed, in a later release without a deprecation period. It edits your source files, only while you develop; review each change as a diff before you commit it.
@thunc.function(write=True)
def minutes(duration: str) -> int:
"""Convert a duration like '1h 30m', '90 min' or '2 hours' to whole minutes."""
...
minutes("1h 30m") # -> 90, and minutes() is now Python in your filethunc: writing minutes() in durations.py (first call)
thunc: checked against 6 model answers: all agree
thunc: wrote durations.py lines 5-27 in 14s (answer 4s, draft 11s, test calls 9s; side by side). Removed @thunc.function. Review: git diff durations.py
- On the first call, three requests start side by side: the call's answer, a draft of the body from the docstring and signature, and five test calls, each answered by the model on its own. The draft is linted, run on the call and the test calls, and has to match every answer; one that doesn't goes back with the failing calls, up to three drafts.
- A passing draft goes into the file in place of
..., the decorator is removed, the checked calls become doctest examples, and the call runs the new code. - When it isn't written, because the task needs judgment or no draft passed, the call returns the model's answer and the file is untouched. The reason is kept in
.thunc_write/until the docstring or signature changes. - It's refused, with a warning, in CI or with
THUNC_WRITE=0(set it in production), outside your project, and for read-only, installed or changed files and nested functions. thunc write FILE::FUNCTION [--dry-run]writes one ahead of its first call, or shows the diff.
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows, including the events and the
thunc watchcommand. CI runs the dashboard'scargo fmt,clippyand tests on Linux, macOS and Windows, and on Linux builds the PyPI wheel and runsthunc watchfrom it. - thunc write: 45 offline tests of the source editing and the writing, end to end against a scripted model, including CRLF files; live,
live_tests/test_writing.pyon Codex wrote a function whose doctests pass. watch/demo/demo_app.pyruns the support-inbox functions and an agent against a scripted backend, with delays and bad replies, and makes no model calls.
Behavior changes
- None.
thunc watch,THUNC_EVENTSandwrite=are new; nothing is written unlessTHUNC_EVENTSis set, and no file is edited unless a function haswrite=True. Nothing is removed.
Known issues: the dashboard only watches: it can't stop a single agent run or approve ask: actions yet. --agents sees one folder, not every project on the machine. On Windows, Ctrl+C and quitting end the program at once, without the KeyboardInterrupt it gets on macOS and Linux. thunc write's checks are only as good as the model's answers, so review the code it writes; thunc write can't write methods ahead of their first call, and it doesn't run on jev. The full list is in CONTRIBUTING.md, and every change is in CHANGELOG.md.
Full changelog: v0.2.3...v0.3.0
thunc-watch v0.1.0 (beta)
The first release of thunc-watch, the thunc watch dashboard: a live view in the terminal of a program's thunc calls and agent runs. It's a compiled binary published on its own, so thunc stays pure Python. thunc 0.3.0 installs it with:
pip install "thunc[watch]"This release only publishes the thunc-watch package to PyPI (wheels for macOS, Linux glibc and musl, and Windows). The guide is at Watching a program, and thunc's own notes come with v0.3.0.
v0.2.3 (beta)
thunc 0.2.3 is the last 0.2 release. It makes native tool calls the way agents work wherever they run: on Codex now, as on Claude Code, and in durable runs on Claude Code, which pick up after a crash mid-call. Durable agents can take tools=, the Claude API agent path no longer loses runs to max_tokens or a stalled reply, and edit can change many places at once.
| 8 tool-heavy tasks, Claude Sonnet 5.5, 3 runs each | Passed | Seconds per task | $ per task |
|---|---|---|---|
| thunc 0.2.2 agents, text protocol (Codex, durable runs, fallbacks) | 20/24 | 47 | 0.084 |
| thunc 0.2.3 agents, text protocol | 24/24 | 27 | 0.082 |
| thunc 0.2.3 agents on Claude Code, native calls | 24/24 | 8 | 0.021 |
| Claude Code itself, for reference | 24/24 | 10 | 0.078 |
Through the Claude API, thunc's agents passed 8 of 8 on Sonnet 5.5 ($0.023 a task) and on Opus 5.5 ($0.049 a task). On Codex, native calls took 30 seconds a task, against 45 for the text protocol 0.2.2 used there and 27 for Codex itself (16 of 16 each).
pip install --upgrade thuncimport thunc
thunc.configure(backend="codex") # agents now make native tool calls here too
def find_issue(title: str) -> int: # a tool is your own code, with a docstring for the model
"""Find an open issue by title. Returns its number, or 0."""
return tracker.find(title)
triage = thunc.Agent("triage", workdir=".", tools=[find_issue], effort="high")
@triage.task
def flaky_tests() -> list[str]:
"""Run the tests three times and list the ones that fail only sometimes."""
...# On a Temporal worker: durable runs take the agent's own tools now, and name those that may run
# again after a crash.
registry.agent_task("triage", flaky_tests, version="1", workspace_id="repo", retry_safe_tools=["find_issue"])Native tool calls on Codex (#55)
- The agent's tools are an MCP server that Codex calls, through the same relay as Claude Code: thunc carries out each call with its own tools, permissions and run record. Codex's own tools and your
~/.codexconfig stay out, its sandbox stays read-only, and only thunc's server is approved to run without asking. - A turn that ends without
finish, or fails, is continued withcodex exec resume. The run's Codex session is deleted when the run ends. - Fallback: when Codex can't start them, a run uses the text protocol with a warning.
protocol="native"raises instead, andprotocol="text"keeps the old way.
- On Claude Code, native calls in segments. One activity keeps one
claude -pprocess for many model replies, and checkpoints the run and Claude Code's session before each reply's calls. If the worker stops or the CLI dies, the retried activity restores the session and continues it with--resume. The call that was in flight, asked for again, gets its old journal entry: it's replayed, waits forresolve(), or runs. Never twice. tools=. Each call of the agent's own functions is journaled like a command: recorded before it runs and after, replayed when completed, and waiting forresolve()if a worker stopped mid-call.retry_safe_tools=names the ones that may simply run again.
The Claude API agent path (#52)
Agent(effort=...)on every backend. Claude 4.6 and later on the API get"high"by default; Claude Opus 5.5's own default is"medium", which is low for agentic coding.- Replies are streamed with room for 64,000 tokens. A reply cut off at
max_tokensgets an error result for its calls instead of ending the run,pause_turncarries on, and a reply that sends nothing fortimeoutseconds is stopped and asked again (one Opus reply in the benchmark sent nothing for an hour). - Old tool results are cleared on long runs (context editing), tools are strict where the model and schema allow, and connection errors, rate limits and server errors are retried as a step.
Steadier agents everywhere (#51, #52, #53)
- The text protocol reads replies that aren't only the action: the first complete action amid prose, a fence,
<invoke>markup or made-up results, with a note to the model to keep to JSON. On Claude Code, a text step stops as soon as its action has arrived. finishin the same reply as other calls is refused (except besideremember): in the benchmark, a run returned a guess written before its own read came back.- The model is told when few steps are left, in its last three replies before
max_steps.
edittakesreplace_all, andeditsfor several changes to one file in one call, all made or none.editno longer needs a priorread: it only changes text the agent quotes exactly.writestill replaces only a file read withread.- Large system prompts work on Claude Code: the prompt goes in a file, not on the command line, where large
follow=files could pass the operating system's limit.
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows; Temporal integration tests on Python 3.10 and 3.14, including a Claude Code run whose CLI dies mid-run. Tests can't reach a real
claude,codexorjev. - Live: the tool-use benchmark on Claude Code (Sonnet 5.5), the Claude API (Sonnet 5.5 and Opus 5.5) and Codex, with the results in
live_tests/bench_tooluse_report.md; a durable run on real Claude Code with the CLI killed after its first tool result, which finished with each effect done once.
Behavior changes
- Agents on
codexmake native tool calls by default, and fall back to the text protocol if they can't. - Durable runs on
claude-codemake native tool calls. Runs already in progress keep the text protocol. - Agents on the Claude API think at effort
highon Claude 4.6 and later. editno longer needs a priorread;writestill does.finishin the same reply as other calls is refused, except besideremember.- Nothing is removed.
Known issues: on Codex, native calls count each tool call as a step, so a long task reaches max_steps sooner, and their tokens aren't reported. Durable runs on Codex use the text protocol. On a fix in a long file, native calls take about twice Claude Code's steps. The full list is in CONTRIBUTING.md, and every change is in CHANGELOG.md.
Full changelog: v0.2.2...v0.2.3
v0.2.2 (beta)
thunc 0.2.2 makes agents use their tools reliably, especially on Claude Code, and makes calls faster. Agents on Claude Code now make native tool calls instead of writing each action as JSON text, a failed step is retried instead of ending the run, and the agent's tools fill gaps that a new tool-use benchmark found.
| 8 tool-heavy tasks, Claude Sonnet 5.5 | Passed | Seconds per task | $ per task |
|---|---|---|---|
| thunc 0.2.1 agents on Claude Code | 12/24 | 99 | 0.084 |
| thunc 0.2.2 agents on Claude Code | 24/24 | 10 | 0.022 |
| Claude Code itself, for reference | 24/24 | 11 | 0.069 |
On Claude Opus 5.5 both versions passed every task, but 0.2.2 took 15 seconds and $0.051 a task instead of 44 seconds and $0.264.
pip install --upgrade thuncimport thunc
thunc.configure(backend="claude-code") # agents now make native tool calls here
fixer = thunc.Agent("fixer", workdir=".", permissions=["write:src/**", "run:pytest"])
@fixer.task
def make_tests_pass() -> str:
"""Run the tests in services/api and fix what fails. Say what you changed."""
...
run = fixer.run(make_tests_pass) # run.commands, run.files_changed, and any retries in run.sessionthunc run --profile my_script.py # where the time went: model time, thunc's own, per toolAgents on Claude Code make native tool calls (#46)
- The agent's tools are an MCP server that one
claude -pprocess per run calls, and thunc carries out each call with its own tools, permissions and run record. Before, the model wrote each action as JSON text. Current models slip back into their trained tool calls: they invented tool results and ran on to the timeout, or the CLI refused a tool call it couldn't parse. - Calls from one model reply count as one step. A turn without a tool call gets a nudge, and a turn that ends in an error gets a second chance, both in the same session.
- Fallback: with a
claudeCLI too old for these options, or with MCP servers turned off by a policy, a run falls back to the text protocol with a warning.protocol="native"raises instead, andprotocol="text"keeps the old way. - The CLI runs in the agent's
workdir, so the environment details it adds to the prompt name the right folder.
Steadier runs (#46)
- A failed step is retried twice before the run fails, and each retry is logged in the run record. This covers timeouts, lost connections, rate limits, server errors and CLI calls that end in an error (
thunc.errors.TransientError). Before, one bad call ended the run. - A text-protocol step on Claude Code or Codex now times out after 120 seconds instead of the whole 300-second
timeout.
Better tools for agents (#46)
runtakescwd: a folder insideworkdirto run the command in.- The
shellpermission (opt-in) runs command lines through the system shell, so pipes,&&,cdand redirects work. It can't be combined with!run:rules, since a shell command line can't be checked word by word. searchtakesglob:*.pymatches by file name,src/**/*.tsby path.- Git-aware
listandsearch: in a git repository they leave out what git ignores, so a stalebuild/copy no longer crowds out the real source. - Long command output keeps its start and its end, so an agent sees the first error as well as the summary.
Faster calls (#45)
- The
anthropicandopenaibackends reuse their connections: one SDK client per process instead of a new TCP and TLS handshake each call. In a local benchmark with 60 ms of connection setup, 20 calls in a row went from 1.47 s to 68 ms. - Text-protocol agents can act several times per reply (Codex and
protocol="text"), with a JSON array of independent actions. On Codex, the prompt eval took fewer replies (fix / review / analysis: 6.0 / 4.8 / 5.0 → 5.0 / 3.0 / 3.6) and less time (37 / 27 / 27 s → 29 / 19 / 21 s). - The
codexbackend returns as soon as the answer arrives, without waiting about 0.4 s for Codex to shut down. thunc run --profile SCRIPT(or-m module) reports where a program's time went, per function: calls, cache hits, retries, failures, model time against thunc's own, and time per tool for agents.python -m benchmarksruns a dependency-free benchmark suite.
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows. Temporal integration tests run on Python 3.10 and 3.14.
- The MCP path has an end-to-end test with a fake
claudethat starts the real tool server, covering parallel and serial calls, permissions, errors and the fallback. - Live: the tool-use benchmark (
live_tests/bench_tooluse.py, with its report inlive_tests/bench_tooluse_report.md) on Sonnet 5.5 and Opus 5.5, the prompt eval on Claude Code (45/45), and the live tests on Claude Code. - The agent loops on the Claude and OpenAI APIs haven't had a live tool-use benchmark yet.
Behavior changes
- Agents on
claude-codemake native tool calls by default, and fall back to the text protocol if they can't. - In a git repository,
listandsearchleave out what git ignores. A folder named explicitly is still listed and searched. - Long command output keeps its start as well as its end.
- The
claude-codebackend loads none of your Claude Code settings (--setting-sources ""). NoCLAUDE.md, settings or hooks reach thunc's calls, plain function calls included, so a folder you point an agent at can't instruct it unless you passfollow=. - Durable runs on Claude Code still use the text protocol.
- Nothing is removed.
Still beta: expect bugs, and the API may change. Known issues are listed in CONTRIBUTING.md, and every change is in CHANGELOG.md.
Full changelog: v0.2.1...v0.2.2
v0.2.1 (beta)
thunc 0.2.1 adds durable agents: an optional runtime on Temporal that records each model turn and tool call. A run survives worker restarts, and you can reattach to it from another process. Local thunc stays dependency-free, and agent.run() works as before.
pip install --upgrade "thunc[temporal]"from thunc.temporal import Registry, Runtime, Worker
# Worker process: register an existing @agent.task with a version and a workspace.
registry = Registry(state_dir="/srv/thunc-state")
registry.agent_task("repo.review", review_task, version="1", workspace_id="repo")
worker = await Worker.connect("localhost:7233", task_queue="repo-v1", registry=registry)
await worker.run()
# Client process: submit by a stable request ID, then reattach from anywhere.
runtime = await Runtime.connect("localhost:7233", task_queue="repo-v1")
handle = await runtime.start("repo.review", version="1", workspace_id="repo", inputs={},
returns=str, request_id="review-123", deadline_seconds=1800)
run = await handle.result() # DurableRun[str]Durable agents
thunc.temporal:Registry,Worker,RuntimeandHandlelet you register versioned tasks, then start, reattach to, inspect, cancel and resolve runs. The same request ID reattaches to the same run; the same ID with different inputs is rejected (#37).- Resumes where it stopped: each model turn and each tool call is its own recorded step. After a worker restart, completed turns aren't sent to the model again (#37).
- Recoverable file writes and memory: an intent and receipt journal, atomic replacement and content hashes. An edit made outside the run is reported as a conflict, never overwritten (#37).
- Commands are never run twice by accident: if a worker dies while a command is running, the run waits in
needs_attentionuntil an operator callsresolve()withcomplete,abortorretry(#37). - One run at a time per workspace, in order, through a coordinator workflow (#37).
- Bounded retries for transient provider errors (connection errors, 408, 429 and 5xx), at most three attempts, with the provider SDK's own retries turned off (#37).
- Composition:
thunc.temporal.adapters.execute_taskcalls registered tasks from your own Temporal workflows.examples/temporalhas a runnable classify → agent analysis → typed summary pipeline (#38).
What durable mode needs
A Temporal service (the local dev server or your own), a worker process, and persistent storage on one volume. Temporal doesn't back up your workspace or reverse side effects. The Temporal guide covers setup, recovery, storage and upgrades.
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and on Windows.
- Integration tests run against the official local Temporal service on Python 3.10 and 3.14, without model calls. They cover worker kill and restart, history replay, Continue-As-New, duplicate submissions, cancellation and crash recovery in the write journal.
- Durable mode hasn't been smoke-tested against live model providers yet.
Behaviour changes
- Local and durable runs share one agent engine (#36). A
remembercall that fails no longer appears inRun.notes. - Agents with
tools=ortimeout=can't be registered for durable runs yet; durable runs usedeadline_seconds=instead. - Nothing changes for
@thunc.function,thunc.callor localagent.run().
Still beta: expect bugs, and the durable API may change. See CHANGELOG.md.
Full changelog: v0.2.0...v0.2.1
v0.2.0 (beta)
thunc 0.2 adds agents: typed functions that can look around before they answer. An agent lists, reads and searches files in a working directory, and, when its permissions allow, writes, edits and runs commands, then returns a checked value of your return type.
pip install --upgrade thuncimport thunc
repo = thunc.Agent("repo-guide", workdir="~/code/myapp")
@repo.task
def request_timeout() -> int:
"""Find the HTTP request timeout this app uses, in seconds."""
...
request_timeout() # -> 45, after the agent searched the code and read the file that sets itAgents
thunc.Agentand@agent.task: tasks are declared like@thunc.function, sync or async.@thunc.agent(name, workdir=...)declares a one-task agent in one go, andagent.call(...)runs a task built in code (#23, #34).- Permissions: by default an agent reads everything in
workdirand writes nothing. Rules such as["write:src/**", "run:pytest", "!read:.env*"]open up thewrite,editandruntools; a deny always wins. Every path must stay insideworkdir, and a file is only edited after the agent read it in the same run (#26, #28). - Commands run without a shell, with a minimal environment (your API keys don't reach them) and a time limit that also stops their child processes. Permissions are not a sandbox:
run:pytestruns the project's code (#28). - Your own functions as tools:
tools=[open_issue]; arguments are checked against the type hints (#34). - Memory between runs: the agent saves notes with
remember. They're kept in.thunc_agents/<name>/memory.md, a plain file you can edit, along with one JSONL record per run (#23). - A record of each run:
agent.run(task, ...)returns athunc.Runwith the value, files changed (by edits and by commands), commands, denials and steps. A failed run raisesthunc.AgentErrorcarrying the partial record (#29, #34). - Instruction files:
follow=Truegives the agentAGENTS.md/CLAUDE.mdas instructions. It's off by default, so a folder you point an agent at can't instruct it (#31). - Native tool calls on the Claude and OpenAI APIs, with the fixed part of the prompt cached. Claude Code and Codex use a JSON text protocol (#32).
- System prompt presets:
thunc.prompts.CODING,CODE_REVIEWandANALYSISforsystem=(#34). - Limits:
max_steps=40andtimeout=(seconds) per run (#34).
Tested
- Offline tests run on Python 3.10–3.14 on Ubuntu, and now on Windows.
- Live agent tests passed on Claude Code, Codex, the Claude API and the OpenAI API.
- A prompt evaluation (
live_tests/eval_prompts.py) passed 90/90 runs on the Claude API and Claude Code. The tasks are too easy to tell the prompt versions apart, so the presets are opt-in.
Behaviour changes
thunc.agentis now the@thunc.agentdecorator.from thunc.agent import Agentstill works.- Agents refuse the
jevbackend before a run starts; use Jev with@thunc.function. - Nothing changes for
@thunc.functionandthunc.call.
Still beta: expect bugs, and the agent API may change. The new CHANGELOG.md lists every release.
Full changelog: v0.1.3...v0.2.0