Your first agent#
You will learn: how to create a client, run more than one turn, and what keeps the conversation between them. Prerequisites: Install Zett Agent. No API credentials are needed.
This tutorial uses a scripted model so you can check the result on any machine. It is not a language model and does not learn; it lets you experience the runtime before choosing a provider. If you already have credentials, jump to the complete tool-calling request.
1. Give the loop a model#
An agent does not know how to reach a specific vendor’s HTTP API. It expects a
model adapter: an object that accepts a ModelRequest
and streams ModelEvent objects back.
For the first example, the adapter counts user turns and echoes the latest one. It is not an LLM, but it plugs into exactly the same loop a real provider does.
class EchoModel:
"""Deterministic teaching adapter: count user turns and echo the latest one."""
retry = RetryOptions(max_retries=0)
async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
users = [message for message in request.messages if isinstance(message, UserMessage)]
answer = f"Turn {len(users)}: {users[-1].text}"
yield ModelEvent.text(answer)
yield ModelEvent.completed(ModelResponse(AssistantMessage(content=answer)))
Every model step ends with one complete ModelResponse.
The text deltas before it exist for display; the final response is what the
runtime uses.
2. Create one client, run many turns#
async def main() -> None:
client = await create_agent(EchoModel())
first = await client.run("Hello")
second = await client.run("Remember this conversation")
assert first.content == "Turn 1: Hello"
assert second.content == "Turn 2: Remember this conversation"
print(first.content)
print(second.content)
create_agent() builds the runtime and returns an
AgentClient. Because no session is supplied, it
generates one for you.
The second request remembers the first because the default extensions keep
in-memory history for the session. run() returns the complete
AssistantMessage.
3. Run the whole program#
1"""A complete offline conversation through the real Agent runtime."""
2
3import asyncio
4from collections.abc import AsyncIterator
5
6from zett_agent.client import create_agent
7from zett_agent.messages import (
8 AssistantMessage,
9 UserMessage,
10)
11from zett_agent.model import (
12 ModelEvent,
13 ModelRequest,
14 ModelResponse,
15 RetryOptions,
16)
17
18
19class EchoModel:
20 """Deterministic teaching adapter: count user turns and echo the latest one."""
21
22 retry = RetryOptions(max_retries=0)
23
24 async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
25 users = [message for message in request.messages if isinstance(message, UserMessage)]
26 answer = f"Turn {len(users)}: {users[-1].text}"
27 yield ModelEvent.text(answer)
28 yield ModelEvent.completed(ModelResponse(AssistantMessage(content=answer)))
29
30
31async def main() -> None:
32 client = await create_agent(EchoModel())
33 first = await client.run("Hello")
34 second = await client.run("Remember this conversation")
35 assert first.content == "Turn 1: Hello"
36 assert second.content == "Turn 2: Remember this conversation"
37 print(first.content)
38 print(second.content)
39
40
41if __name__ == "__main__":
42 asyncio.run(main())
After saving the download as first_agent.py in an environment where you
installed Zett Agent:
python first_agent.py
Or, from a checkout:
uv run python docs/_examples/first_agent.py
Expected output:
Turn 1: Hello
Turn 2: Remember this conversation
What happened?#
create_agent initialized a conversation with default history. Each
run appended a user message, called the adapter, and returned its final
assistant message. On the second call, the adapter counted two user messages
because the previous turn was restored. You did not need to pass history
manually or write a conversation loop.
Real providers can request tools before answering. The runtime executes those
tools and calls the model again with their results; a single run can
therefore involve several model steps. Try
the deterministic tool round trip to see
that behavior before making a paid request.
Next: switch the scripted model for a real provider#
Only the adapter construction changes. The same client, messages, extensions, tools, and event callbacks work with every built-in provider. Continue with Connect a model provider, then Stream to a terminal or UI.
Warning
Passing extensions=[] removes the optional defaults, including in-memory
history. Each request starts with fresh context; add
InMemoryMessageAccumulator or a
persistence extension when you want the conversation to carry over.
Related API: Agent,
AgentRunConfig,
InMemoryMessageAccumulator.