Stream a tool round trip#

Turn a Python function into a tool and show its progress in your application. The model asks for add, receives 42, and produces a final answer.

Run it#

From the repository root:

uv run python docs/_examples/streaming_tools.py

The output includes:

Answer: 42

This program uses deterministic offline models and temporary storage where needed. It requires no API key and is executed by the documentation test suite.

The three pieces to reuse#

Define the capability. Type annotations describe the arguments, and the docstring tells the model when to call it:

@tool
def add(left: int, right: int) -> int:
    """Add two integers exactly.

    Args:
        left: First operand.
        right: Second operand.

    Snippet:
        add(left=20, right=22)

    Guidelines:
        - Use this tool for exact integer addition.
    """
    return left + right

Display progress. Event callbacks receive reasoning, the tool call, its result, and incremental answer text:

class ConsoleEvents(AgentEventDispatcher):
    async def on_reasoning_delta_event(self, event: AgentEvent) -> None:
        print(f"Reasoning: {event.delta}")

    async def on_tool_started_event(self, event: AgentEvent) -> None:
        call = event.tool_calls[0]
        print(f"Tool: {call.name} {call.arguments}")

    async def on_tool_completed_event(self, event: AgentEvent) -> None:
        assert event.message is not None
        print(f"Result: {event.message.content}")

    async def on_text_delta_event(self, event: AgentEvent) -> None:
        print(event.delta)

Connect them to a client. Replace MathModel() with your provider when you are ready to make real requests:

async def main() -> None:
    client = await create_agent(MathModel(), tools=[add], event_dispatcher=ConsoleEvents())
    reply = await client.run("What is 20 plus 22?")
    assert reply.content == "Answer: 42"

The scripted model always calls the tool. A real model chooses whether to call it, and may not return visible reasoning even when it reasons internally.

Complete source#

Download streaming_tools.py.

 1"""Run a typed tool and display reasoning, tool results, and final text."""
 2
 3import asyncio
 4from collections.abc import AsyncIterator
 5
 6from zett_agent.client import create_agent
 7from zett_agent.dispatcher import AgentEventDispatcher
 8from zett_agent.events import AgentEvent
 9from zett_agent.messages import (
10    AssistantMessage,
11    ToolCall,
12    ToolMessage,
13)
14from zett_agent.model import (
15    ModelEvent,
16    ModelRequest,
17    ModelResponse,
18    RetryOptions,
19)
20from zett_agent.tools.base import tool
21
22
23@tool
24def add(left: int, right: int) -> int:
25    """Add two integers exactly.
26
27    Args:
28        left: First operand.
29        right: Second operand.
30
31    Snippet:
32        add(left=20, right=22)
33
34    Guidelines:
35        - Use this tool for exact integer addition.
36    """
37    return left + right
38
39
40class MathModel:
41    """Script a model/tool/model round trip without making a network request."""
42
43    retry = RetryOptions(max_retries=0)
44
45    async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
46        if isinstance(request.messages[-1], ToolMessage):
47            answer = f"Answer: {request.messages[-1].content}"
48            yield ModelEvent.text(answer)
49            yield ModelEvent.completed(ModelResponse(AssistantMessage(content=answer)))
50        else:
51            assert any(definition.name == "add" for definition in request.tools)
52            yield ModelEvent.reasoning("Use exact arithmetic.")
53            yield ModelEvent.completed(
54                ModelResponse(AssistantMessage(tool_calls=(ToolCall("sum-1", "add", {"left": 20, "right": 22}),)))
55            )
56
57
58class ConsoleEvents(AgentEventDispatcher):
59    async def on_reasoning_delta_event(self, event: AgentEvent) -> None:
60        print(f"Reasoning: {event.delta}")
61
62    async def on_tool_started_event(self, event: AgentEvent) -> None:
63        call = event.tool_calls[0]
64        print(f"Tool: {call.name} {call.arguments}")
65
66    async def on_tool_completed_event(self, event: AgentEvent) -> None:
67        assert event.message is not None
68        print(f"Result: {event.message.content}")
69
70    async def on_text_delta_event(self, event: AgentEvent) -> None:
71        print(event.delta)
72
73
74async def main() -> None:
75    client = await create_agent(MathModel(), tools=[add], event_dispatcher=ConsoleEvents())
76    reply = await client.run("What is 20 plus 22?")
77    assert reply.content == "Answer: 42"
78
79
80if __name__ == "__main__":
81    asyncio.run(main())

Next: Stream to a terminal or UI for event handling and cancellation, or Define and register tools for schemas, errors, and safe execution.