Skip to content

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