This note was generated with AI assistance, but the order for reading the code should be fine (all examples use the official Eino examples; personally I felt the official tutorials were a bit hard to understand).
Additionally, possibly due to the fast update pace, some methods in the code have already been marked as deprecated, but the examples still haven’t been updated to the latest methods — please map them to the current ones yourself.
Before diving into details, keep a master map in your head. Eino has only two call paths:
sequenceDiagram
actor U as User code
participant CM as ChatModel
participant A as Agent
participant R as Runner
participant I as Iterator
Note over U,I: === Lightweight path: use ChatModel directly (Getting Started §2) ===
U->>CM: Generate(messages) / Stream(messages)
CM-->>U: *schema.Message / StreamReader
Note over U,I: === Full path: Agent + Runner (Getting Started §3~§5) ===
U->>CM: ① NewChatModel(config)
CM-->>U: model
U->>A: ② NewChatModelAgent(model, instruction, tools)
A-->>U: agent
U->>R: ③ NewRunner(agent, streaming, checkpoint)
R-->>U: runner
U->>R: ④ Run(input) / Query(input, checkpointID)
R-->>I: AsyncIterator[*AgentEvent]
loop Consume events
I->>I: Next() → (event, ok)
end
Note over U,I: === Agentic path: Responses API (Agentic Advanced) ===
U->>CM: ① New(agenticModel)
CM-->>U: model
U->>A: ② NewTypedChatModelAgent[*AgenticMessage](model, ...)
A-->>U: agent
U->>R: ③ NewTypedRunner[*AgenticMessage](agent, ...)
R-->>U: runner
U->>R: ④ Run(AgenticMessage input)
R-->>I: Iterator
loop Consume events
I->>I: TypedGetMessage(event) → msg.String()
end
📇 Full cheat-sheet cards (after reading this you only need to remember these)
1
Create chain: NewChatModel → NewChatModelAgent → NewRunner → Run
⚡ Key Concept: What is an Iterator (event iterator)?
In all Eino examples, runner.Run() and runner.Query() return immediately with an AsyncIterator[*AgentEvent] (Iterator for short), and then you consume the events one by one via Next(). This is the most core data consumption pattern in Eino.
Why an Iterator?
Agent execution is asynchronous: a model call may take anywhere from a few seconds to tens of seconds, and may go through multiple stages like “thinking → call Tool → rethink → output text”. If Run() waited until everything finished before returning, your program would freeze.
The Iterator solves this problem — it immediately returns an “event pipeline”, the Agent generates events in the background and pushes them into the pipeline, and you pull them out and process them one by one in the foreground. Analogy:
1
Runner.Run() returning an Iterator = you get a "walkie-talkie"
2
The Agent works in the background, saying something through the walkie-talkie every now and then
3
You in the foreground: Next() → receive a message → Next() → receive the next one → ...
4
Next() returns (event, false) = the walkie-talkie loses signal = the Agent is done
The Nature of the Iterator
1
typeAsyncIterator[T any]struct{
2
// internally has a channel connecting the "producer" (Agent goroutine) and the "consumer" (your code)
3
}
4
5
func(iter *AsyncIterator[T])Next()(item T, ok bool){
6
// blocks waiting for the next event
7
// ok=true: got a new event
8
// ok=false: the pipeline is closed, Agent execution finished
9
}
Comparison with Go channels
Go channel
AsyncIterator
Create
make(chan T)
Returned by Runner.Run() / Runner.Query()
Send
ch <- item
Agent internally calls gen.Send(event)
Receive
item := <-ch
event, ok := iter.Next()
Close
close(ch)
Agent internally calls gen.Close()
Close detection
item, ok := <-ch
event, ok := iter.Next() — also ok=false
🧠 In one sentence: The Iterator is an “event stream” wrapping a channel — the Agent writes events into it in the background, and you read them one by one in the foreground with Next(). When you finish reading (ok=false), the Agent is done.
I. Hello World: The Four-Step Assembly Line
📁 Code: adk/helloworld/helloworld.go
Why is it designed this way?
Eino splits an AI conversation application into four independent parts, created one by one like an assembly line, and finally assembled and run together. The benefit of this split is: each part can be replaced individually (e.g., swap the model, swap the Agent type) without affecting the others.
The four-step assembly line (memory mnemonic: “model → agent → runner → message”)
// runner.Run() returns an event iterator; consume events one by one
52
events := runner.Run(ctx, input)
53
for{
54
event, ok := events.Next()
55
if!ok {
56
break
57
}
58
if event.Err !=nil{
59
log.Printf("Error: %v", event.Err)
60
break
61
}
62
if msg, err := event.Output.MessageOutput.GetMessage(); err ==nil{
63
fmt.Printf("Agent: %s\n", msg.Content)
64
}
65
}
66
}
💡 Key understanding
Concept
In one sentence
Analogy
context.Background()
The “pass” for all calls, fixed boilerplate
ID card, shown every time you do something
ChatModel
The component responsible for communicating with the LLM
Database driver (abstracts MySQL/PG differences)
Agent
Encapsulates the business logic of “how to use the model”
Business logic layer
Runner
Manages the Agent’s execution lifecycle
Car engine (you only turn the wheel, not the internals)
events.Next()
Consume the next event, returns false when no more events
Buffet conveyor belt, take plate by plate
🧠 Memory trick
Remember the creation process as one sentence: “Use a certain model (ChatModel), build an agent (Agent), hand it to the engine (Runner) to run (Run)”
1
NewChatModel → NewChatModelAgent → NewRunner → Run
2
(model) (agent) (runner) (message)
Common import paths:
1
"github.com/cloudwego/eino-ext/components/model/openai"// OpenAI-compatible model; more supported code not listed — refer directly to the official docs
"github.com/cloudwego/eino/schema"// UserMessage, SystemMessage, etc.
💡 Type alias note: adk.Message is just *schema.Message (a Go type alias). Writing []adk.Message{ schema.UserMessage(...) } and []*schema.Message{ schema.UserMessage(...) } is equivalent. Both forms appear in the docs — when you see them, just know they’re the same thing.
📇 Cheat-sheet cards for this section
1
NewChatModel → NewChatModelAgent → NewRunner → Run
"question":"My code keeps throwing errors, what should I do?",
22
"chat_history":[]*schema.Message{
23
schema.UserMessage("Hello"),
24
schema.AssistantMessage("Hey! Keep it up!",nil),
25
},
26
})
27
// ... handle err
28
return messages
29
}
💡 components/prompt/chat_prompt/chat_prompt.go shows a more complete template usage.
2.2 One-shot generation (Generate)
1
result, err := model.Generate(ctx, messages)
2
if err !=nil{
3
log.Fatal(err)
4
}
5
fmt.Println(result.Content)
2.3 Streaming generation (Stream) — focus on mastering this
1
streamReader, err := model.Stream(ctx, messages)
2
if err !=nil{
3
log.Fatal(err)
4
}
5
defer streamReader.Close()// ⚠️ Remember to close when done!
6
7
for{
8
chunk, err := streamReader.Recv()
9
if err == io.EOF {break}
10
if err !=nil{
11
log.Fatal(err)
12
}
13
fmt.Print(chunk.Content)
14
}
💡 What is StreamReader?*schema.StreamReader[*schema.Message] returned by model.Stream() is similar to the Iterator — the model generates chunks in the background, and you consume them chunk by chunk via Recv(). But there are key differences:
Iterator (used by Agent): Next() returns (event, bool), !ok means the Agent finished execution
StreamReader (used by ChatModel): Recv() returns (chunk, error), io.EOF means the stream ended
defer Close() is mandatory: StreamReader holds an HTTP connection underneath; not calling Close causes connection leak / goroutine leak
⚠️ The most easily confused part of this section
Eino has two iteration modes, with different end conditions:
The compose package will be covered in depth in section seven — here you only need to know that compose.ToolsNodeConfig is the “config shell” used when registering Tools.
3.1 Why do you need an Agent instead of using ChatModel directly?
Dimension
ChatModel (component)
ChatModelAgent (agent)
Position
Single capability unit
Complete AI application
Output
Generate() / Stream() return message directly
Run() returns an event stream which can include tool calls, interrupts, etc.
🧠 How to choose: 90% of scenarios are covered by InferTool; use NewTool when you need to manually constrain parameter types / enum values; use the struct approach when the Tool itself is stateful (e.g., has a database connection). All three registration methods into the Agent are exactly the same — just drop them into the ToolsConfig.Tools array.
3.3 Assemble the full application (Tool + streaming)
ℹ️ Focus of this section: only shows the main flow of Tool + Agent + Runner + Query. Interrupt/resume (Resume) is covered separately in section five.
Error: open nonexistent.txt: no such file or directory
3
// 💥 Conversation interrupted directly
Problem 2: Model API rate limiting causes failure
1
Error: rate limit exceeded (429)
2
// 💥 Conversation interrupted
The behavior you want: when a Tool errors, hand the error to the LLM to self-heal; when the model is rate-limited, retry automatically. This is what Middleware solves — an interceptor for the Agent, inserting custom logic before and after the call.
4.2 The nature of Middleware: the onion model
1
Request → A.Wrap → B.Wrap → C.Wrap → actual execution → C returns → B returns → A returns → Response
2
↑ ↑
3
Outermost intercepts first Innermost touches the actual result first
4.3 Core scenario 1: SafeToolMiddleware (custom new middleware: Tool error to string)
1
// Define a struct to carry the middleware logic. Embed a base middleware to reuse its default behavior.
2
typesafeToolMiddlewarestruct{
3
*adk.BaseChatModelAgentMiddleware
4
}
5
6
// Intercept synchronous tool calls; the method signature is defined in BaseChatModelAgentMiddleware
// ⚠️ Interrupt errors must continue propagating, cannot be swallowed
17
if _, ok := compose.IsInterruptRerunError(err); ok {
18
return"", err
19
}
20
// normal error → convert to string so the LLM can see it
21
return fmt.Sprintf("[tool error] %v", err),nil
22
}
23
// no error, just return the raw result
24
return result,nil
25
},nil
26
}
27
28
// You can also intercept other calls like WrapStreamableToolCall for streaming tools, etc. — see the official examples yourself
💡 Key distinction: compose.IsInterruptRerunError is a special error thrown by interrupt/resume (section five); it must continue propagating upward and cannot be converted to a string.
4.4 Core scenario 2: ModelRetryConfig (automatic model call retry)
The original IsRetryAble was marked deprecated; now you should use ShouldRetry instead
Some middleware can be specified directly when defining the agent, such as ModelRetryConfig
Eino also pre-defines some middleware, such as agentsmd, dynamictool/toolsearch, etc., which can be called directly. A few common ones are introduced below.
📁 adk/middlewares/dynamictool/toolsearch/
Pain point: Too many Tools will blow up the context window. ToolSearch first puts all Tools into a “tool library”, and searches/filters when calling:
🧠 Difference from section three’s ToolsConfig: Section three writes Tools into ToolsConfig.Tools — all sent to the LLM. ToolSearch searches/filters first, only sending the relevant ones to the LLM.
// Section five only cares about the interrupt API; the underlying principle of CheckPointStore is in section seven
5.2 Method 1: Interrupt inside Tool (actively pause and wait for input)
1
User inputs "recommend a book"
2
│
3
▼
4
runner := adk.NewRunner(ctx, adk.RunnerConfig{
5
Agent: a,
6
CheckPointStore: store.NewInMemoryStore(), // runner sets CheckPointStore to store the subsequent CheckPointID
7
})
8
│
9
▼
10
runner.Query(ctx, "recommend a book", WithCheckPointID("1")) pass CheckPointID; subsequent resume uses this
11
│
12
▼
13
Agent analysis: not enough info → Tool returns Interrupt("What genre of books do you like?") Note: in the example code NewInterruptAndRerunErr is marked enabled
14
│
15
▼
16
Receive Interrupted event in the event stream → pause, wait for user input
17
│
18
▼
19
runner.Resume(ctx, "1", WithToolOptions(WithNewInput("sci-fi"))) resume via CheckPointID
20
│
21
▼
22
Agent continues → re-calls Tool with "sci-fi" → returns result
Note: since this is already outdated + essentially the same as Method 3, you can directly look at how Method 3 interrupts.
Design intent: When the user presses Ctrl-C, instead of killing the process and wasting the work — safely pause, automatically save progress, then resume from the breakpoint later.