1. See Why Manually Chained Services Lose Control
Suppose a company's order, risk, and search services already run in Go. The team builds a refund-risk assistant: read the order, check risk rules, ask the user when information is missing, and stream a recommendation. A few functions called in sequence are enough for the first demo.
More features make that manual chain brittle. The order function returns a struct while the model expects messages; the model streams chunks while the next function only accepts a complete value; a user reply arrives but the program cannot identify the paused step; a retry may repeat a side-effecting tool. The missing piece is not one larger function. It is an execution structure that constrains input and output, organizes control flow, and handles interruption and recovery consistently.
2. Learn Eino As Six Layers Of Building Blocks
Eino exposes many types, but a beginner only needs six layers from smallest to largest. Each answers one concrete question:
- Component is one capability such as a model, tool, retriever, or prompt template; it declares what it accepts and returns.
- Node is the position a component occupies in one execution, such as the read-order node.
- Graph defines node order, branches, joins, and loops: the route through the task.
- Runnable is the compiled graph's uniform execution contract, supporting ordinary and streaming input or output.
- Runner advances the graph and owns active tasks, data channels, and interruption; recoverable checkpoints require a configured store and stable ID.
- Agent is a model loop built on the first five layers: the model chooses tools while the graph and Runner execute and manage the loop.
Calling Eino an execution-graph framework does not mean every application is merely a picture. It means models, tools, and agents eventually share one set of composition, execution, and recovery rules. With these six layers in place, the Go generics and interfaces later in the chapter have a clear purpose.
3. Follow One Refund-Risk Check From Start To Finish
Eino has two valid entry routes, not one four-layer sequence. The Compose application route starts
with NewGraph → Compile → Runnable. The ADK agent route starts with
Runner.Query → flowAgent → ChatModelAgent, and ChatModelAgent uses a ReAct graph internally. This refund
example chooses Compose because order, risk, and approval steps need explicit business shapes. ADK appears later as an
alternative entry.
- The user submits an order identifier, and the message enters the graph at its start node.
- The read-order component returns order data; the type boundary ensures the next node receives the promised structure rather than improvised text.
- The model node decides that risk data is required and routes execution to a tool node that calls the risk service.
- With enough data, the graph proceeds to recommendation generation, and Runnable streams model chunks to the interface.
- If the refund reason is missing, Runner interrupts at the question node and stores the position and accumulated data as a checkpoint.
- After the user replies, Runner restores channels, state, and pending input, then task calculation chooses the next nodes.
- The graph reaches END and returns a recommendation; business validation and the refund system decide whether to adopt it.
Steps five and six have prerequisites.
The application must configure a CheckPointStore and start this execution with a stable checkpoint ID.
TypedRunner.Resume fails when the store is nil. Interrupt tells the caller where execution paused and what it awaits;
one token does not persist every channel and state value by itself.
Eino's README opens with a direct positioning statement: Eino is an LLM application development framework in Golang, drawing from LangChain, Google ADK, and other open-source frameworks while following Golang conventions. It provides four areas: Components, Agent Development Kit, Composition, and Examples. Source: Eino README overview.
The short package docs make the runtime boundary sharper. The root package provides building blocks for agent workflows, tools, and composable graph utilities. The compose package provides graph and workflow primitives for composable, interruptible execution pipelines with callback support.
Sources:
doc.go
and
compose/doc.go.
Reading contract.
Follow one line: Eino first solves how Go components compose, stream, interrupt, and resume; it then
implements ADK Runner, ChatModelAgent, and AgentTool on that foundation. Start with
Runnable[I, O] and compose.Graph[I, O], not with team roles or an isolated ReAct loop.
Evidence boundary.
Eino links are pinned to public snapshot e8832e223b93a7f45b1cc3d491239c14f94717c0.
This article uses only public README and source. Typed Go execution graph is my engineering reading of the source owner, not an official Eino term.
A "check refund risk for this order" request makes the runtime shape easier to follow:
Compose application path used by this refund example
RefundRequest
-> compose.NewGraph
-> OrderSnapshot
-> RiskContext
-> DecisionDraft
-> Graph.Compile() -> Runnable
-> runner: channels + tasks + checkpoint
ADK agent path, shown later as an alternative entry
[]*schema.Message{user("check refund risk")}
-> adk.Runner.Query
-> flowAgent
-> ChatModelAgent
-> internal ReAct graph
Shared compose primitive inside either path
graph := compose.NewGraph[[]*schema.Message, *schema.Message]()
Graph.AddEdge(START, "planner")
Graph.AddEdge("planner", "tools")
Graph.Compile() -> Runnable
The point is not a nicer model-call wrapper. Eino puts node input / output, streaming data, interrupt points, and resume state into one Go graph runtime. The component, Graph, runner, and ADK sections below all hang off that route.
4. Components Seal Model, Tool, And Message Shapes
Do not let generic message types hide the business records. The Compose refund graph carries
RefundRequest{order_id, reason} from the user, OrderSnapshot{status, amount} from the order
service, RiskContext{score, policy} from risk evaluation, and finally
DecisionDraft{action, rationale} into generation. Graph generics constrain these transitions.
[]*schema.Message later describes the ADK model ledger; it is not the order record.
Eino's component layer is idiomatic Go. BaseModel[M] uses a sealed type constraint: message type M can only be *schema.Message or *schema.AgenticMessage. It exposes Generate and Stream. BaseChatModel is the *schema.Message alias and takes conversation history. The old ChatModel.BindTools path is deprecated because it mutates the instance and can race under concurrency. ToolCallingChatModel.WithTools returns an immutable per-request variant. AgenticModel is the *schema.AgenticMessage alias and receives tools through request options.
Source:
components/model/interface.go.
The tool layer separates metadata from execution. BaseTool.Info returns name, description, and parameter JSON schema. Execution requires InvokableTool, StreamableTool, or enhanced variants that return multimodal tool results. That split keeps model-visible schema separate from the side-effecting runtime.
Source:
components/tool/interface.go.
schema.Message
Roles are system, user, assistant, and tool. Assistant messages can carry ToolCalls; tool messages return results through ToolCallID. This is the conversation-history shape familiar from chat completions.
schema.AgenticMessage
Content is a list of ContentBlock values. Blocks include reasoning, function tools, server tools, MCP calls, and MCP approval request / response records. This better matches provider-native agentic events.
This is not a naming difference. schema.Message defines roles, tool calls, multimodal parts, response metadata, and reasoning content. schema.AgenticMessage models reasoning, generated media, server tool calls, MCP tool calls, and approval requests as content blocks.
Sources:
Message role and tool call,
chat message parts,
Message struct,
and
AgenticMessage blocks.
5. Runnable Is The Minimal compose Runtime Contract
Eino's key abstraction is not a graph with one run method. Runnable[I, O] promises four data-flow shapes: Invoke for normal input to normal output, Stream for normal input to streaming output, Collect for streaming input to normal output, and Transform for streaming input to streaming output. The source comment says graph and chain compile into Runnable and that Eino provides downgrade compatibility across these four patterns.
Source:
Runnable interface.
| Data flow | Input | Output | Typical component |
|---|---|---|---|
Invoke |
Value | Value | One-shot classification, formatting, or synchronous tool execution. |
Stream |
Value | Stream | LLM token output or streaming search results. |
Collect |
Stream | Value | Aggregate upstream chunks into one final structure. |
Transform |
Stream | Stream | Rewrite while reading, such as streaming redaction, filtering, or conversion. |
This solves a real composition problem. Different components naturally support different stream shapes, so the framework should not force every component to implement all four manually. runnablePacker wraps user-provided invoke / stream / collect / transform functions into a composableRunnable with input, output, and option type checks. Default conversion functions then bridge modes through stream concatenation or single-element streams.
Sources:
runnable packing
and
default flow conversion.
Graph[I, O] is the typed graph that organizes those runnables. NewGraph can carry a local state generator. AddEdge requires the previous node's output type to match the next node's input type. Compile returns a Runnable, so the compiled graph still supports Invoke / Stream / Collect / Transform.
Sources:
NewGraph and state
and
AddEdge and Compile.
Internally, graph records control edges, data edges, branches, start and end nodes, state type, expected input and output types, handlers, and a compiled flag. graphRunType has Pregel and DAG modes. Pregel supports cycles and AnyPredecessor; DAG targets directed acyclic graphs with AllPredecessor.
Source:
graph fields and run types.
6. The Execution Loop Joins Channels, Tasks, Checkpoints, And Interrupts
Once a Graph is compiled, the runner owns more than edge traversal. It decides where data comes from, which
tasks are ready, what a checkpoint must restore, and how an interrupt hands control back to the caller.
Those responsibilities are visible in
runner fields.
| Runner-owned layer | What it makes explicit |
|---|---|
| channels / predecessors / successors | Data and control flow are scheduled by the graph runtime, not improvised through callbacks. |
| task manager | The runner computes which nodes are ready and which tasks have completed in each round. |
| checkpoint pointer | Recovery can reconstruct channels, graph state, and pending work. |
| interruptBefore / interruptAfter | Human approval or external waiting becomes a formal graph pause point. |
The first half of runner.run chooses Invoke or Transform, initializes the channel and task managers, resolves max steps, node options, checkpoint id, and subgraph path. It then either restores from checkpoint or starts from START and calculates the first tasks. If those tasks hit an interrupt-before node, execution returns an interrupt immediately.
Source:
runner.run setup.
The main loop repeats three operations: submit next tasks, wait for completed tasks, and calculate next tasks. It handles cancellation, max steps, subgraph interrupts, rerun nodes, interrupt-before and interrupt-after nodes, then returns the result when END is reached. DAG mode rejects max run steps, while non-DAG mode requires a positive limit. Source: main execution loop.
Recovery is more than a pointer to the next node. restoreCheckPointState restores channels, applies a state modifier, and attaches checkpoint state back to context. handleInterrupt copies state, records pending inputs, maps interrupt ids to addresses and state, and writes to the checkpoint store when needed.
Sources:
restore checkpoint state
and
handle interrupt.
The public interrupt API follows the same path. Compile options can interrupt before or after named nodes. A simple component uses Interrupt; a component that needs to persist internal state uses StatefulInterrupt; a composite node such as ToolsNode uses CompositeInterrupt to flatten multiple sub-interrupts into one resumable signal.
Sources:
interrupt compile options,
Interrupt and StatefulInterrupt,
and
CompositeInterrupt.
Recovery boundary. A checkpoint owns the replayable channels, state, and pending inputs. An interrupt identifies the paused address and exposes the waiting condition. The caller later supplies resume data; it does not have to reconstruct the graph's internal state, and the interrupt token alone is not that state.
| Application provides | Recoverable state | After process restart |
|---|---|---|
| Interrupt information only | The caller knows the waiting condition, but Resume has no complete execution state to load. | Not recoverable. |
In-process CheckPointStore + stable ID | Channels, state, and pending inputs can be restored in that process. | The checkpoint disappears when its store disappears. |
Durable CheckPointStore + stable ID | A new Runner can load the checkpoint by ID and restore graph state. | Cross-process recovery depends on application-provided durability and shared visibility. |
The checkpoint also does not wrap an external risk call or refund write in one transaction. Task calculation may select a node again after recovery. Reads must tolerate repeat queries; writes need idempotency keys, deduplication, or compensation. Eino can restore graph state without promising exactly-once behavior for every business side effect.
7. Callbacks Are Aspects, Not Loose Logging Hooks
Eino treats callbacks as a runtime boundary. RunInfo describes the entity that triggered a callback: user-meaningful name, implementation type, and component category. The docs tell handlers to filter by RunInfo rather than assuming any fixed order between different handlers.
Source:
RunInfo.
Handler unifies OnStart, OnEnd, OnError, stream-input start, and stream-output end. Context can flow between timings of the same handler, but not across different handlers. Stream handlers receive copied StreamReader values and must close those copies, otherwise the original stream cannot be freed. The same comment forbids mutating input or output values because downstream nodes and handlers share the same pointer, which would race in concurrent graph execution.
Source:
Handler contract.
8. ADK Runner Is The Agent Entry Point, Still Behind flowAgent
The README quick start creates a ChatModel, creates adk.NewChatModelAgent, hands it to adk.NewRunner, then reads events from runner.Query. Tools go into ToolsConfig through compose.ToolsNodeConfig. The README says the agent handles the ReAct loop internally.
Source:
ChatModelAgent quick start.
In source, TypedRunner is the primary entry point for executing an Agent. It starts, resumes, and checkpoints. More importantly, the comment says execution always goes through the flowAgent pipeline, which handles multi-agent orchestration, callbacks, agent naming, run paths, and cancellation. Run and Query return AsyncIterator. When a checkpoint store is available, Resume and ResumeWithParams continue from checkpoints and can target resume data by address.
Source:
TypedRunner.
Runner implementation preserves the message split. For *schema.Message, it converts the agent to a legacy-compatible flowAgent. Otherwise it uses typed flowAgent. During resume, the streaming mode stored in the checkpoint is the source of truth, not the value passed to a newly constructed Runner. The event loop turns internal interrupt signals into public interrupt contexts and saves checkpoints when a checkpoint id is present.
Sources:
runner run implementation,
runner resume implementation,
and
runner event handling.
9. ChatModelAgent Is An Agent Loop Built On A Graph
ToolsConfig embeds compose.ToolsNodeConfig, then adds ReturnDirectly and EmitInternalEvents. Internal agent-tool events can be forwarded to the parent agent's AsyncGenerator, but they are not recorded in the parent state or checkpoint. Interrupted is the exception, propagated through CompositeInterrupt for cross-agent resume.
Source:
ToolsConfig.
TypedChatModelAgentConfig shows the agent-level surface: Name, Description, Instruction, Model, ToolsConfig, GenModelInput, Exit, OutputKey, MaxIterations, middlewares, handlers, retry, and failover. The handler comment is long for a reason: model calls, tool calls, event sending, state rewriting, dynamic tool lists, and prompt cache behavior all depend on wrapper order.
Source:
TypedChatModelAgentConfig.
This is the best place to connect back to the AgentScope chapter on OpenAI Responses API. Eino is not merely selecting a different endpoint in an adapter. It separates the two message models at the type level: *schema.Message uses a full ReAct loop, model to tool calls to model; *schema.AgenticMessage uses a single-shot chain because agentic models handle tool calling internally. The comment is on TypedChatModelAgent, and buildReActRunFunc switches by message type.
Sources:
TypedChatModelAgent mode split
and
buildReActRunFunc.
The normal Message branch creates a newReact graph. It wraps agent input with a compose.NewChain, converts it into react input, appends the ReAct graph, then compiles with graph name, checkpoint store, serializer, and max steps. At runtime it calls runnable.Stream or runnable.Invoke depending on EnableStreaming.
Source:
message ReAct run function.
The AgenticMessage branch also builds a graph and chain, but its graph comes from newAgenticReact, its input is *schema.AgenticMessage, and its model type is model.AgenticModel. Provider-native agentic content blocks and traditional chat history remain separated at the Go type layer, graph compile layer, and event layer.
Source:
agentic run function.
Runtime execution freezes a default run function through buildRunFunc. If handlers modify tools or instruction before the agent runs, getRunFunc can rebuild the graph for that runtime context. Run creates the AsyncIterator / AsyncGenerator pair, appends a bridge checkpoint id, passes tool infos through model.WithTools, and runs the function in a goroutine.
Source:
ChatModelAgent.Run.
10. AgentTool Collapses Multi-Agent Coordination Into A Tool Boundary
The README describes DeepAgent as a pattern that breaks complex tasks into steps, delegates to sub-agents, and tracks progress. The Composition example also shows a graph wrapped as a tool and handed to an agent. Eino connects multi-agent behavior and deterministic workflow through the same tool and graph surfaces, not through a separate message bus. Source: DeepAgent and Composition.
NewAgentTool draws the boundary explicitly. The wrapped agent must have non-empty Name and Description because they become the tool name and description. Internal events can be forwarded to the parent's user-visible event stream, but not recorded in parent state or checkpoint. Exit, TransferToAgent, and BreakLoop are scoped inside the agent tool. Only Interrupted crosses the boundary through CompositeInterrupt.
Source:
NewAgentTool.
At runtime, InvokableRun first checks whether this tool call is resuming from interrupt state. On a new run, it builds agent input from arguments or full chat history, creates a runner, and uses the bridge checkpoint id. On resume, it creates a resume bridge store and calls runner.Resume. Its event loop closes previous streams, forwards internal events when configured, and if the final event is Interrupted, it loads bridge checkpoint data and returns tool.CompositeInterrupt.
Sources:
agent tool setup and resume
and
agent tool event loop.
10.1 Graph Completion Produces An Output; Business Adoption Comes Later
Reaching END or exhausting the iterator only produces a DecisionDraft. The application still
validates data freshness, required rules, unresolved interrupts, and approval for high-risk actions. Only the refund
system accepting an idempotent write and returning a receipt makes the change adopted. Keeping produced, validated,
and adopted separate prevents the false conclusion that “the graph ended, therefore the money was refunded.”
11. Put It Back Beside The Earlier Frameworks
| Framework | First Owner To Read | Common Misread |
|---|---|---|
| AgentScope | Agent turn ledger and OpenAI API adapters. | Responses API is not just Chat Completions with renamed fields; it is an evented item model. |
| ADK Python | Code-first Agent + Workflow + Runner. | It is not only a graph DSL. Autonomous agents and deterministic workflows share runtime boundaries. |
| Agno | AgentOS platform control plane. | It is not just a larger agent dataclass. API, storage, approval, RBAC, and interfaces are platform surfaces. |
| AutoGen / MAF | Message runtime to production orchestration. | After AutoGen entered maintenance mode, new projects should start from MAF Agent, Workflow, and Hosting. |
| CrewAI | Agent / Task / Crew / Flow. | Do not look for the bus first. It models team autonomy and production control for application developers. |
| Eino | Component / Runnable / compose Graph / ADK Runner. | Do not reduce it to Go ReAct. ReAct agent behavior is one layer over typed runnable graphs. |
| tRPC-Agent-Go | Runner / session summary / memory / recall tools. | Do not read memory as chat summarization. Summary compresses the current session; memory persists durable facts. |
The route is now cleaner. Read AgentScope for provider items and turn ledgers. Read Pi for a minimal coding harness, session tree, and compaction. Read ADK for code-first agent and workflow boundaries. Read Agno for a platform control plane. Read AutoGen and MAF for the move from message runtime to production orchestrator. Read CrewAI for team workflow. Read Eino for the Go-native path that gathers components, streams, interrupts, and agent loops into composable execution graphs. Read the next tRPC-Agent-Go chapter for a Go agent service runtime, then its summary, memory, and hidden-history recall path.
Sources
- Eino README
- Eino root package docs
- Eino compose package docs
- Eino model interfaces
- Eino tool interfaces
- Eino
schema.Message - Eino
schema.AgenticMessage - Eino
Runnable - Eino generic graph
- Eino graph internals
- Eino graph runner
- Eino interrupt API
- Eino callbacks
- Eino ADK Runner
- Eino ChatModelAgent
- Eino AgentTool