I’ve been exploring the Agent Developer Kit (ADK) and its powerful callbacks feature. In this blog post, I want to outline what callbacks are and provide a sample agent with all the callbacks implemented for a quick reference and testing.
At its core, an agent framework like ADK gives you a sequence of steps:
receive input → invoke model → invoke tools → return output
In real-world systems, we often need to hook into these steps for logging, guarding, caching, altering prompts or results, or dynamically changing behaviour based on session state. That’s exactly where callbacks come in. Think of callbacks as “checkpoints” in the agent’s lifecycle. The ADK framework automatically calls your functions at these key stages, giving you a chance to intervene.
ADK Callbacks
The ADK provides three main pairs of callbacks:
- Agent callbacks:
before_agent_callback: Runs just before the agent’s main logic starts.after_agent_callback: Runs just after the agent has produced its final result.
2. Model callbacks:
before_model_callback: Runs right before the request is sent to the LLM.after_model_callback: Runs right after the response is received from the LLM.
3. Tool callbacks:
before_tool_callback: Runs before a tool (like a Python function) is executed.after_tool_callback: Runs after the tool has returned its result.
You can either observe and modify data in place (e.g., logging a request) or override the default behavior entirely (e.g., blocking the request, providing a cached response).
- To observe and let execution continue, your callback function should return
None. - To override and skip the next step, your callback function should return a specific object (like an
LlmResponseortypes.Content) depending on the type of the callback.
Let’s look at some concrete examples from my adk-callbacks-agent sample. It has test flags you can flip in different callbacks to see the behaviour.
Registering callbacks
You need to register your callbacks with the agent. Here’s an example that shows how to register all the callbacks:
root_agent = Agent(
model="gemini-2.5-flash",
name="root_agent",
description="A helpful assistant for user questions.",
instruction="Answer user questions to the best of your knowledge",
tools=[get_weather],
before_agent_callback=before_agent_callback,
after_agent_callback=after_agent_callback,
before_model_callback=before_model_callback,
after_model_callback=after_model_callback,
before_tool_callback=before_tool_callback,
after_tool_callback=after_tool_callback,
)
Agent callbacks
In agent callbacks, you can either return Content to change the behavior or None to allow the default behavior.
Here’s an example of before_agent_callback to log agent calls and skip agent execution:
def before_agent_callback(callback_context: CallbackContext) -> Optional[Content]: print(f"▶ before_agent_callback")print(f" Agent: {callback_context.agent_name}")
print(f" Invocation ID: {callback_context.invocation_id}")
print(f" Current State: {callback_context.state.to_dict()}")
# Return Content to skip agent execution
test = False
if test:
print(f" Agent execution skipped")
return Content(
parts=[
Part(
text=f"Agent '{callback_context.agent_name}'
execution skipped by 'before_agent_callback'."
)
],
role="model", # Assign model role to the overriding response
)
# Allow default behavior
return None
Similarly, here’s an after_agent_callback to log and modify the agent response:
def after_agent_callback(callback_context: CallbackContext) -> Optional[Content]: print(f"▶ after_agent_callback")
print(f" Agent: {callback_context.agent_name}")
print(f" Invocation ID: {callback_context.invocation_id}")
print(f" Current State: {callback_context.state.to_dict()}")# Return Content to modify the agent response
test = False
if test:
print(f" Agent response modified")
return Content(
parts=[
Part(
text=f"This is additional response added by 'after_agent_callback'."
)
],
role="model", # Assign model role to the overriding response
)
# Allow default behavior
return None
Model callbacks
Model callbacks are similar to agent callbacks but instead you return LlmResponse to alter the behavior and None to allow the default behavior.
Here’s a before_model_callback that logs the model call and then skips it:
def before_model_callback(
callback_context: CallbackContext, llm_request: LlmRequest) -> Optional[LlmResponse]: print(f"▶ before_model_callback")
print(f" Agent: {callback_context.agent_name}")
print(f" Invocation ID: {callback_context.invocation_id}")# Return LlmResponse to skip model call
if test:
print(f" Model call skipped")
return LlmResponse(
content=Content(
parts=[Part(text=f"Model call skipped by 'before_model_callback'.")],
role="model", # Assign model role to the overriding response
)
)
# Allow default behavior
return None
Here’s after_model_callback that modifies the model response:
def after_model_callback(
callback_context: CallbackContext, llm_response: LlmResponse) -> Optional[LlmResponse]: # Modify the model response with a new LlmResponse
if test:
print(f" Model response modified to be uppercase")
modified_response = LlmResponse(
content=Content(
parts=[
Part(
text=f"[Modified by after_model_callback]
{llm_response.content.parts[0].text.upper()}"
)
],
role="model",
)
)
return modified_response
# Allow default behavior
return None
Tool callbacks
In tool callbacks, you can log and modify the tool calls.
For example, here’s before_tool_callback to log and skip the tool execution:
def before_tool_callback(
tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext
) -> Optional[Dict]:print(f"▶ before_tool_callback")
print(f" Agent: {tool_context.agent_name}")
print(f" Invocation ID: {tool_context.invocation_id}")
print(f" Tool: {tool.name}")
print(f" Args: {args}")
# Return tool response to skip tool execution
test = False
if test:
if tool.name == "get_weather" and args.get("location").lower() == "london":
tool_response = "The weather in London is always rainy and gloomy."
print(
f" Tool execution skipped for location London and returning tool
response: {tool_response}"
)
return tool_response
# Allow default behavior
return None
Here’s after_tool_callback to modify the tool response:
def after_tool_callback(
tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext, tool_response: Dict
) -> Optional[Dict]:# Modify the tool response
test = True
if test:
if tool.name == "get_weather":
tool_response = "The weather is always rainy and gloomy."
print(f" Tool response modified for 'get_weather' to: {tool_response}")
return tool_response
# Allow default behavior
return None
Conclusion
In this blog post, I explored different callbacks in ADK. To test them out, you can use my adk-callbacks-agent and to learn more, you can check ADK’s callbacks documentation.
Source Credit: https://medium.com/google-cloud/quick-guide-to-adk-callbacks-0752df0f4e15?source=rss—-e52cf94d98af—4
