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

What a tool should return

A tool's return value goes straight back to the model. Its shape is a decision, not a detail.

Return a plain string and ADK wraps it up for you, which you saw in lesson 6: the result arrived as {'result': '...'}. Return a dictionary and it travels as you wrote it.

A tool that can fail

python
def lookup_order(order_id: str) -> dict:
    """Look up the status of an order by its id."""
    if order_id != "A17":
        return {"status": "not_found", "order_id": order_id}
    return {"status": "success", "state": "shipped", "date": "3 March"}

Two returns, one shape. A status field the model can read, and the rest of the fields only when there are any. Nothing here raises.

python
agent = LlmAgent(
    name="support",
    model=PretendModel(replies=[call("lookup_order", order_id="ZZZ"),
                                say("I could not find that order.")]),
    instruction="Help with orders.",
    tools=[lookup_order],
)

The model is scripted to ask about an order that does not exist, so the failing branch is the one that runs.

What the model is handed

Example
runner = InMemoryRunner(agent=agent, 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="Where is order ZZZ?")])

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("handed to the model:", part.function_response.response)
        elif part.text:
            print("the model then said: ", part.text.strip())

The dictionary reached the model unchanged, and it answered from it. That is the whole mechanism: your return value is the next thing the model reads.

The convention worth following

ReturnWhat the model can do with it
"Order A17 shipped"Read it. Fine when nothing can go wrong
{"status": "success", ...}Tell success from failure, and use the fields
{"status": "not_found", ...}Say what went wrong, and which input caused it
  • Return a status for things that fail normally: not found, out of stock, no permission.
  • Raise for things that mean your program is broken: a missing configuration, a bad credential.
  • Keep it small. Everything you return costs context on every later turn.
Return what is needed
Do not return your whole database row. The model reads every field you send, and so does every turn after this one, so a tool returning forty columns makes the conversation slower and more expensive for the rest of its life.
Try it yourself
  • Return a plain string instead and print what the model is handed.
  • Add a field the answer does not need and watch it travel anyway.

Slow is fine. Stopping is the only problem.