Skip to main content
The Python SDK brings the same enforcement model as the Rust SDK to Python, @tool, @rule, and @agent decorators that enforce limits per function call.

Passthrough mode

When running outside nanny run, every decorator is a no-op. The function executes normally with no enforcement overhead:

@tool — declare a governed tool

Mark a function as a tool that Nanny should track and charge against the budget:
When the agent calls fetch_page:
  1. Nanny checks: is fetch_page in the [tools] allowed list?
  2. Nanny checks: has fetch_page exceeded [tools.fetch_page] max_calls?
  3. Nanny charges 10 tokens against the budget.
  4. If any check fails, a NannyStop exception is raised — the function body never runs.
Works identically for async functions:

Tokens

The tokens argument is required. Set it to 0 for tools you want tracked but not charged:

Matching the tool allowlist

The tool name used for allowlist checks is the function name as declared in Python:
metrics_crew — ToolDenied fires when the analysis agent calls write_report, a tool outside its allowlist

instrument — automatic LLM token tracking

Call nanny_sdk.instrument(client) once at agent startup to automatically report LLM token usage to Nanny’s budget. Every completion response is intercepted and its token counts are debited from the same ledger that @tool charges against.
Supported clients (detected by duck-typing — no provider package is imported):
  • OpenAI, Groq, Together AI, Azure OpenAI, LiteLLM — any client with a chat.completions.create method
  • Anthropicclient.messages.create
  • Mistralclient.chat.complete
  • Google Gemini (google-genai SDK) — client.models.generate_content
  • Cohere v2cohere.ClientV2
instrument returns the same client unchanged — it patches the method in-place. In passthrough mode (outside nanny run), it is a no-op: no wrapping, no overhead. For providers that report prompt-caching usage (OpenAI, Anthropic, DeepSeek, Gemini), instrument also captures it and reports it as cache_read/cache_write — a finer split of input, never additional tokens beyond it, and never used for enforcement (Nanny still debits input + output exactly as before). Every provider names and shapes this data differently, so instrument normalizes each one’s own vocabulary into these two generic fields; absent, not zero, for a provider/response that doesn’t report cache usage at all. Exists purely so a downstream cost calculator can price cache-hit tokens at their real, much cheaper rate — see LlmUsageRecorded for the full field reference.

@rule — declare an enforcement rule

A rule is a function that returns a verdict on whether execution should continue. Return True to allow, False to deny:
Rules are evaluated client-side on every tool call, before Nanny’s enforcement layer is contacted. When a rule returns False, Nanny raises:
The denied tool never runs and no tokens are charged.

PolicyContext fields

The ctx parameter gives you a snapshot of the current execution state: Rules are evaluated before the tool runs — requested_tool is set to the tool name being checked. Use last_tool_args for content-based enforcement:
All fields are live. Before each rule evaluation, the SDK fetches current counters from Nanny’s enforcement layer — step_count, tokens_spent, tool_call_counts, and tool_call_history reflect the actual execution state at the moment your rule runs.
metrics_crew — RuleDenied fires on the loop-detection rule, stopping the analysis agent from repeating the same computation

@agent — activate named limits for a scope

In a multi-agent system, each agent has a different role and a different risk profile. The analysis agent makes expensive API calls and deserves a tight token ceiling. The reporter just writes a file and barely needs a budget at all. @agent activates the right named limit set when each role runs, then reverts automatically when it’s done.
The named set inherits from [limits] and overrides only the declared fields. Works identically for async functions. Limits revert on exit whether the function returns normally or raises.
metrics_crew running under nanny run — ingestion, analysis, visualization, and reporter agent scopes entering and exiting with structured NDJSON events

fresh_run — starting a fresh run mid-process

@agent changes which ceiling the run’s one running total is checked against; it does not give a role its own budget (see Named sets share one counter). If your process genuinely runs multiple, independent phases back to back, say, a research phase that hands off to a drafting phase, or a long-lived server that should give each incoming request its own clean slate, and you want each one to start from zero rather than inherit whatever an earlier phase already spent, that’s a new run, Nanny’s real unit of governance: one cumulative counter, one stop state, “a stop is final.”
Only meaningful when governed through a network server (nanny run --serve / --join): the server keys independent state per run, so a stop in the run you just left has zero effect on the one you’re starting. Under local nanny run (no --serve), one process is already always exactly one run, so this is a safe no-op there, code that might run under either mode doesn’t need to branch on which one it’s in.
Before this existed, the only way to do this was setting the NANNY_RUN_ID environment variable directly, an internal detail the client happens to read fresh on every call, never documented as something to rely on. fresh_run() is that same mechanism, given a real, discoverable name.

What happens on stop

When Nanny stops execution, it raises a NannyStop exception. All stop reasons are distinct subclasses:
Catch them by category or individually:
BridgeUnavailable is raised when Nanny’s enforcement layer is active but unreachable during rule evaluation or a tool call. Nanny fails closed — the agent does not continue ungoverned. Like all NannyStop subclasses it extends BaseException, so it propagates through broad except Exception handlers in agent frameworks without being swallowed. Under plain nanny run (single process, no --serve), you do not need to handle stop reasons in most agent code. They propagate up the call stack and terminate the process, since nanny run is watching that process and exits it for you. Catching them there is useful mainly in test code and at the CLI entry point. This is not true once you’re running under nanny run --serve / --join. When your process joins a governance server, the wrapping nanny run --join command does not watch your process for stops at all: no polling loop, no kill on budget exhaustion, nothing. That’s deliberate. The stop guarantee is that a stop ends one run, not your whole process, so a long-lived server (a web app, a Discord bot, anything handling more than one request per process) has to keep running after another request’s run gets stopped. Nothing does that for you. If a request handler lets NannyStop propagate uncaught, your framework’s own crash behavior decides what happens next, which for many servers means the whole process going down over one governed request. Catch it at the boundary of whatever “one governed unit of work” means for your app, typically each request handler, and turn it into a failure response for that one request, not an unhandled exception:
See Long-lived processes and NannyStop for the full explanation of why this only applies under --serve/--join.
metrics_crew — BudgetExhausted stops the analysis agent when it hits its token ceiling

Complete example

Run it under Nanny:
Run it without Nanny (decorators silent, agent runs normally):

Multi-agent pattern

The canonical use case: a pipeline where each agent has a specific role, a specific budget, and access to only the tools it needs. This is the metrics_crew pattern — four specialized agents, each governed independently.
The key properties this gives you:
  • A ceiling tailored to each role: the analysis agent gets a tighter check than the reporter while it’s running, expressed once in nanny.toml. Worth being precise about what this is not: tokens spent is one running total for the whole run, not a separate pool per role, so if analysis exhausts its own ceiling, the run stops there, the reporter never gets to run at all. See Named sets share one counter for the full explanation, and fresh_run() below if you actually want each phase to have its own independent budget.
  • Least-privilege tool access: each agent only receives the tools it needs; calling outside its role raises ToolDenied immediately
  • Loop detection: the @rule fires client-side before Nanny’s enforcement layer is contacted — the denied tool never runs and no tokens are charged
  • Full audit trail: every tool call, every limit activation, every stop reason logged to NDJSON
metrics_crew — MaxStepsReached stops the analysis agent after it exhausts its step allowance
For cross-process and cross-machine enforcement, use the governance server. See examples/python/metrics_crew for the complete working implementation of this pattern with four agents, Plotly chart generation, and a full incident report output.

Framework integration

LangChain

Stack @lc_tool (outer) and @nanny_tool (inner). LangChain registers the function for dispatch; Nanny intercepts every call regardless of which model or API style invoked it:
Execution order: your code calls tool.run(args) → LangChain validates args → Nanny wrapper intercepts → enforcement check → if allowed, file is read.

CrewAI

Same stacking pattern. CrewAI’s @tool decorator and Nanny’s @tool decorator both wrap the function — Nanny’s wrapper fires on every tool.run() call inside the crew:
See examples/python/dev_assist for a complete LangGraph integration and examples/python/metrics_crew for the canonical multi-agent governance example with four specialized agents, per-role limits, and per-role tool allowlists.