TL;DR

An Eino ADK application is driven by Runner, not by calling a model-like agent.Run and expecting one message. In v0.9.21, ChatModelAgentConfig uses Instruction, ToolsConfig, and MaxIterations; Runner.Query or Runner.Run returns an AsyncIterator of AgentEvent values. Checkpointed interrupt/resume requires a CheckPointStore and stable checkpoint ID. These framework controls organize execution, but the application still owns authorization, budgets, idempotency, and outcome verification.

Key Takeaways

  • ChatModelAgent provides the common ReAct model-tool loop; without tools it performs one model invocation.
  • Runner owns execution, event delivery, streaming mode, checkpointing, and resume entry points.
  • MaxIterations is a loop guard, not a complete token, time, cost, or side-effect budget.
  • DeepAgent is imported from adk/prebuilt/deep and exposes high-impact capabilities that need sandboxing.
  • Human approval must bind the exact principal, action, arguments, policy revision, and expiry.

The Current ADK Contract

The pinned v0.9.21 package documentation and ChatModelAgent guide define these central types:

Type Role
adk.Agent Common agent interface
adk.ChatModelAgent ReAct-style model and tool loop
adk.Runner Starts runs, emits events, checkpoints, and resumes
adk.AgentEvent Message or control output plus error
adk.ToolsConfig ToolsNode configuration and return behavior
adk.CheckPointStore Persists resumable framework state

Older examples using SystemPrompt, a top-level Tools field, MaxSteps, StreamRun, adk.NewDeepAgent, or an invented serializable AgentCheckpoint do not match the current API.

sequenceDiagram participant C as Client participant R as Runner participant A as ChatModelAgent participant M as Model participant T as ToolsNode C->>R: Query or Run R->>A: AgentInput A->>M: Messages plus ToolInfo alt Tool call proposed M-->>A: Assistant ToolCall A->>T: Execute configured Tool T-->>A: Tool result A->>M: Updated messages else Final answer M-->>A: Assistant message end A-->>R: AgentEvent stream R-->>C: Message and control events

ChatModelAgent Configuration

The useful configuration fields in v0.9.21 include:

Field Purpose Boundary
Name / Description Agent identity and delegation metadata Not authentication
Instruction System instruction Not authorization
Model model.BaseModel implementation Must support tool options when tools are used
ToolsConfig Tool set and execution behavior Tool still needs server-side policy
MaxIterations Upper bound on model generations Default 20; not a cost SLA
Handlers Current interface-based middleware Ordering and data exposure need review
ModelRetryConfig Model-call retry policy Do not retry validation or policy denial
ModelFailoverConfig Select another model after eligible failure Re-evaluate behavior across models

Name and Description become inputs when an Agent is exposed to another Agent. Treat them as untrusted metadata, not a permission declaration.

A Runnable Agent Without Provider Credentials

This complete example targets Eino Core v0.9.21. A deterministic mock implements BaseChatModel, so the ChatModelAgent and Runner path can be compiled and executed without a network call.

go
package main

import (
	"context"
	"fmt"

	"github.com/cloudwego/eino/adk"
	"github.com/cloudwego/eino/components/model"
	"github.com/cloudwego/eino/schema"
)

type fixedModel struct{}

func (fixedModel) Generate(
	ctx context.Context,
	input []*schema.Message,
	_ ...model.Option,
) (*schema.Message, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	return schema.AssistantMessage("accepted: "+input[len(input)-1].Content, nil), nil
}

func (fixedModel) Stream(
	ctx context.Context,
	input []*schema.Message,
	_ ...model.Option,
) (*schema.StreamReader[*schema.Message], error) {
	message, err := (fixedModel{}).Generate(ctx, input)
	if err != nil {
		return nil, err
	}
	return schema.StreamReaderFromArray([]*schema.Message{message}), nil
}

func main() {
	ctx := context.Background()
	agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
		Name:          "bounded-assistant",
		Description:   "Returns one deterministic response.",
		Instruction:   "Answer once.",
		Model:         fixedModel{},
		MaxIterations: 2,
	})
	if err != nil {
		panic(err)
	}

	runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent})
	events := runner.Query(ctx, "inspect incident")
	for {
		event, ok := events.Next()
		if !ok {
			break
		}
		if event.Err != nil {
			panic(event.Err)
		}
		if event.Output == nil || event.Output.MessageOutput == nil {
			continue
		}
		message, err := event.Output.MessageOutput.GetMessage()
		if err != nil {
			panic(err)
		}
		fmt.Println(message.Content)
	}
}

Expected output:

text
accepted: inspect incident

Replace fixedModel with a pinned EinoExt provider only after adding credentials, timeout, rate-limit, retry, usage, and model-version tests.

Add Tools Without Expanding Authority

Tools belong inside adk.ToolsConfig, normally through compose.ToolsNodeConfig. The visible tool set should be derived per request from policy:

  1. Authenticate the caller outside the model.
  2. Resolve tenant, object scope, and data classification.
  3. Select the minimum Tool set for the task.
  4. Pass Tool descriptions to the model.
  5. Parse generated arguments strictly.
  6. Authorize the exact action again at execution.
  7. Apply idempotency, quotas, egress policy, and result-size limits.
  8. Record the effect ID and authoritative outcome.

ReturnDirectly controls Agent response flow; it does not make a Tool result trusted or authorized.

Interrupt and Resume

The HITL documentation models an interrupt as resumable execution state. A production flow needs:

  • a CheckPointStore configured on Runner;
  • a stable WithCheckPointID value when starting the run;
  • an approval record tied to the authenticated reviewer;
  • the exact Tool name, normalized arguments, object version, policy revision, and expiry;
  • Runner.Resume for implicit resume or ResumeWithParams for targeted data.

Do not serialize an imagined checkpoint struct from a blog example. Use the framework store contract for the pinned version, protect it as sensitive state, and test schema migration and expiry.

Approval is not the same as execution success. After resume, the downstream operation still needs idempotency and reconciliation.

When DeepAgent Is Justified

The current DeepAgents documentation exposes deep.New under github.com/cloudwego/eino/adk/prebuilt/deep. It can add planning, file operations, shell execution, and sub-agent delegation.

That larger capability surface is justified only when:

  • representative tasks need decomposition or delegated context;
  • the improvement beats a simpler ChatModelAgent or deterministic Workflow;
  • file and shell backends are sandboxed with narrow mounts and egress;
  • generated plans and TODOs are treated as working state, not authority;
  • output, fan-out, iterations, time, and spend are bounded.

Planning does not guarantee a good plan. Reflection does not prove correctness. A sub-agent does not inherit access merely because the parent can describe a task.

State, Memory, and Checkpoints Are Different

Concept Purpose Common mistake
Input messages Context for one run Assuming the Agent stores them forever
Session values Request or workflow state Treating them as an authorization source
Long-term memory Product-managed durable facts Storing model inference as user fact
Checkpoint Resume framework execution Assuming external side effects roll back
Trace Observable evidence Logging secrets or hidden reasoning

The Agent Memory guide covers durable memory, while state persistence covers checkpoints and recovery.

Release Gates

  • Compile against an exact Eino and EinoExt dependency graph.
  • Test direct answer, one Tool Call, repeated Tool Calls, malformed arguments, and unknown Tool.
  • Enforce MaxIterations, wall time, Token, Tool, byte, and spend budgets independently.
  • Test cancellation before, during, and after Tool execution.
  • Inject transient, permanent, unauthorized, and ambiguous failures.
  • Verify checkpoint expiry, resume ownership, duplicate resume, and version migration.
  • Compare ChatModelAgent and DeepAgent against a deterministic baseline.
  • Review every permission increase separately from a model or Prompt change.

Frequently Asked Questions

Does ChatModelAgent keep conversation history automatically across requests?

Do not assume that. A run receives messages and can accumulate state inside that execution. Cross-request history needs an explicit Session or application store and a retention, deletion, and authorization policy.

What is a sensible MaxIterations value?

There is no universal number. Start from the shortest successful traces in a representative evaluation set, then choose a bound that limits worst-case harm and cost. Keep separate wall-time, Tool, Token, and byte limits.

Does enabling Runner streaming expose model tokens directly?

Runner emits typed AgentEvent values. Consumers must handle streaming and non-streaming message variants, Tool events, actions, errors, cancellation, and redaction rather than assuming every event is displayable text.

Should Tool errors be returned to the model?

Only return a bounded, sanitized, typed error that helps choose a safe next step. Do not expose stack traces, credentials, internal topology, or authorization details.

Is an approval prompt enough for a sensitive action?

No. Bind approval to the reviewer identity, exact action and arguments, object revision, policy revision, and expiry. Re-authorize at execution and make the effect idempotent.

Primary Sources