Chapter 2: ChatModelAgent, Runner, and AgentEvent (Console Multi-Turn)
Goal of this chapter: introduce the ADK execution abstraction (Agent + Runner) and implement a multi-turn conversation in a console program.
Code Location
- Entry code: cmd/ch02/main.go
Prerequisites
Same as Chapter 1: configure a working ChatModel (OpenAI or Ark).
Run
From examples/quickstart/chatwitheino:
go run ./cmd/ch02
After you see the prompt, type questions (empty line to exit):
you> Hi — what is an Agent in Eino?
...
you> Summarize it in one sentence
...
Key Concepts
From Component to Agent
In Chapter 1 we learned Components: replaceable, composable capability units in Eino:
ChatModel: call an LLMTool: execute specific tasksRetriever: retrieve informationLoader: load data
Relationship between Components and Agents:
- A Component is not a complete AI app: it is just a capability unit that must be organized/orchestrated/executed
- An Agent is a complete AI app: it encapsulates business logic and can run directly
- Agents use Components internally: the most fundamental are
ChatModel(conversation) andTool(actions)
Why do we need Agents?
If you only had Components, you would need to build a lot yourself:
- manage conversation history
- orchestrate call flow (when to call the model, when to call tools)
- handle streaming output
- implement interrupt/resume
- …
What does an Agent provide?
- A complete runtime framework:
Runneruniformly manages execution - A standard event-stream output:
Run() -> AsyncIterator[*AgentEvent], enabling streaming, interrupt, and resume - Extensibility: add tools, middleware, interrupts, etc.
- Out of the box: once created, an Agent can run without caring about internals
This chapter’s example:
ChatModelAgent is the simplest Agent. Internally it only uses ChatModel, but it already provides the full Agent runtime shape. Later chapters add Tool and more capabilities.
Agent Interface
Agent is the core interface in ADK and defines the basic behavior of an agent:
type Agent interface {
Name(ctx context.Context) string
Description(ctx context.Context) string
// Run executes the Agent and returns an event stream.
Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
}
Responsibilities:
Name()/Description(): identify the AgentRun(): execute the Agent, take input messages, and return an event stream
Design ideas:
- Unified abstraction: all Agents (ChatModelAgent, WorkflowAgent, SupervisorAgent, etc.) implement this interface
- Event-driven: the execution is emitted as
AsyncIterator[*AgentEvent]to support streaming responses - Extensibility: adding tools/middleware/interrupts later does not change the interface
ChatModelAgent
ChatModelAgent is an implementation of the Agent interface built on a ChatModel:
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "Ch02ChatModelAgent",
Description: "A minimal ChatModelAgent with in-memory multi-turn history.",
Instruction: instruction,
Model: cm,
})
ChatModel vs ChatModelAgent: what’s the difference?
| Dimension | ChatModel | ChatModelAgent |
| Role | Component | Agent |
| Interface | Generate() / Stream() | Run() -> AsyncIterator[*AgentEvent] |
| Output | Returns message content directly | Returns an event stream (messages + control actions) |
| Capability | Pure model calling | Extensible with tools, middleware, interrupts, etc. |
| When to use | Simple chat | Complex agent applications |
Why ChatModelAgent?
- Unified abstraction: ChatModel is one Component; an Agent is a higher-level abstraction combining multiple Components
- Event-driven output: streaming response, interrupt/resume, state transitions, etc.
- Extensibility: add tools/middleware/interrupts; ChatModel itself only calls the model
- Orchestration-friendly: managed by Runner and supports runtime features like checkpoints and recovery
In short:
- ChatModel = “a component that talks to LLM providers and abstracts differences (OpenAI, Ark, Claude, etc.)”
- ChatModelAgent = “an agent built on a model: it can call the model, and it can do more”
Analogy:
- ChatModel is like a “database driver”: hides differences between MySQL/PostgreSQL
- ChatModelAgent is like the “business logic layer”: built on the driver, plus rules and runtime management
Characteristics:
- encapsulates ChatModel calling logic
- provides a unified
Run() -> AgentEventoutput shape - can be extended with tools/middleware later
Runner
Runner is the entry point for executing an Agent and manages the Agent lifecycle:
type Runner struct {
a Agent // Agent to execute
enableStreaming bool
store CheckPointStore // state store for interrupt/resume
}
Why do we need Runner?
Although an Agent exposes Run(), calling it directly lacks many runtime capabilities:
- Lifecycle management: start/recover/interrupt states
- Checkpoint support: with
CheckPointStoreto implement interrupt/resume (later chapters) - Unified entry: convenient methods like
Run()andQuery() - Event stream wrapping: packages the agent’s output into a consumable
AsyncIterator[*AgentEvent]
Usage:
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
})
// Option 1: pass a message list
events := runner.Run(ctx, history)
// Option 2: convenience method with a single query string
events := runner.Query(ctx, "hello")
AgentEvent
AgentEvent is the event unit returned by Runner:
type AgentEvent struct {
AgentName string
RunPath []RunStep
Output *AgentOutput // output content
Action *AgentAction // control actions
Err error // execution error
}
Key fields:
event.Err: execution errorevent.Output.MessageOutput: message or message stream (streaming)event.Action: control actions such as interrupt/transition/exit (later chapters)
AsyncIterator: How to Consume an Event Stream
Runner.Run() returns *AsyncIterator[*AgentEvent], a non-blocking streaming iterator.
Why AsyncIterator instead of returning a final result directly?
Agent execution is streaming: the model generates token by token, with tool calls interleaved. Waiting for the full completion would increase perceived latency. AsyncIterator lets you consume events as they arrive.
Consumption pattern:
// events is *AsyncIterator[*AgentEvent], returned by runner.Run()
events := runner.Run(ctx, history)
for {
event, ok := events.Next() // blocks until there is an event or the stream ends
if !ok {
break // iterator closed, all events consumed
}
if event.Err != nil {
// handle error
}
if event.Output != nil && event.Output.MessageOutput != nil {
// handle message output (may be streaming)
}
}
Note: each runner.Run() creates a new iterator. After consumption it cannot be reused.
Implementing Multi-Turn Conversation
This chapter implements a simple multi-turn loop: user input → model reply → user input → …
How it works:
Without tools, ChatModelAgent completes a single model call within one Run(). Multi-turn chat is implemented by the caller maintaining history:
- Keep
history []*schema.Messageas accumulated conversation - For each user input, append
UserMessageto history - Call
runner.Run(ctx, history)and consume the event stream to collect assistant text - Append the assistant reply back into history and continue
Key snippet (note: this is a simplified excerpt and not directly runnable; see cmd/ch02/main.go):
history := make([]*schema.Message, 0, 16)
for {
// 1. Read user input
line := readUserInput()
if line == "" {
break
}
// 2. Append user message into history
history = append(history, schema.UserMessage(line))
// 3. Execute the Agent via Runner
events := runner.Run(ctx, history)
// 4. Consume the stream and collect assistant reply
content := collectAssistantFromEvents(events)
// 5. Append assistant message back into history
history = append(history, schema.AssistantMessage(content, nil))
}
Flow:
┌─────────────────────────────────────────┐
│ initialize history = [] │
└─────────────────────────────────────────┘
↓
┌──────────────────────┐
│ user input UserMessage│
└──────────────────────┘
↓
┌──────────────────────┐
│ append to history │
└──────────────────────┘
↓
┌──────────────────────┐
│ runner.Run(history) │
└──────────────────────┘
↓
┌──────────────────────┐
│ consume event stream │
└──────────────────────┘
↓
┌──────────────────────┐
│ append AssistantMessage│
└──────────────────────┘
↓
(loop)
Summary
- Agent interface: defines the agent’s behavior; the core is
Run() -> AsyncIterator[*AgentEvent] - ChatModelAgent: an Agent built on ChatModel, providing a unified execution abstraction
- Runner: the execution entry, managing lifecycle, checkpoints, and event streams
- AgentEvent: event-driven output unit supporting streaming and control actions
- Multi-turn conversation: maintained by caller-side history; each
Run()completes one turn