budgetloop
The tool-calling loop, written out: schemas from type hints, parallel calls, budgets and a trace. No dependencies.
Every agent framework hides the same forty lines. This is those lines, with the parts that actually bite — a tool that raises, arguments that nearly fit, a model that never stops asking — handled where you can see them.
The shape of it
from budgetloop import Budget, tool, run
@tool
def price(isin: str) -> float:
"""Look up the last price for an instrument.
Args:
isin: the twelve-character identifier
"""
return lookup(isin)
result = run(
complete, # your model, see below
"What is SE0000108656 worth?",
tools=[price],
budget=Budget(max_steps=6, max_cost_usd=0.05),
)
print(result.answer, result.stop, result.cost_usd)
The model
One callable, provider-agnostic:
def complete(messages: list[Message], tools: list[dict]) -> Completion:
...
It gets the conversation and the tool schemas, and returns either text or tool calls. Adapting an SDK to it is a dozen lines, and in return the whole loop is testable with a scripted model and no network. Every test in this repository runs offline.
Run the example
python examples/offline.py
step 0: Looking both of them up.
- price({"isin": "SE0000108656"}) -> 118.4
x price({"isin": "SE0000000000"}) -> error: LookupError: SE0000000000 is not in the price list
step 1: One of those is unknown. Converting the one that resolved.
- convert({"amount": "118.4", "rate": 0.091}) -> 10.77
step 2: SE0000108656 is 118.40 SEK, about 10.77 USD.
stop=answered tools=3 failed=1 cost=$0.0027
Three things are visible in that output, and each one is a decision.
A tool that raises becomes a result, not a crash. The second lookup failed, the model read the failure and worked around it. A loop that propagates the exception throws away a model that was about to recover on its own.
Arguments are checked, and corrected once where the intent is not in doubt. The
model sent "118.4" as a string. A number written as a string is the mistake models
make constantly, so it is coerced; "five" is not, and comes back as
error: convert.amount: expected number, got str. Unknown argument, missing argument,
value outside an enum: same treatment, reported to the model in words it can act on.
Both calls in step 0 ran at the same time. When a model asks for several tools in one turn it means they are independent, so they are run on a pool and the results go back in the order they were asked for.
Schemas come from the signature
@tool builds the JSON Schema from the type hints and the docstring. Writing the
schema by hand next to the function is how the two drift apart, and a drifted schema
shows up as a model calling the tool wrongly for reasons nobody can see.
Supported: str, int, float, bool, list[T], dict[str, T], Literal[...],
unions with None, Any, and defaults. A parameter without a type hint is an error at
import time, not a surprise at runtime. Args: lines in the docstring become parameter
descriptions, which is most of what makes a model call a tool correctly.
Budgets
Budget(max_steps=10, max_seconds=30, max_cost_usd=0.50, max_tool_calls=25)
Only max_steps has a default, because a loop without a ceiling is a loop that bills
you for a model's bad afternoon. A run that hits a limit ends with result.stop set to
which one, and result.answered is False. Nothing raises.
The trace
result.to_dict() is plain data: every step, what the model said, what each tool was
called with, what came back, how long it took and what it cost. Store it next to the
request and a bad answer stops being a mystery.
Tests
pytest -q
ruff check .
65 tests, no network, no API key. MIT licensed.
Metadata
Release files for budgetloop 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| budgetloop-0.1.0.tar.gz | 17.3 kB | With provenance | Details for budgetloop-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| budgetloop-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for budgetloop-0.1.0-py3-none-any.whl |
Total release size: 31.2 kB
Release files / budgetloop-0.1.0.tar.gz
| Download URL | budgetloop-0.1.0.tar.gz |
|---|---|
| Size | 17.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7d7cc09413915d94fc6ef714e08c99531984c089fb3a9d2889c640b8936aa7dd
|
|
BLAKE2b-256 checksum How to use checksums |
27e90555fb93cf7e63d205af10bd05134d36c9e3ad078923a12424e9e032abb5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / budgetloop-0.1.0-py3-none-any.whl
| Download URL | budgetloop-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
33840d214e0a8469c41ef87c855b93bb36714252f4f09dfb29153608fb027d1c
|
|
BLAKE2b-256 checksum How to use checksums |
4387fd0a65ad697dc244069097456b41b684d599ff82ea4a16f9d4f9ff9b7dfe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for budgetloop 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for budgetloop-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.