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?
| Feature | AgentMiddleware (struct) | ChatModelAgentMiddleware (interface) |
| Extensibility | Closed, users cannot add new methods | Open, users can implement custom handlers |
| Context Propagation | Callbacks only return error | All methods return (context.Context, ..., error) |
| Configuration Management | Scattered in closures | Centralized 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
| Method | Input | Output | Scope |
BeforeAgent | Agent runtime env (*ChatModelAgentContext) | Modified Agent runtime env | Entire Run lifecycle, called only once |
BeforeModelRewriteState | Persistent state + Model runtime env | Modified persistent state | Persistent state across iterations (message list) |
WrapModel | ChatModel being wrapped + Model runtime env | Wrapped Model | Single Model request input, output and config |
AfterModelRewriteState | Persistent state (with response) + Model runtime env | Modified persistent state | Persistent state across iterations (message list) |
WrapInvokableToolCall | Tool being wrapped + Tool runtime env | Wrapped endpoint | Single Tool request input, output and config |
WrapStreamableToolCall | Tool being wrapped + Tool runtime env | Wrapped endpoint | Single Tool request input, output and config |
Execution Order
Model Call Lifecycle (wrapper chain from outer to inner)
AgentMiddleware.BeforeChatModel(hook, runs before model call)ChatModelAgentMiddleware.BeforeModelRewriteState(hook, can modify state before model call)retryModelWrapper(internal - retries on failure, if configured)eventSenderModelWrapperpreprocessing (internal - prepares event sending)ChatModelAgentMiddleware.WrapModelpreprocessing (wrapper, wrapped at request time, first registered runs first)callbackInjectionModelWrapper(internal - injects callbacks if not enabled)Model.Generate/StreamcallbackInjectionModelWrapperpostprocessingChatModelAgentMiddleware.WrapModelpostprocessing (wrapper, first registered runs last)eventSenderModelWrapperpostprocessing (internal - sends model response event)retryModelWrapperpostprocessing (internal - handles retry logic)ChatModelAgentMiddleware.AfterModelRewriteState(hook, can modify state after model call)AgentMiddleware.AfterChatModel(hook, runs after model call)
Tool Call Lifecycle (from outer to inner)
eventSenderToolHandler(internal ToolMiddleware - sends tool result event after all processing)ToolsConfig.ToolCallMiddlewares(ToolMiddleware)AgentMiddleware.WrapToolCall(ToolMiddleware)ChatModelAgentMiddleware.WrapInvokableToolCall/WrapStreamableToolCall(wrapped at request time, first registered is outermost)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 {
}