Agent Callbacks
Callbacks provide hooks into the lifecycle of an AnyAgent execution. Using callbacks, you can monitor, control, and extend agent behavior without modifying the core underlying agent logic.
Implementing Callbacks
All callbacks must inherit from the base Callback class and can choose to implement any subset of the available callback methods. These methods include:
| Callback Method | When It Fires | Example Use Cases |
|---|---|---|
| before_agent_invocation | Once at start, before any LLM calls | Initialize counters, validate inputs, set up logging |
| before_llm_call | Before each LLM API call | Content filtering, cost tracking, prompt inspection |
| after_llm_call | After LLM responds, before adding to history | Response validation, token counting, logging |
| before_tool_execution | Before each tool runs | Rate limiting, input validation, authorization checks |
| after_tool_execution | After tool completes | Result validation, metrics collection, error handling |
| after_agent_invocation | Once at end, before returning final response | Cleanup, final metrics, audit logging |
# Minimum valid implementation
def before_llm_call(self, context: Context, *args, **kwargs) -> Context:
return context # <--- Essential!
Managing State (Context)
During an agent run ( agent.run_async or agent.run ), a unique Context object is created and shared across all callbacks.
Use Context.shared (a dictionary) to persist data across different steps and callbacks.
Note: The
Contextobject is mutable. You should modifyContext.shareddirectly and return the same object.
any-agent populates the Context.current_span
property so that callbacks can access information in a framework-agnostic way.
You can see what attributes are available for LLM Calls and Tool Executions by examining the GenAI class.
Common Pattern: Initialize a counter in one callback and check it in another.
from any_agent.callbacks import Callback, Context
from any_agent.tracing.attributes import GenAI
class CountSearchWeb(Callback):
def after_tool_execution(self, context: Context, *args, **kwargs) -> Context:
if "search_web_count" not in context.shared:
context.shared["search_web_count"] = 0
if context.current_span.attributes[GenAI.TOOL_NAME] == "search_web":
context.shared["search_web_count"] += 1
return context
Callbacks can raise exceptions to stop agent execution. This is useful for implementing safety guardrails or validation logic:
class LimitSearchWeb(Callback):
def __init__(self, max_calls: int):
self.max_calls = max_calls
def before_tool_execution(self, context: Context, *args, **kwargs) -> Context:
if context.shared["search_web_count"] > self.max_calls:
raise RuntimeError("Reached limit of `search_web` calls.")
Warning
Raising an exception is the standard way to halt execution. This effectively acts as a 'circuit breaker' for your agent.
Inspecting Data (Context.current_span)
The Context.current_span attribute provides access to the active trace span. This allows you to inspect (and modify) the data being processed, such as LLM inputs or Tool outputs.
Common attributes (available via any_agent.tracing.attributes.GenAI) include:
-
GenAI.INPUT_MESSAGES: The chat history sent to the model. -
GenAI.TOOL_NAME: The name of the tool currently being executed. -
GenAI.OUTPUT_MESSAGES: The response received from the model.
How it Works
When agent.run() or agent.run_async() executes, it triggers a series of events (e.g., before the LLM is called, after a tool is executed). You can register custom Callback classes to listen for these events.
The Callback Contract
All callbacks share a strict contract: They receive the current Context as input and must return a Context as output.
# pseudocode of an Agent run
history = [system_prompt, user_prompt]
context = Context()
for callback in agent.config.callbacks:
# 1. Agent Start
context = callback.before_agent_invocation(context)
while True:
for callback in agent.config.callbacks:
# 2. Pre-LLM
context = callback.before_llm_call(context)
response = CALL_LLM(history)
for callback in agent.config.callbacks:
# 3. Post-LLM
context = callback.after_llm_call(context)
history.append(response)
if response.tool_executions:
for tool_execution in tool_executions:
# 4. Pre-Tool
for callback in agent.config.callbacks:
context = callback.before_tool_execution(context)
tool_response = EXECUTE_TOOL(tool_execution)
for callback in agent.config.callbacks:
# 5. Post-Tool
context = callback.after_tool_execution(context)
history.append(tool_response)
else:
for callback in agent.config.callbacks:
# 6. Agent DONE
context = callback.after_agent_invocation(context)
return response
Advanced designs such as safety guardrails or custom side-effects can be integrated into your agentic system using this functionality.
Default Callbacks
any-agent comes with a set of default callbacks that will be used by default (if you don't pass a value to AgentConfig.callbacks):
If you want to disable these default callbacks, you can pass an empty list:
from any_agent import AgentConfig, AnyAgent
from any_agent.tools import search_web, visit_webpage
agent = AnyAgent.create(
"tinyagent",
AgentConfig(
model_id="mistral:mistral-small-latest",
instructions="Use the tools to find an answer",
tools=[search_web, visit_webpage],
callbacks=[]
),
)
Registering your own Callbacks
Callbacks are provided to the agent using the AgentConfig.callbacks property.
any-agent includes default callbacks (like console logging). Use get_default_callbacks to keep them:
from any_agent import AgentConfig, AnyAgent
from any_agent.callbacks import get_default_callbacks
from any_agent.tools import search_web, visit_webpage
agent = AnyAgent.create(
"tinyagent",
AgentConfig(
model_id="gpt-4.1-nano",
instructions="Use the tools to find an answer",
tools=[search_web, visit_webpage],
callbacks=[
CountSearchWeb(), # Custom callbacks first
LimitSearchWeb(max_calls=3),
*get_default_callbacks() #Runs after custom callbacks
]
),
)
To disable default logging or replace it entirely, pass a list without the defaults:
from any_agent import AgentConfig, AnyAgent
from any_agent.tools import search_web, visit_webpage
agent = AnyAgent.create(
"tinyagent",
AgentConfig(
model_id="gpt-4.1-nano",
instructions="Use the tools to find an answer",
tools=[search_web, visit_webpage],
callbacks=[
CountSearchWeb(),
LimitSearchWeb(max_calls=3) # Default console logging disabled
]
),
)
Warning
Callbacks will be called in the order that they are added, so it is important to pay attention to the order in which you set the callback configuration.
In the above example, passing:
callbacks=[
LimitSearchWeb(max_calls=3) # ← This will fail!
CountSearchWeb() # Counter must come first
]
Would fail because context.shared["search_web_count"] was not set yet.
Examples
Offloading sensitive information
Some inputs and/or outputs in your traces might contain sensitive information that you don't want to be exposed in the traces.
You can use callbacks to offload the sensitive information to an external location and replace the span attributes with a reference to that location:
import json
from pathlib import Path
from any_agent.callbacks.base import Callback
from any_agent.callbacks.context import Context
from any_agent.tracing.attributes import GenAI
class SensitiveDataOffloader(Callback):
def __init__(self, output_dir: str) -> None:
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True, parents=True)
def before_llm_call(self, context: Context, *args, **kwargs) -> Context:
span = context.current_span
if input_messages := span.attributes.get(GenAI.INPUT_MESSAGES):
output_file = self.output_dir / f"{span.get_span_context().trace_id}.txt"
output_file.write_text(str(input_messages))
span.set_attribute(
GenAI.INPUT_MESSAGES,
json.dumps(
{"ref": str(output_file)}
)
)
return context
You can find a working example in the Callbacks Cookbook.
Limit the number of steps
Some agent frameworks allow to limit how many steps an agent can take and some don't. In addition,
each framework defines a step differently: some count the llm calls, some the tool executions,
and some the sum of both.
You can use callbacks to limit how many steps an agent can take, and you can decide what to count
as a step:
from any_agent.callbacks.base import Callback
from any_agent.callbacks.context import Context
class LimitLLMCalls(Callback):
def __init__(self, max_llm_calls: int) -> None:
self.max_llm_calls = max_llm_calls
def before_llm_call(self, context: Context, *args, **kwargs) -> Context:
if "n_llm_calls" not in context.shared:
context.shared["n_llm_calls"] = 0
context.shared["n_llm_calls"] += 1
if context.shared["n_llm_calls"] > self.max_llm_calls:
raise RuntimeError("Reached limit of LLM Calls")
return context
class LimitToolExecutions(Callback):
def __init__(self, max_tool_executions: int) -> None:
self.max_tool_executions = max_tool_executions
def before_tool_execution(self, context: Context, *args, **kwargs) -> Context:
if "n_tool_executions" not in context.shared:
context.shared["n_tool_executions"] = 0
context.shared["n_tool_executions"] += 1
if context.shared["n_tool_executions"] > self.max_tool_executions:
raise RuntimeError("Reached limit of Tool Executions")
return context