Repository navigation
v0.4.0 (beta)
Pre-release
Pre-release
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