Google ADKgoogle-adk 2.8 · Python 3.10+
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
28 small wins to finish your pathNext lesson

Callbacks: code around the loop

A callback is your function, run at a fixed moment in the loop, with the power to look at what is happening and sometimes to stop it.

There are eight, in three families: around the agent, around the model, and around the tools.

CallbackWhat it receives
before_agent_callback, after_agent_callbackThe callback context
before_model_callbackThe context, and the request about to be sent
after_model_callbackThe context, and the response that came back
before_tool_callbackThe tool, its arguments, and the tool context
after_tool_callbackThe same, plus the tool's result
on_model_error_callback, on_tool_error_callbackThe same, plus the error

Only the two agent callbacks exist on every agent. The six model and tool ones are fields on LlmAgent, because they are about a loop with a model in it.

Two functions that watch

python
def before_tool(tool, args, tool_context):
    print("about to run:", tool.name, args)


def after_tool(tool, args, tool_context, tool_response):
    print("came back:   ", tool_response)

Ordinary functions. The parameter names are what ADK passes in, and printing is the whole body for now.

python
def lookup_order(order_id: str) -> dict:
    """Look up the status of an order by its id."""
    return {"status": "success", "state": "shipped"}


agent = LlmAgent(
    name="support",
    model=PretendModel(replies=[call("lookup_order", order_id="A17"), say("It shipped.")]),
    instruction="Help with orders.",
    tools=[lookup_order],
    before_tool_callback=before_tool,
    after_tool_callback=after_tool,
)
Example
print("answer:      ", await ask(agent, "Where is order A17?"))

Every tool call in this agent is now visible without touching the tools themselves. This is the cheapest observability in ADK and it is worth adding on day one of a real project.

Stopping something

A callback that returns a value replaces what would have happened. Return a tool response from before_tool_callback and the tool never runs.

python
def refund(amount: int) -> dict:
    """Refund an amount in rupees."""
    return {"status": "success", "refunded": amount}


def guard(tool, args, tool_context):
    if tool.name == "refund" and args.get("amount", 0) > 100:
        return {"status": "refused", "reason": "refunds over 100 need a person"}

Four lines of rule. Under a hundred, it returns nothing and the tool runs normally. Over, it returns a refusal and the tool is skipped.

python
guarded = LlmAgent(
    name="support",
    model=PretendModel(replies=[call("refund", amount=500),
                                say("I cannot refund that much without a person.")]),
    instruction="Help with orders.",
    tools=[refund],
    before_tool_callback=guard,
)
Example
runner = InMemoryRunner(agent=guarded, app_name="demo")
session = await runner.session_service.create_session(app_name="demo", user_id="u1")
message = types.Content(role="user", parts=[types.Part(text="refund 500")])

async for event in runner.run_async(user_id="u1", session_id=session.id, new_message=message):
    for part in (event.content.parts if event.content else []):
        if part.function_response:
            print("tool result:", part.function_response.response)
        elif part.text:
            print("answer:     ", part.text.strip())

The refund never ran. The model asked, the callback answered instead, and the model read that answer and explained it. Nothing about the tool changed, and nothing about the instruction had to be trusted.

Where a hard rule belongs
This is the difference between a rule in an instruction and a rule in code. The instruction is a request the model usually follows. A callback is a function that runs.
Try it yourself
  • Lower the limit to 100 and watch the same refund go through.
  • Add an after_model_callback that prints what the model returned.

This is what real progress feels like.