Per-tool budget reminders for Pydantic AI agents
Project description
pydantic-ai-tool-budget
Per-tool budget reminders for Pydantic AI agents.
Give your agent awareness of how many tool calls it has left — per tool, per shared pool, or globally — so it can plan ahead instead of crashing into a hard limit.
Why?
Pydantic AI's UsageLimits(tool_calls_limit=N) is a silent kill switch. The model has no idea it's running low on tool calls until the hard cap fires UsageLimitExceeded — often after it already did useful work that never gets returned.
pydantic-ai-tool-budget fixes this by injecting budget reminders directly into the conversation after each tool result. The model sees exactly how many calls remain and can wrap up gracefully.
search: 3/5 calls used, 2 remaining.
See pydantic/pydantic-ai#4359 for the upstream discussion.
Install
uv add pydantic-ai-tool-budget
# or
pip install pydantic-ai-tool-budget
Requires pydantic-ai-slim>=0.100.0.
Quick Start
from pydantic_ai import Agent
from pydantic_ai_tool_budget import budgeted
def search(query: str) -> str:
"""Search the web."""
return f"Results for {query}"
def lookup(city: str) -> str:
"""Look up city info."""
return f"Info about {city}"
agent = Agent(
"openai:gpt-4o",
tools=[
budgeted(search, limit=5),
budgeted(lookup, limit=3),
# undecorated tools work normally — no budget tracking
],
)
After each call, the model sees a reminder like:
search: 3/5 calls used, 2 remaining.
When the budget runs out:
search: 5/5 calls used, 0 remaining. This tool's budget is exhausted.
Use Cases
Only remind when budget is tight
Don't clutter the context when there's plenty of budget left. Use threshold to only inject reminders when remaining calls drop below a value:
budgeted(search, limit=10, threshold=3) # silent until ≤ 3 calls remain
Shared budget across tools
Multiple tools can draw from the same pool using ToolBudget. This is useful when you don't care which tools the agent calls, just that total tool usage stays within bounds:
from pydantic_ai_tool_budget import ToolBudget, budgeted
pool = ToolBudget(limit=20)
agent = Agent(
"openai:gpt-4o",
tools=[
budgeted(search_signals, budget=pool),
budgeted(get_signal_details, budget=pool),
budgeted(analyze_competitor, budget=pool),
],
)
All three tools share the same 20-call budget. The model sees the shared remaining count after every call.
Exempt tools that shouldn't count
Some tools — like a final "save" or "submit" action — should always be available but still show the shared budget status. Mark them exempt:
pool = ToolBudget(limit=20)
agent = Agent(
"openai:gpt-4o",
tools=[
budgeted(search_signals, budget=pool),
budgeted(get_signal_details, budget=pool),
# exempt: doesn't count against the pool, but still shows remaining
budgeted(register_opportunity, budget=pool, exempt=True),
],
)
register_opportunity never decrements the shared counter, but the model still sees "X/20 calls remaining" after calling it.
Custom behavior when budget is exhausted
Instead of letting the model call a tool that can't do anything useful, intercept it with on_exhaust:
budgeted(
search,
limit=5,
on_exhaust=lambda name, used, limit: (
f"Budget for {name} is exhausted. Summarize what you have."
),
)
When the budget hits zero, on_exhaust is called instead of the real tool function. The model gets your message as the tool result and can act on it. If on_exhaust returns a ToolReturn, it's used as-is; otherwise, the standard budget reminder is appended.
Custom reminder format
Override the default reminder text entirely:
budgeted(
search,
limit=5,
formatter=lambda name, used, limit: (
f"⚠️ Only {limit - used} calls left for {name}. Prioritize."
if limit - used <= 3
else None # suppress when there's plenty of budget
),
)
Return None from the formatter to suppress the reminder for that call.
How It Works
budgeted() wraps your tool function using functools.wraps, preserving its name, docstring, and parameter schema. After each call, it returns a ToolReturn with a .content field containing the budget reminder. Pydantic AI surfaces this as a UserPromptPart placed after the tool result in the conversation — exactly where the model reads it before deciding what to do next.
This means:
- Reminders sit in the conversation body, not the system prompt — no prompt cache busting
- Each tool gets its own budget counter (or shares one via
ToolBudget) - No string mappings — you pass the function directly, so typos are
NameErrors - Works with sync and async tools, with or without
RunContext
API Reference
budgeted(func, *, limit, budget, exempt, threshold, formatter, on_exhaust)
| Parameter | Type | Default | Description |
|---|---|---|---|
func |
Callable |
required | The tool function to wrap |
limit |
int | None |
None |
Per-tool call limit. Mutually exclusive with budget |
budget |
ToolBudget | None |
None |
Shared budget pool. Mutually exclusive with limit |
exempt |
bool |
False |
Don't count against shared budget, but still show reminders. Only valid with budget |
threshold |
int | None |
None |
Only remind when remaining ≤ threshold |
formatter |
(name, used, limit) → str | None |
None |
Custom reminder text. Return None to suppress |
on_exhaust |
(name, used, limit) → Any |
None |
Called instead of the tool when budget is exhausted |
ToolBudget(limit)
A shared call-count pool. Pass to budgeted(..., budget=pool) so multiple tools draw from the same budget.
| Property / Method | Description |
|---|---|
used |
Number of calls made so far |
remaining |
Calls left before exhaustion |
is_exhausted() |
Whether the budget is fully consumed |
reset() |
Reset the counter to zero |
License
MIT
Releasing
GitHub Actions: easiest path
Run the Release workflow from the Actions tab.
Or trigger it from the CLI:
gh workflow run Release --ref main -f bump=patch
- Choose
patch,minor,major, orcustom - If you choose
custom, provide an explicitX.Y.Zversion
For an explicit version:
gh workflow run Release --ref main -f bump=custom -f version=0.2.0
The workflow will:
- bump
pyproject.toml - run lint, type checks, and tests
- commit the version bump to
main - create and push the git tag
- build and publish to PyPI
- create the GitHub release with generated notes
Local CLI
Preview the next version:
uv run python scripts/release.py patch
Write the next patch version into pyproject.toml:
uv run python scripts/release.py patch --write
Set an exact version:
uv run python scripts/release.py custom --version 0.2.0 --write
Safety rails
- The publish workflow checks that the git tag matches
pyproject.toml - PyPI uploads use
skip-existing: true, so reruns do not fail just because a version is already published
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_ai_tool_budget-0.1.1.tar.gz.
File metadata
- Download URL: pydantic_ai_tool_budget-0.1.1.tar.gz
- Upload date:
- Size: 19.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38ea5e43d340a04d14d09cdbc534969790ea00880895b41d3062154cdd489c14
|
|
| MD5 |
108d7c10b63d2e67b6feb531c5ca405c
|
|
| BLAKE2b-256 |
305307adbb1e317068c4f599589a899bcc7c9ca276a00ef35bdfb8b88cde1883
|
Provenance
The following attestation bundles were made for pydantic_ai_tool_budget-0.1.1.tar.gz:
Publisher:
publish.yml on sarth6/pydantic-ai-tool-budget
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_ai_tool_budget-0.1.1.tar.gz -
Subject digest:
38ea5e43d340a04d14d09cdbc534969790ea00880895b41d3062154cdd489c14 - Sigstore transparency entry: 1096873336
- Sigstore integration time:
-
Permalink:
sarth6/pydantic-ai-tool-budget@8034b5eeecf52deca7a8bd333a8ff4306f261020 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sarth6
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8034b5eeecf52deca7a8bd333a8ff4306f261020 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file pydantic_ai_tool_budget-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pydantic_ai_tool_budget-0.1.1-py3-none-any.whl
- Upload date:
- Size: 8.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aae59042dc6aa993c511a093b17d2f786de889e3b8efab7a8b598fd240869256
|
|
| MD5 |
6883d05b581c4f3dd52a4975065e05c0
|
|
| BLAKE2b-256 |
a342d1c58fd9b9bb077c0055771eec9abf42367b35e9476929918ddd2c275c7e
|
Provenance
The following attestation bundles were made for pydantic_ai_tool_budget-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on sarth6/pydantic-ai-tool-budget
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_ai_tool_budget-0.1.1-py3-none-any.whl -
Subject digest:
aae59042dc6aa993c511a093b17d2f786de889e3b8efab7a8b598fd240869256 - Sigstore transparency entry: 1096873338
- Sigstore integration time:
-
Permalink:
sarth6/pydantic-ai-tool-budget@8034b5eeecf52deca7a8bd333a8ff4306f261020 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sarth6
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8034b5eeecf52deca7a8bd333a8ff4306f261020 -
Trigger Event:
workflow_dispatch
-
Statement type: