Hooks

techrevati.runtime.hooks

Hooks — interceptor chain that mutates model, tool, and handoff boundaries.

Hooks differ from EventSink (which observes only): each hook may inspect and rewrite the data flowing through a turn, a tool call, or a handoff. The orchestrator runs hooks left-to-right around every run_turn / arun_turn, run_tool / arun_tool, and handoff call, so a later hook always sees the output of the previous one.

The two built-in transformations are:

  • before_model / before_tool — receive a mutable HookContext; mutate ctx.prompt (model) or ctx.args (tool) in place. Callers expose the mutable container via the prompt_ctx= / args_ctx= kwargs on the session methods; if not supplied, the orchestrator synthesizes a no-op context so hooks still fire.

  • after_model / after_tool — receive the model/tool result and return a (possibly new) replacement value. Returning None is taken literally — return the original value to leave it unchanged.

Hooks are intentionally caller-supplied, not "smart" defaults: the runtime ships three reference implementations (RedactPIIHook, LogModelIOHook, TokenBudgetCheckHook) and applications compose their own. Heavy/IO-bound work belongs in AsyncHook so it does not block the event loop.

This module is zero-dep. Built-ins use stdlib only.

HookContext dataclass

HookContext(
    role,
    phase,
    model="",
    prompt=None,
    tool="",
    args=dict(),
    extra=dict(),
)

Mutable context passed through the hook chain.

The same instance flows left-to-right through the chain; later hooks see earlier hooks' mutations. The orchestrator does not introspect any field except for logging — caller controls the shape of prompt and args.

Fields

role: the session role (AgentSession.role). phase: the session phase (AgentSession.phase). model: the model name passed to run_turn / arun_turn. Empty string for tool hooks. prompt: opaque caller-supplied model input. Hooks redact / log / mutate this in place; the caller's coro_factory closes over it so the model call sees the post-hook value. tool: the tool name for run_tool / arun_tool. Empty string for model hooks. args: caller-supplied tool input dict. Hooks may mutate keys. extra: free-form dict for caller-defined keys (e.g. correlation id, trace context).

Hook

Bases: Protocol

Sync interceptor.

Implementations override any subset of the five lifecycle methods — the dispatcher uses hasattr to skip unimplemented ones, so a hook that only cares about after_tool need not provide stubs.

The name attribute labels the hook in logs and events; default to the class name if not set explicitly.

AsyncHook

Bases: Protocol

Async sibling of Hook.

Async sessions dispatch both — sync hooks run inline, async hooks are awaited. Optional methods (override the ones you need):

  • async def abefore_model(self, ctx: HookContext) -> None
  • async def aafter_model(self, ctx: HookContext, result: Any) -> Any
  • async def abefore_tool(self, ctx: HookContext) -> None
  • async def aafter_tool(self, ctx: HookContext, result: Any) -> Any
  • async def abefore_handoff(self, ctx: HookContext) -> None

RedactPIIHook

RedactPIIHook(
    *,
    patterns=None,
    replacement="[REDACTED]",
    name="redact_pii",
)

Redact PII patterns from text-bearing ctx.prompt before model calls.

Operates on:

  • ctx.prompt when it is a str — replaces matches with replacement (default "[REDACTED]").
  • ctx.prompt when it is a dict — walks string leaves.
  • ctx.prompt when it is a list of message dictionaries — walks each message's content field.

Other shapes are skipped silently — caller can supply a custom hook when the schema is exotic. The hook is best-effort: it is a first-line defense, not a substitute for a dedicated PII scrubber (see docs/patterns/hooks.md for the discussion).

Also redacts after_model(result) when the result is a string, so log sinks downstream cannot leak content the upstream redactor caught coming in.

LogModelIOHook

LogModelIOHook(
    *,
    logger=None,
    level=logging.INFO,
    include_prompt=False,
    include_result=False,
    max_chars=4000,
    name="log_model_io",
)

Log model call metadata, with opt-in payload logging.

Defaults to logger.info at the techrevati.runtime.hooks logger; pass logger= to redirect. Prompt and result payloads are disabled by default so production deployments do not accidentally leak model inputs or outputs. Set include_prompt=True and/or include_result=True only after redaction and retention policy are in place.

The hook truncates payloads above max_chars (default 4000) and suffixes "…(truncated)" so a runaway 100k-token blob does not flood the log pipeline.

HookBudgetExceededError

HookBudgetExceededError(*, estimated, limit, model)

Bases: Exception

Raised by TokenBudgetCheckHook when an estimate exceeds the cap.

Catchable inside arun_turn so the recovery loop can react (e.g. smaller context budget). Use GovernancePlane with MaxBudgetLimit(on_breach="terminate") if you want a hard stop that bypasses recovery.

TokenBudgetCheckHook

TokenBudgetCheckHook(
    *,
    token_limit,
    estimator=None,
    name="token_budget_check",
)

Pre-flight token-budget guard.

Estimates input tokens for the configured model using a caller- supplied estimator (e.g. lambda p: len(str(p)) // 4 as a coarse fallback, or a real tokenizer). Raises HookBudgetExceededError if the estimate exceeds token_limit.

This is an alternative to UsageLimits for the case where the budget needs to be enforced before the model call rather than after the response comes back. The two compose: keep the pre-flight check tight (cheap, catches obvious mistakes) and let UsageLimits enforce the cumulative quota across many turns.