Eino ADK: ChatModelAgentMiddleware

Overview

ChatModelAgentMiddleware Interface

ChatModelAgentMiddleware defines the interface for customizing ChatModelAgent behavior.

Important: This interface is designed specifically for ChatModelAgent and Agents built on top of it (such as DeepAgent).

πŸ’‘ The ChatModelAgentMiddleware interface was introduced in v0.8.0.Beta

Why Use ChatModelAgentMiddleware Instead of AgentMiddleware?

FeatureAgentMiddleware (struct)ChatModelAgentMiddleware (interface)
ExtensibilityClosed, users cannot add new methodsOpen, users can implement custom handlers
Context PropagationCallbacks only return errorAll methods return
(context.Context, ..., error)
Configuration ManagementScattered in closuresCentralized in struct fields

Interface Definition

type ChatModelAgentMiddleware interface {
    // BeforeAgent is called before each agent run, allows modifying instruction and tools configuration
    BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)

    // BeforeModelRewriteState is called before each model call
    // The returned state will be persisted to the agent's internal state and passed to the model
    // The returned context will be propagated to the model call and subsequent handlers
    BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)

    // AfterModelRewriteState is called after each model call
    // The input state contains the model response as the last message
    AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)

    // WrapInvokableToolCall wraps the synchronous execution of a tool with custom behavior
    // If no wrapping is needed, return the original endpoint and nil error
    // Only called for tools that implement InvokableTool
    WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error)

    // WrapStreamableToolCall wraps the streaming execution of a tool with custom behavior
    // If no wrapping is needed, return the original endpoint and nil error
    // Only called for tools that implement StreamableTool
    WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error)

    // WrapEnhancedInvokableToolCall wraps the synchronous execution of an enhanced tool with custom behavior
    WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error)

    // WrapEnhancedStreamableToolCall wraps the streaming execution of an enhanced tool with custom behavior
    WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error)

    // WrapModel wraps the chat model with custom behavior
    // If no wrapping is needed, return the original model and nil error
    // Called at request time, executed before each model call
    WrapModel(ctx context.Context, m model.BaseChatModel, mc *ModelContext) (model.BaseChatModel, error)
}

Using BaseChatModelAgentMiddleware

Embed *BaseChatModelAgentMiddleware to get default no-op implementations:

type MyHandler struct {
    *adk.BaseChatModelAgentMiddleware
}

func (h *MyHandler) BeforeModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, mc *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) {
    return ctx, state, nil
}

Tool Call Endpoint Types

Tool wrapping uses function types instead of interfaces, more clearly expressing the wrapping intent:

// InvokableToolCallEndpoint is the function signature for synchronous tool calls
type InvokableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error)

// StreamableToolCallEndpoint is the function signature for streaming tool calls
type StreamableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error)

// EnhancedInvokableToolCallEndpoint is the function signature for enhanced synchronous tool calls
type EnhancedInvokableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error)

// EnhancedStreamableToolCallEndpoint is the function signature for enhanced streaming tool calls
type EnhancedStreamableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error)

Why Use Separate Endpoint Types?

The previous ToolCall interface contained both InvokableRun and StreamableRun, but most tools only implement one of them. Separate endpoint types enable:

  • Corresponding wrap methods are only called when the tool implements the respective interface
  • Clearer contract for wrapper authors
  • No ambiguity about which method to implement

ChatModelAgentContext

ChatModelAgentContext contains runtime information passed to handlers before each ChatModelAgent run.

type ChatModelAgentContext struct {
    // Instruction is the instruction for the current Agent execution
    // Includes agent-configured instructions, framework and AgentMiddleware appended extra instructions,
    // and modifications applied by previous BeforeAgent handlers
    Instruction string

    // Tools are the original tools (without any wrappers or tool middleware) currently configured for Agent execution
    // Includes tools passed in AgentConfig, tools implicitly added by the framework (like transfer/exit tools),
    // and other tools added by middleware
    Tools []tool.BaseTool

    // ReturnDirectly is the set of tool names currently configured to make the Agent return directly
    ReturnDirectly map[string]bool
}

ChatModelAgentState

ChatModelAgentState represents the state of the chat model agent during conversation. This is the primary state type for ChatModelAgentMiddleware and AgentMiddleware callbacks.

type ChatModelAgentState struct {
    // Messages contains all messages in the current conversation session
    Messages []Message
}

ToolContext

ToolContext provides metadata about the tool being wrapped. Created at request time, contains information about the current tool call.

type ToolContext struct {
    // Name is the tool name
    Name string

    // CallID is the unique identifier for this specific tool call
    CallID string
}

Usage Example: Tool Call Wrapping

func (h *MyHandler) WrapInvokableToolCall(ctx context.Context, endpoint adk.InvokableToolCallEndpoint, tCtx *adk.ToolContext) (adk.InvokableToolCallEndpoint, error) {
    return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
        log.Printf("Tool %s (call %s) starting with args: %s", tCtx.Name, tCtx.CallID, argumentsInJSON)
        
        result, err := endpoint(ctx, argumentsInJSON, opts...)
        
        if err != nil {
            log.Printf("Tool %s failed: %v", tCtx.Name, err)
            return "", err
        }
        
        log.Printf("Tool %s completed with result: %s", tCtx.Name, result)
        return result, nil
    }, nil
}

ModelContext

ModelContext contains context information passed to WrapModel. Created at request time, contains tool configuration for the current model call.

type ModelContext struct {
    // Tools is the list of tools currently configured for the agent
    // Populated at request time, contains the tools that will be sent to the model
    Tools []*schema.ToolInfo

    // ModelRetryConfig contains the retry configuration for the model
    // Populated at request time from the agent's ModelRetryConfig
    // Used by EventSenderModelWrapper to appropriately wrap stream errors
    ModelRetryConfig *ModelRetryConfig
}

Usage Example: Model Wrapping

func (h *MyHandler) WrapModel(ctx context.Context, m model.BaseChatModel, mc *adk.ModelContext) (model.BaseChatModel, error) {
    return &myModelWrapper{
        inner: m,
        tools: mc.Tools,
    }, nil
}

type myModelWrapper struct {
    inner model.BaseChatModel
    tools []*schema.ToolInfo
}

func (w *myModelWrapper) Generate(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
    log.Printf("Model called with %d tools", len(w.tools))
    return w.inner.Generate(ctx, msgs, opts...)
}

func (w *myModelWrapper) Stream(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
    return w.inner.Stream(ctx, msgs, opts...)
}

Run-Local Storage API

SetRunLocalValue, GetRunLocalValue, and DeleteRunLocalValue provide the ability to store, retrieve, and delete values during the current agent Run() call.

// SetRunLocalValue sets a key-value pair that persists during the current agent Run() call
// The value is scoped to this specific execution and is not shared between different Run() calls or agent instances
//
// Values stored here are compatible with interrupt/resume cycles - they are serialized and restored when the agent resumes
// For custom types, they must be registered in init() using schema.RegisterName[T]() to ensure proper serialization
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution
// Returns an error if called outside of agent execution context
func SetRunLocalValue(ctx context.Context, key string, value any) error

// GetRunLocalValue retrieves a value set during the current agent Run() call
// The value is scoped to this specific execution and is not shared between different Run() calls or agent instances
//
// Values stored via SetRunLocalValue are compatible with interrupt/resume cycles - they are serialized and restored when the agent resumes
// For custom types, they must be registered in init() using schema.RegisterName[T]() to ensure proper serialization
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution
// Returns (value, true, nil) if found, (nil, false, nil) if not found,
// returns error if called outside of agent execution context
func GetRunLocalValue(ctx context.Context, key string) (any, bool, error)

// DeleteRunLocalValue deletes a value set during the current agent Run() call
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution
// Returns an error if called outside of agent execution context
func DeleteRunLocalValue(ctx context.Context, key string) error

Usage Example: Sharing Data Across Handler Points

func init() {
    schema.RegisterName[*MyCustomData]("my_package.MyCustomData")
}

type MyCustomData struct {
    Count int
    Name  string
}

type MyHandler struct {
    *adk.BaseChatModelAgentMiddleware
}

func (h *MyHandler) WrapInvokableToolCall(ctx context.Context, endpoint adk.InvokableToolCallEndpoint, tCtx *adk.ToolContext) (adk.InvokableToolCallEndpoint, error) {
    return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
        result, err := endpoint(ctx, argumentsInJSON, opts...)
        
        data := &MyCustomData{Count: 1, Name: tCtx.Name}
        if err := adk.SetRunLocalValue(ctx, "my_handler.last_tool", data); err != nil {
            log.Printf("Failed to set run local value: %v", err)
        }
        
        return result, err
    }, nil
}

func (h *MyHandler) AfterModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, mc *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) {
    if val, found, err := adk.GetRunLocalValue(ctx, "my_handler.last_tool"); err == nil && found {
        if data, ok := val.(*MyCustomData); ok {
            log.Printf("Last tool was: %s (count: %d)", data.Name, data.Count)
        }
    }
    return ctx, state, nil
}

SendEvent API

SendEvent allows sending custom AgentEvent to the event stream during agent execution.

// SendEvent sends a custom AgentEvent to the event stream during agent execution
// Allows ChatModelAgentMiddleware implementations to emit custom events,
// which will be received by callers iterating over the agent's event stream
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution
// Returns an error if called outside of agent execution context
func SendEvent(ctx context.Context, event *AgentEvent) error

State Type (To Be Deprecated)

State holds agent runtime state, including messages and user-extensible storage.

⚠️ Deprecation Warning: This type will be made unexported in v1.0.0. Please use ChatModelAgentState in ChatModelAgentMiddleware and AgentMiddleware callbacks. Direct use of compose.ProcessState[*State] is not recommended and will stop working in v1.0.0; please use the handler API instead.

type State struct {
    Messages []Message
    extra    map[string]any  // unexported, access via SetRunLocalValue/GetRunLocalValue

    // The following are internal fields - do not access directly
    // Kept exported for backward compatibility with existing checkpoints
    ReturnDirectlyToolCallID string
    ToolGenActions           map[string]*AgentAction
    AgentName                string
    RemainingIterations      int

    internals map[string]any
}

Architecture Diagram

The following diagram shows how ChatModelAgentMiddleware works during ChatModelAgent execution:

Agent.Run(input)
                                  β”‚
                                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  BeforeAgent(ctx, *ChatModelAgentContext)                               β”‚
β”‚    Input: Current Instruction, Tools and other Agent runtime env        β”‚
β”‚    Output: Modified Agent runtime env                                   β”‚
β”‚    Purpose: Called once at Run start, modifies config for entire Run    β”‚
β”‚             lifecycle                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          ReAct Loop                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                                                                   β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚  BeforeModelRewriteState(ctx, *ChatModelAgentState, *MC)    β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Input: Persistent state like message history, plus Model β”‚  β”‚  β”‚
β”‚  β”‚  β”‚           runtime env                                       β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Output: Modified persistent state, returns new ctx       β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Purpose: Modify persistent state across iterations       β”‚  β”‚  β”‚
β”‚  β”‚  β”‚             (mainly message list)                           β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                            β”‚                                      β”‚  β”‚
β”‚  β”‚                            β–Ό                                      β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚  WrapModel(ctx, BaseChatModel, *ModelContext)               β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Input: ChatModel being wrapped, plus Model runtime env   β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Output: Wrapped Model (onion model)                      β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Purpose: Modify input, output and config for single      β”‚  β”‚  β”‚
β”‚  β”‚  β”‚             Model request                                   β”‚  β”‚  β”‚
β”‚  β”‚  β”‚                         β”‚                                   β”‚  β”‚  β”‚
β”‚  β”‚  β”‚                         β–Ό                                   β”‚  β”‚  β”‚
β”‚  β”‚  β”‚                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚  β”‚  β”‚
β”‚  β”‚  β”‚                 β”‚    Model      β”‚                           β”‚  β”‚  β”‚
β”‚  β”‚  β”‚                 β”‚ Generate/Streamβ”‚                          β”‚  β”‚  β”‚
β”‚  β”‚  β”‚                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                            β”‚                                      β”‚  β”‚
β”‚  β”‚                            β–Ό                                      β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚  AfterModelRewriteState(ctx, *ChatModelAgentState, *MC)     β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Input: Persistent state like message history (with Model β”‚  β”‚  β”‚
β”‚  β”‚  β”‚           response), plus Model runtime env                 β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Output: Modified persistent state                        β”‚  β”‚  β”‚
β”‚  β”‚  β”‚    Purpose: Modify persistent state across iterations       β”‚  β”‚  β”‚
β”‚  β”‚  β”‚             (mainly message list)                           β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                            β”‚                                      β”‚  β”‚
β”‚  β”‚                            β–Ό                                      β”‚  β”‚
β”‚  β”‚                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                             β”‚  β”‚
β”‚  β”‚                  β”‚ Model return?    β”‚                             β”‚  β”‚
β”‚  β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                             β”‚  β”‚
β”‚  β”‚                     β”‚            β”‚                                β”‚  β”‚
β”‚  β”‚       Final responseβ”‚            β”‚ ToolCalls                      β”‚  β”‚
β”‚  β”‚                     β”‚            β–Ό                                β”‚  β”‚
β”‚  β”‚                     β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚  WrapInvokableToolCall / WrapStream β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚  ableToolCall(ctx, endpoint, *TC)   β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚    Input: Tool being wrapped plus   β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚           Tool runtime env          β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚    Output: Wrapped endpoint         β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚            (onion model)            β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚    Purpose: Modify input, output    β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚             and config for single   β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚             Tool request            β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚                  β”‚                  β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚                  β–Ό                  β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚          β”‚ Tool.Run()  β”‚            β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚    β”‚  β”‚
β”‚  β”‚                     β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  β”‚
β”‚  β”‚                     β”‚            β”‚                                β”‚  β”‚
β”‚  β”‚                     β”‚            β”‚ (Result added to Messages)     β”‚  β”‚
β”‚  β”‚                     β”‚            β”‚                                β”‚  β”‚
β”‚  β”‚                     β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                β”‚  β”‚
β”‚  β”‚                     β”‚  β”‚                                          β”‚  β”‚
β”‚  β”‚                     β”‚  └──────────► Continue loop                 β”‚  β”‚
β”‚  β”‚                     β”‚                                             β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                        β”‚                                                β”‚
β”‚                        β–Ό                                                β”‚
β”‚               Loop until complete or maxIterations reached              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                          Agent.Run() ends

Handler Method Description

MethodInputOutputScope
BeforeAgent
Agent runtime env (
*ChatModelAgentContext
)
Modified Agent runtime envEntire Run lifecycle, called only once
BeforeModelRewriteState
Persistent state + Model runtime envModified persistent statePersistent state across iterations (message list)
WrapModel
ChatModel being wrapped + Model runtime envWrapped ModelSingle Model request input, output and config
AfterModelRewriteState
Persistent state (with response) + Model runtime envModified persistent statePersistent state across iterations (message list)
WrapInvokableToolCall
Tool being wrapped + Tool runtime envWrapped endpointSingle Tool request input, output and config
WrapStreamableToolCall
Tool being wrapped + Tool runtime envWrapped endpointSingle Tool request input, output and config

Execution Order

Model Call Lifecycle (wrapper chain from outer to inner)

  1. AgentMiddleware.BeforeChatModel (hook, runs before model call)
  2. ChatModelAgentMiddleware.BeforeModelRewriteState (hook, can modify state before model call)
  3. retryModelWrapper (internal - retries on failure, if configured)
  4. eventSenderModelWrapper preprocessing (internal - prepares event sending)
  5. ChatModelAgentMiddleware.WrapModel preprocessing (wrapper, wrapped at request time, first registered runs first)
  6. callbackInjectionModelWrapper (internal - injects callbacks if not enabled)
  7. Model.Generate/Stream
  8. callbackInjectionModelWrapper postprocessing
  9. ChatModelAgentMiddleware.WrapModel postprocessing (wrapper, first registered runs last)
  10. eventSenderModelWrapper postprocessing (internal - sends model response event)
  11. retryModelWrapper postprocessing (internal - handles retry logic)
  12. ChatModelAgentMiddleware.AfterModelRewriteState (hook, can modify state after model call)
  13. AgentMiddleware.AfterChatModel (hook, runs after model call)

Tool Call Lifecycle (from outer to inner)

  1. eventSenderToolHandler (internal ToolMiddleware - sends tool result event after all processing)
  2. ToolsConfig.ToolCallMiddlewares (ToolMiddleware)
  3. AgentMiddleware.WrapToolCall (ToolMiddleware)
  4. ChatModelAgentMiddleware.WrapInvokableToolCall/WrapStreamableToolCall (wrapped at request time, first registered is outermost)
  5. Tool.InvokableRun/StreamableRun

Migration Guide

Migrating from AgentMiddleware to ChatModelAgentMiddleware

Before (AgentMiddleware):

middleware := adk.AgentMiddleware{
    BeforeChatModel: func(ctx context.Context, state *adk.ChatModelAgentState) error {
        return nil
    },
}

After (ChatModelAgentMiddleware):

type MyHandler struct {
    *adk.BaseChatModelAgentMiddleware
}

func (h *MyHandler) BeforeModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, mc *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) {
    newCtx := context.WithValue(ctx, myKey, myValue)
    return newCtx, state, nil
}

Migrating from compose.ProcessState[*State]

Before:

compose.ProcessState(ctx, func(_ context.Context, st *adk.State) error {
    st.Extra["myKey"] = myValue
    return nil
})

After (using SetRunLocalValue/GetRunLocalValue):

if err := adk.SetRunLocalValue(ctx, "myKey", myValue); err != nil {
    return ctx, state, err
}

if val, found, err := adk.GetRunLocalValue(ctx, "myKey"); err == nil && found {
}

Last modified March 2, 2026: feat: sync eino docs (#1512) (96139d41)