Callbacks¶
adk-fluent provides a fluent API for attaching callbacks to agents. All callback methods are additive – multiple calls accumulate handlers, never replace.
Tip
Visual learner? Open the Module Lifecycle Interactive Reference{target=”_blank”} for a swim-lane timeline showing exactly when each callback fires during agent execution.
Callback Methods¶
Method |
Alias for |
Description |
|---|---|---|
|
|
Runs before each LLM call. Receives |
|
|
Runs after each LLM call. Receives |
|
|
Runs before agent execution |
|
|
Runs after agent execution |
|
|
Runs before each tool call |
|
|
Runs after each tool call |
|
|
Handles LLM errors |
|
|
Handles tool errors |
Additive Semantics¶
Each call appends to the list of handlers for that callback type. This is different from native ADK where setting a callback replaces the previous one:
from adk_fluent import Agent
def log_fn(ctx, req):
print(f"Request: {req}")
def metrics_fn(ctx, req):
print(f"Metrics: {req}")
# Both handlers run before every LLM call
agent = (
Agent("service", "gemini-2.5-flash")
.instruct("Handle requests.")
.before_model(log_fn)
.before_model(metrics_fn)
.build()
)
Conditional Callbacks¶
Conditional variants append only when the condition is true:
debug_mode = True
audit_enabled = False
agent = (
Agent("service", "gemini-2.5-flash")
.instruct("Handle requests.")
.before_model_if(debug_mode, log_fn) # Added (debug_mode is True)
.after_model_if(audit_enabled, audit_fn) # Skipped (audit_enabled is False)
.build()
)
This is useful for toggling callbacks based on environment variables or feature flags without cluttering your code with if-else blocks.
Guards¶
.guard(fn) is a shorthand that registers the function as both before_model and after_model:
def safety_check(ctx, data):
# Runs both before and after model calls
if "dangerous" in str(data):
raise ValueError("Safety violation detected")
agent = (
Agent("service", "gemini-2.5-flash")
.instruct("Handle requests.")
.guard(safety_check)
.build()
)
Middleware Stacks with .apply()¶
For agents that need multiple layers of callbacks, use Presets to bundle them into reusable middleware stacks:
from adk_fluent.presets import Preset
# Define reusable middleware
logging_preset = Preset(before_model=log_fn, after_model=log_response_fn)
security_preset = Preset(before_model=safety_check, after_model=audit_fn)
# Apply multiple presets
agent = (
Agent("service", "gemini-2.5-flash")
.instruct("Handle requests.")
.use(logging_preset)
.use(security_preset)
.build()
)
See Presets for more on reusable configuration bundles.
Error Handling¶
Error callbacks handle failures in LLM calls and tool executions:
def handle_model_error(ctx, error):
print(f"Model error: {error}")
# Optionally return a fallback response
def handle_tool_error(ctx, error):
print(f"Tool error: {error}")
# Optionally return a fallback result
agent = (
Agent("service", "gemini-2.5-flash")
.instruct("Handle requests.")
.on_model_error(handle_model_error)
.on_tool_error(handle_tool_error)
.build()
)
Complete Example¶
from adk_fluent import Agent
def log_request(ctx, req):
print(f"[LOG] Model request at {ctx.agent_name}")
def log_response(ctx, resp):
print(f"[LOG] Model response at {ctx.agent_name}")
def validate_output(ctx, resp):
if not resp:
raise ValueError("Empty response")
def audit_tool(ctx, result):
print(f"[AUDIT] Tool result: {result}")
agent = (
Agent("production_agent", "gemini-2.5-flash")
.instruct("You are a production service.")
.before_model(log_request)
.after_model(log_response)
.after_model(validate_output)
.before_tool(lambda ctx, tool: print(f"Calling tool: {tool}"))
.after_tool(audit_tool)
.on_model_error(lambda ctx, e: print(f"Error: {e}"))
.build()
)
Callbacks vs. Middleware¶
Callbacks are per-agent – they apply only to the agent they’re attached to. For cross-cutting concerns that should apply to the entire execution (all agents in a pipeline), use middleware instead.
Aspect |
Callbacks |
Middleware |
|---|---|---|
Scope |
Single agent |
Entire execution |
Attachment |
|
|
Multiplicity |
Multiple per agent |
Stack of middleware on pipeline |
Compilation |
Stored on IR node |
Stored in ExecutionConfig |
# Per-agent callback: only affects this agent
agent = Agent("a").before_model(log_fn)
# App-global middleware: affects all agents in the pipeline
from adk_fluent import RetryMiddleware
pipeline = (Agent("a") >> Agent("b")).middleware(RetryMiddleware())
See Middleware for the full middleware guide.
Interplay with Other Modules¶
Callbacks + Guards¶
.guard(fn) registers a function as both before_model and after_model. The G module provides structured guards that compile to callbacks automatically. Prefer G for safety/validation, raw callbacks for custom logic:
from adk_fluent import Agent, G
# G module: declarative, composable, phase-aware
agent = Agent("safe").guard(G.pii("redact") | G.length(max=500))
# Raw callback: custom logic that doesn't fit G
agent = Agent("custom").before_model(my_custom_check)
See Guards.
Callbacks + Presets¶
Bundle callbacks into reusable Presets to avoid repetition across agents:
from adk_fluent.presets import Preset
observability = Preset(before_model=log_fn, after_model=metrics_fn)
agent_a = Agent("a").use(observability)
agent_b = Agent("b").use(observability)
See Presets.
Callbacks + Context Engineering¶
Callbacks run after context engineering. The LLM request that before_model receives already has context filtering applied:
from adk_fluent import Agent, C
agent = (
Agent("classifier")
.context(C.none()) # Context filtered first
.before_model(log_request) # Sees the filtered request
)
See Context Engineering.
Callbacks + Testing¶
Test that callbacks are attached correctly by inspecting the IR:
ir = agent.to_ir()
assert ir.before_model_callbacks # Callbacks preserved in IR
See Testing.
Best Practices¶
Use callbacks for agent-specific behavior. Logging one agent’s requests? Callback. Logging all agents? Middleware
Use additive semantics intentionally. Multiple
.before_model()calls accumulate. If you want to replace, build a new agentUse
.guard()for safety, not.before_model(). Guards are semantically clearer and compose with the G moduleUse Presets for shared callbacks. Don’t repeat the same
.before_model().after_model()chain on 10 agentsKeep callbacks pure. Side effects (DB writes, API calls) in callbacks make testing hard. Log, validate, or transform – don’t orchestrate
See also
Middleware – pipeline-wide cross-cutting concerns
Presets – reusable callback bundles
Guards – structured safety with the G module
Testing – verifying callbacks are attached correctly
Best Practices – the “Callbacks vs. Middleware” decision tree