Agents

Run the default agent loop with tools.

Use an agent when the model needs to call tools and continue with the tool results.

Create an agent

An agent wraps ai.stream in a loop. It streams model output, executes requested tools, appends tool results to history, and repeats until the model returns a final assistant message.

Subclass ai.Agent and override async def loop() when you need to change control flow.

agent_loop.py
import asyncio
import ai


@ai.tool
async def contact_mothership(query: str) -> str:
    """Contact the mothership for important decisions."""
    return "Soon."


async def main() -> None:
    model = ai.get_model("anthropic/claude-sonnet-4")
    agent = ai.Agent(tools=[contact_mothership])
    messages = [
        ai.system_message(
            "Use the contact_mothership tool when asked about the future."
        ),
        ai.user_message("When will the robots take over?"),
    ]

    async with agent.run(model, messages) as stream:
        async for event in stream:
            if isinstance(event, ai.events.TextDelta):
                print(event.chunk, end="", flush=True)

    print(stream.output)


if __name__ == "__main__":
    asyncio.run(main())

The stream yields model events and agent events. After the run finishes, stream.messages contains the updated history, and stream.output contains the final assistant output.

Unlike ai.stream, every agent.run can produce multiple messages alternating between "user"/"tool" and "assistant", representing turns in the LLM request and response cycle.

Understand multi-turn behavior

Each loop turn makes one call to ai.stream and produces one assistant message. If the message contains tool calls, the agent executes them, appends one tool-result message, and starts the next model turn.

async with agent.run(model, messages) as stream:
    async for event in stream:
        if isinstance(event, ai.events.ToolCallResult):
            for result in event.results:
                print(result.tool_name, result.result)
        elif isinstance(event, ai.events.TextDelta):
            print(event.chunk, end="", flush=True)

history = stream.messages

Use stream.messages when you want to persist the complete conversation after the run.

Pass params and structured output

Pass provider options with params. Pass a Pydantic model with output_type when the final assistant text should validate as JSON:

import pydantic


class Forecast(pydantic.BaseModel):
    answer: str
    eta: str


async with agent.run(
    model,
    [ai.user_message("Return a JSON mothership forecast.")],
    output_type=Forecast,
    params=ai.InferenceRequestParams().with_temperature(0),
) as stream:
    async for event in stream:
        if isinstance(event, ai.events.TextDelta):
            print(event.chunk, end="", flush=True)

forecast = stream.output
print(forecast.eta)