Skip to content

Hooks

Hooks around tool, model and node calls, the built-in hooks and content guards. Concepts: Hooks, Guardrails, Team budgets.

agent_adapter.hooks

Hooks: code that runs before, after and on errors of tool, LLM and flow-node calls.

DEFAULT_REDACT = ('key', 'token', 'secret', 'password', 'authorization', 'cookie') module-attribute

CallableGuard

Bases: ContentGuard

Wraps fn(stage, payload): True passes; False, a reason or a GuardVerdict says no.

ContentGuard

Bases: Hook

Base guard: inspect(stage, payload) decides; action blocks, redacts or asks.

confirm(call, ctx) async

A ToolSpec.confirm rule: True (ask for approval) when the arguments fail the guard.

inspect(stage, payload) async

The verdict on one payload; subclasses implement it.

GuardVerdict dataclass

A guard's answer: allowed, or a reason and (to redact) the rewritten payload.

kinds labels what was found (email, card) and count how many: they go into the guardrail.redacted event, so they must never hold the found values.

LLMInvocation dataclass

A model call as hooks see it.

NemoGuard

Bases: TextGuard

NeMo Guardrails as a guard: pass rails (an LLMRails) or config (its folder).

review_as(text, role) async

The rails' verdict on text sent as a role turn.

NodeInvocation dataclass

A flow node run as hooks see it.

PIIGuard

Bases: TextGuard

Blocks, redacts or approves payloads holding personal data or secrets (regexes only).

PresidioGuard

Bases: TextGuard

Presidio as a guard; pass your own analyzer and anonymizer engines or get defaults.

Rate dataclass

At most calls calls per per seconds, with bursts of up to calls.

ShortCircuit dataclass

Returned by a before_* / on_*_error hook: skip the call and use value.

ToolInvocation dataclass

A tool call as hooks see it; call.arguments are validated, LLM-facing, JSON mode.

hook_name(hook)

The hook's name, or its class name.

redact(value, patterns=DEFAULT_REDACT, *, match=None)

A copy of value whose secret mapping keys are ***.

By default a key is secret when it contains any pattern (case-insensitive); match, e.g. is_secret_name, decides instead.

run_after(hooks, method, inv, value) async

Thread value through each hook; a None return keeps the current value.

run_before(hooks, method, inv) async

Call method on each hook; the first ShortCircuit wins and stops the phase.

run_on_error(hooks, method, inv, error) async

Offer error to each hook; the first ShortCircuit recovers and stops the phase.

Also exported here, documented under the canonical path: BudgetHook, GuardrailHook, Hook, LoggingHook, RateLimitHook.

agent_adapter.hooks.builtin

Built-in hooks: logging, budget, guardrails and rate limits.

BudgetScope = Literal['run', 'thread', 'global', 'principal', 'user', 'team'] module-attribute

GuardrailCheck = Callable[[str, Any], bool | str | Awaitable[bool | str]] module-attribute

OnExceed = Literal['warn', 'stop'] module-attribute

REDACTED = '***' module-attribute

Stage = Literal['args', 'result', 'llm_request', 'llm_response'] module-attribute

Also exported here, documented under the canonical path: BudgetHook, DEFAULT_REDACT, GuardrailHook, LoggingHook, Rate, RateLimitHook, redact.

agent_adapter.hooks.builtin.budgets

Budget counters shared across processes: the BudgetStore port and its backends.

PERIODS = ('day', 'month') module-attribute

Per = Literal['day', 'month'] module-attribute

BudgetStatus dataclass

One budget as a caller sees it: limit, spend, live holds and when it resets.

remaining_usd property

What is left before the limit, never below 0.

view()

A JSON-ready dict, remaining_usd included.

BudgetStore

Bases: Protocol

Spend counters shared by every hook that uses the store; reserve is atomic.

A hold (reserve) counts against the limit until it is settled with the real cost, released, or hold_s passes (a crashed caller's hold frees itself). expire_at (epoch seconds) is when the whole counter may be dropped, its window having ended; None leaves the counter's current expiry as it is. Backend errors propagate, so a budget check fails closed.

BudgetUsage dataclass

A counter's settled spend and its live holds (USD).

committed_usd property

Spent plus held: what a new reservation is checked against.

MemoryBudgetStore

A BudgetStore in this process (threads and event loops share it), the default.

Keeps at most max_keys live counters; ended windows are dropped first, and a new key past that is refused (fails closed) rather than forgetting another key's spend.

Raises:

Type Description
ConfigError

max_keys is not a positive int.

release(key, reservation_id) async

Drop the hold; nothing was spent.

reserve(key, reservation_id, amount_usd, *, limit_usd, hold_s, expire_at) async

Hold amount_usd when it fits limit_usd (None: always); one atomic step.

settle(key, reservation_id, cost_usd, *, expire_at) async

Drop the hold and add the real cost (even when the hold already expired).

usage(key) async

The counter's spend and live holds; zero for an unknown or expired key.

RedisBudgetStore

A BudgetStore on Redis (agent-adapter[redis]), shared by every worker and host.

client_factory() returns a redis.asyncio.Redis (one per event loop). Each operation is one Lua script (Redis 5 or later), so the check and the hold are atomic across processes; hold expiry uses the Redis clock. A windowed counter expires with its window. A counter's three keys share a hash tag (one Cluster slot). Backend errors propagate.

Raises:

Type Description
ConfigError

client_factory is not callable, or prefix does not end in ":".

keys_of(key)

The counter, holds and expiry keys of one budget counter.

release(key, reservation_id) async

Drop the hold; nothing was spent.

reserve(key, reservation_id, amount_usd, *, limit_usd, hold_s, expire_at) async

Hold amount_usd when it fits limit_usd (None: always), in one script.

settle(key, reservation_id, cost_usd, *, expire_at) async

Drop the hold and add the real cost (even when the hold already expired).

usage(key) async

The counter's spend and live holds; zero for an unknown or ended key.

Reserved dataclass

The outcome of reserve: accepted or not, and the usage it was checked against.

SQLAlchemyBudgetStore

A BudgetStore on a SQLAlchemy async engine (agent-adapter[sqlalchemy]).

engine is a URL ("postgresql+asyncpg://...") or a callable returning an AsyncEngine; one engine is made per event loop. reserve locks the counter row with an UPDATE (READ COMMITTED on Postgres; SQLite's write lock) before it checks and holds, so workers never hold past the limit together. metadata holds both tables.

Raises:

Type Description
ConfigError

a bad engine or table prefix; on first use, a dialect other than Postgres or SQLite.

MissingDependency

SQLAlchemy is not installed.

aclose() async

Dispose of the running loop's engine.

release(key, reservation_id) async

Drop the hold; nothing was spent.

reserve(key, reservation_id, amount_usd, *, limit_usd, hold_s, expire_at) async

Hold amount_usd when it fits limit_usd (None: always), under a row lock.

settle(key, reservation_id, cost_usd, *, expire_at) async

Drop the hold and add the real cost (even when the hold already expired).

usage(key) async

The counter's spend and live holds; zero for an unknown or ended key.

SQLiteBudgetStore

A BudgetStore in a SQLite file (WAL), safe across threads, loops and processes.

Each call runs in a worker thread on its own connection. reserve and settle hold BEGIN IMMEDIATE, so the check and the hold are one step for every process; timeout is how long one waits for the file lock (seconds). Ended windows and expired holds are deleted as new reservations arrive.

Raises:

Type Description
ConfigError

a bad path or timeout (here); on first use, a database without WAL support or with another schema version.

release(key, reservation_id) async

Drop the hold; nothing was spent.

reserve(key, reservation_id, amount_usd, *, limit_usd, hold_s, expire_at) async

Hold amount_usd when it fits limit_usd (None: always), under the file lock.

settle(key, reservation_id, cost_usd, *, expire_at) async

Drop the hold and add the real cost (even when the hold already expired).

usage(key) async

The counter's spend and live holds; zero for an unknown or ended key.

Window dataclass

One UTC budget window: label names it in keys; end is when it resets (epoch s).

principal_of(ctx)

Who a call is charged to: the authenticated principal's id, else ctx.user_id.

window_at(per, now)

The window holding now: a UTC day, a UTC month, or all time for per=None.

agent_adapter.hooks.builtin.guards

Guard hooks: built-in PII and secret checks, any guard service, Presidio and NeMo Guardrails.

Action = Literal['block', 'redact', 'approve'] module-attribute

GuardFn = Callable[[str, Any], GuardResult | Awaitable[GuardResult]] module-attribute

NO_REPLACEMENT = _Unset() module-attribute

PII_KINDS = ('email', 'phone', 'card', 'iban', 'secret') module-attribute

PIIMatch dataclass

One finding: its kind and where it sits in the text.

TextGuard

Bases: ContentGuard

A guard that reviews each text in a payload; redaction swaps each text for its rewrite.

review(text, stage) async

The verdict on one text of stage; replacement is its redacted text.

review_field(keys, text)

A verdict from the keys above an allowed text (password, auth), or None.

as_verdict(answer)

A guard function's answer as a GuardVerdict.

find_pii(text, kinds=PII_KINDS, *, patterns=None)

Every finding in text of the given kinds (plus patterns), in order, not overlapping.

iban_ok(text)

True when an IBAN-like id passes its mod-97 check.

luhn_ok(digits)

True when a digit string passes the Luhn checksum used by payment cards.

redact_pii(text, matches, placeholder='[{kind}]')

text with each match replaced by placeholder ({kind} is the upper-case kind).

Also exported here, documented under the canonical path: CallableGuard, ContentGuard, GuardVerdict, NemoGuard, PIIGuard, PresidioGuard.