runtime_hook_recipes.md
July 3, 2026 ยท View on GitHub
Runtime Hook Recipes
These recipes show common production uses for LightAgent(..., hooks=[...]).
Each hook can return None, HookDecision.continue_(),
HookDecision.replace(payload), or HookDecision.block(reason).
PII Redaction Before Model Calls
import re
from LightAgent import HookDecision
def redact_pii(ctx):
if ctx.phase != "before_model_request":
return None
params = dict(ctx.payload["params"])
messages = list(params["messages"])
redacted = []
for message in messages:
content = message.get("content", "")
content = re.sub(r"[\w.+-]+@[\w.-]+", "[redacted-email]", str(content))
redacted.append({**message, "content": content})
params["messages"] = redacted
return HookDecision.replace({"params": params})
Cost Budget Enforcement
from LightAgent import HookDecision
def token_budget(max_messages=12):
def hook(ctx):
if ctx.phase != "before_model_request":
return None
message_count = len(ctx.payload["params"].get("messages", []))
if message_count > max_messages:
return HookDecision.block("message budget exceeded")
return None
return hook
Model Routing
from LightAgent import HookDecision
def route_small_tasks(ctx):
if ctx.phase != "before_model_request":
return None
params = dict(ctx.payload["params"])
user_text = params["messages"][-1]["content"]
if len(user_text) < 120:
params["model"] = "gpt-4o-mini"
return HookDecision.replace({"params": params})
Tool Allow/Deny Audit
from LightAgent import HookDecision
def tool_policy(allowed_tools):
allowed = set(allowed_tools)
def hook(ctx):
if ctx.phase != "before_tool_call":
return None
tool_name = ctx.payload["tool_name"]
if tool_name not in allowed:
return HookDecision.block(f"tool `{tool_name}` is not allowed")
return HookDecision.continue_(metadata={"audited_tool": tool_name})
return hook
Memory Retrieve Filtering
from LightAgent import HookDecision
def filter_low_confidence_memory(ctx):
if ctx.phase != "after_memory_retrieve":
return None
memories = dict(ctx.payload["memories"])
memories["results"] = [
item for item in memories.get("results", [])
if item.get("metadata", {}).get("confidence", 1.0) >= 0.7
]
return HookDecision.replace({**ctx.payload, "memories": memories})
Langfuse Or OpenTelemetry Export
def export_hook(exporter):
def hook(ctx):
if ctx.phase in {"after_run", "on_error", "after_tool_result"}:
exporter.record({
"phase": ctx.phase,
"trace_id": ctx.trace_id,
"run_id": ctx.run_id,
"agent_name": ctx.agent_name,
"payload": ctx.payload,
})
return None
return hook
Evaluation Sampling After Run
import random
def sample_for_eval(queue, rate=0.05):
def hook(ctx):
if ctx.phase != "after_run" or not ctx.payload.get("success"):
return None
if random.random() <= rate:
queue.append({
"trace_id": ctx.trace_id,
"content": ctx.payload.get("content"),
"metadata": ctx.metadata,
})
return None
return hook
Async Hook
from LightAgent import HookDecision
async def async_policy(ctx):
if ctx.phase != "before_model_request":
return None
allowed = await check_external_policy(ctx.user_id, ctx.payload["params"])
if not allowed:
return HookDecision.block("external policy rejected request")
return None
HookManager runs async hooks automatically, including when the caller is
already inside an event loop.