Skip to content

Releases: Eltarras/thunc

v0.5.0 (beta)

v0.5.0 (beta) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 12 Oct 06:18
51b834e

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 thunc

A 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 an int makes the call retry as a real one would. Anything else is sent as JSON, so 4, Priority.HIGH or a dataclass work as written.
  • model.calls keeps 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 the thunc_isolated fixture, 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=...) and THUNC_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=True registers a backend that answers typed questions, as jev does. thunc.TextBackend and thunc.TypedBackend are the two shapes, for type hints. Raise thunc.TransientError for errors worth asking again.

Ollama

  • Ollama 0.13.3 and later serve the OpenAI Responses API the openai backend uses: set OPENAI_BASE_URL=http://localhost:11434/v1 and OPENAI_API_KEY=ollama, then configure(backend="openai", model="qwen3:8b").
  • live_tests/test_ollama.py checks 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 in tests/test_typing.py cover the new API under mypy and basedpyright.

Behavior changes

  • None for existing programs. thunc now registers a pytest plugin (the thunc_model and thunc_isolated fixtures), 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)

v0.4.0 (beta) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Oct 11:41
f1ef242

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

Record and replay (#8, #74)

# conftest.py
thunc.configure(recordings="tests/recordings")  # or THUNC_RECORDINGS=tests/recordings
THUNC_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.function and agent run is answered from the folder. One that wasn't recorded raises ThuncError saying 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/recordings lists 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 Literal option. A name sent in place of a value is asked again, not guessed.
  • An enum's values must be strings, numbers or booleans; Flag enums aren't supported. On jev, 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, awaited
  • thunc.amap keeps the input order, takes async def and 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), so thunc.call(..., returns=Literal["a", "b"]) is a Literal["a", "b"] to current mypy and Pyright, not Any. The same goes for unions, list[Item] and agent.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.py checks 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.map over an async def function returns its values instead of coroutines that were never awaited.
  • The trace has a "replayed" field, and the call.end and agent.start events have replayed. 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)

v0.3.2 (beta) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 10 Oct 10:51
d84b65f

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= or THUNC_TRACE naming 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 an OSError from the call. To make it an error again, turn the warning into one with warnings.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)

v0.3.1 (beta) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 15:00
b50eb16

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 watch and 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)

v0.3.0 (beta) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 14:47
f63e614

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 --profile tables, 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 ⏎. f shows only retries, failures and denials; p pauses 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-events and --replay keep a run and play it back, which is a good thing to attach to a bug report. --replay also plays one agent run's session record.
  • --plain prints 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 your PATH as thunc-watch.
  • Without it, thunc watch says how to install it.

THUNC_EVENTS

  • When THUNC_EVENTS names a file, thunc appends one JSON line to it per call start, model reply, call end, agent tool call and agent end; thunc watch sets it for the program it runs. To watch a program started some other way, set it yourself and run thunc watch --events FILE.
  • Inputs, replies and values are cut to 120-character previews; --capture (or THUNC_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 file
thunc: 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 watch command. CI runs the dashboard's cargo fmt, clippy and tests on Linux, macOS and Windows, and on Linux builds the PyPI wheel and runs thunc watch from 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.py on Codex wrote a function whose doctests pass.
  • watch/demo/demo_app.py runs 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_EVENTS and write= are new; nothing is written unless THUNC_EVENTS is set, and no file is edited unless a function has write=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)

Pre-release

Choose a tag to compare

@Eltarras Eltarras released this 07 Oct 13:46
0882205

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)

v0.2.3 (beta) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 06 Oct 07:32
8091ea3

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 thunc
import 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 ~/.codex config 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 with codex 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, and protocol="text" keeps the old way.

Durable runs (#56, #57)

  • On Claude Code, native calls in segments. One activity keeps one claude -p process 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 for resolve(), 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 for resolve() 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_tokens gets an error result for its calls instead of ending the run, pause_turn carries on, and a reply that sends nothing for timeout seconds 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.
  • finish in the same reply as other calls is refused (except beside remember): 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.

Better tools (#50, #51, #49)

  • edit takes replace_all, and edits for several changes to one file in one call, all made or none.
  • edit no longer needs a prior read: it only changes text the agent quotes exactly. write still replaces only a file read with read.
  • 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, codex or jev.
  • 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 codex make native tool calls by default, and fall back to the text protocol if they can't.
  • Durable runs on claude-code make native tool calls. Runs already in progress keep the text protocol.
  • Agents on the Claude API think at effort high on Claude 4.6 and later.
  • edit no longer needs a prior read; write still does.
  • finish in the same reply as other calls is refused, except beside remember.
  • 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)

v0.2.2 (beta) Pre-release
Pre-release

Choose a tag to compare

@Eltarras Eltarras released this 05 Oct 06:47
0181182

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 thunc
import 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.session
thunc run --profile my_script.py  # where the time went: model time, thunc's own, per tool

Agents on Claude Code make native tool calls (#46)

  • The agent's tools are an MCP server that one claude -p process 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 claude CLI 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, and protocol="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)

  • run takes cwd: a folder inside workdir to run the command in.
  • The shell permission (opt-in) runs command lines through the system shell, so pipes, &&, cd and redirects work. It can't be combined with !run: rules, since a shell command line can't be checked word by word.
  • search takes glob: *.py matches by file name, src/**/*.ts by path.
  • Git-aware list and search: in a git repository they leave out what git ignores, so a stale build/ 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 anthropic and openai backends 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 codex backend 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 benchmarks runs 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 claude that 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 in live_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-code make native tool calls by default, and fall back to the text protocol if they can't.
  • In a git repository, list and search leave 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-code backend loads none of your Claude Code settings (--setting-sources ""). No CLAUDE.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 pass follow=.
  • 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)

v0.2.1 (beta) Pre-release
Pre-release

Choose a tag to compare

@Eltarras Eltarras released this 04 Oct 13:18
f535a28

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, Runtime and Handle let 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_attention until an operator calls resolve() with complete, abort or retry (#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_task calls registered tasks from your own Temporal workflows. examples/temporal has 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 remember call that fails no longer appears in Run.notes.
  • Agents with tools= or timeout= can't be registered for durable runs yet; durable runs use deadline_seconds= instead.
  • Nothing changes for @thunc.function, thunc.call or local agent.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)

v0.2.0 (beta) Pre-release
Pre-release

Choose a tag to compare

@Eltarras Eltarras released this 04 Oct 12:12
9f39141

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 thunc
import 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 it

Agents

  • thunc.Agent and @agent.task: tasks are declared like @thunc.function, sync or async. @thunc.agent(name, workdir=...) declares a one-task agent in one go, and agent.call(...) runs a task built in code (#23, #34).
  • Permissions: by default an agent reads everything in workdir and writes nothing. Rules such as ["write:src/**", "run:pytest", "!read:.env*"] open up the write, edit and run tools; a deny always wins. Every path must stay inside workdir, 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:pytest runs 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 a thunc.Run with the value, files changed (by edits and by commands), commands, denials and steps. A failed run raises thunc.AgentError carrying the partial record (#29, #34).
  • Instruction files: follow=True gives the agent AGENTS.md / CLAUDE.md as 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_REVIEW and ANALYSIS for system= (#34).
  • Limits: max_steps=40 and timeout= (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.agent is now the @thunc.agent decorator. from thunc.agent import Agent still works.
  • Agents refuse the jev backend before a run starts; use Jev with @thunc.function.
  • Nothing changes for @thunc.function and thunc.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