ChatModelAgentMiddleware
ChatModelAgentMiddleware is the core interface for customizing the behavior of ChatModelAgent (and DeepAgent built on top of it). Introduced in v0.8.0 and continuously evolving in subsequent releases.
Type Conventions
This document uses aliases with the default M = *schema.Message. The generic raw types are named with a Typed prefix:
type ChatModelAgentMiddleware = TypedChatModelAgentMiddleware[*schema.Message]
type BaseChatModelAgentMiddleware = TypedBaseChatModelAgentMiddleware[*schema.Message]
type ChatModelAgentState = TypedChatModelAgentState[*schema.Message]
type ModelContext = TypedModelContext[*schema.Message]
When you need to use *schema.AgenticMessage, use the Typed generic versions directly.
Interface Definition
type ChatModelAgentMiddleware interface {
// ── Lifecycle Hooks ──
// BeforeAgent: called once before the agent runs, can modify instruction and tools configuration
BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
// AfterAgent: called after the agent terminates successfully (final answer or return-directly tool result)
// Not called on error termination (max iterations exceeded, context cancelled, model error)
AfterAgent(ctx context.Context, state *ChatModelAgentState) (context.Context, error)
// BeforeModelRewriteState: called before each model invocation
// The returned state is persisted; Messages, ToolInfos, and DeferredToolInfos can be modified
BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
// AfterModelRewriteState: called after each model invocation
// The input state contains the model response as the last message
AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
// ── Wrappers ──
WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error)
WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error)
WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error)
WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error)
// WrapModel: wraps the ChatModel; parameter type is model.BaseModel[M] (not ToolCallingChatModel)
// The framework handles WithTools binding separately, not through user wrappers
WrapModel(ctx context.Context, m model.BaseModel[M], mc *ModelContext) (model.BaseModel[M], error)
}
💡 Embed
*BaseChatModelAgentMiddlewareto get no-op default implementations for all methods — only override the ones you care about.
Why an Interface Instead of the AgentMiddleware Struct?
AgentMiddleware is a struct with inherent limitations — users cannot extend methods, and callbacks only return error without propagating context. ChatModelAgentMiddleware is an interface:
- Hook methods return
(context.Context, ..., error), supporting context propagation - Wrapper methods propagate the modified context through the endpoint chain
- Custom handlers can carry arbitrary internal state
Selection principle: Use AgentMiddleware for simple static modifications (appending instructions/tools); use ChatModelAgentMiddleware when you need dynamic behavior, context modification, or call wrapping. Both can be used simultaneously.
Context Types
ChatModelAgentContext
Input to BeforeAgent, called once before each Run:
type ChatModelAgentContext struct {
// Current instruction (includes agent config + framework appended + prior handler modifications)
Instruction string
// Original tool list (includes framework-implicit tools like transfer/exit)
Tools []tool.BaseTool
// Set of tool names configured to "return directly"
ReturnDirectly map[string]bool
// ToolInfo for the model's native tool search capability
// After being set by a handler, the framework passes it to the model via model.WithToolSearchTool
ToolSearchTool *schema.ToolInfo
}
ChatModelAgentState
Persistent state passed before and after each model call (retained across iterations):
type ChatModelAgentState struct {
// All messages in the current session
Messages []*schema.Message
// Tool definitions passed to the model (via model.WithTools), can be modified in BeforeModelRewriteState
ToolInfos []*schema.ToolInfo
// Deferred retrieval tool definitions (via model.WithDeferredTools), used for the model's native search capability
// nil when not in use
DeferredToolInfos []*schema.ToolInfo
}
💡 The recommended place to modify
ToolInfos/DeferredToolInfosisBeforeModelRewriteState— this is the source of truth for tool configuration. Do not modify the tool list inWrapModel.
ModelContext
Context for WrapModel and Before/AfterModelRewriteState:
type ModelContext struct {
// Deprecated: use ChatModelAgentState.ToolInfos instead
Tools []*schema.ToolInfo
// Model retry configuration
ModelRetryConfig *ModelRetryConfig
// Model failover configuration
ModelFailoverConfig *ModelFailoverConfig[*schema.Message]
}
ToolContext
Metadata for tool wrapping:
type ToolContext struct {
Name string // Tool name
CallID string // Unique identifier for this call
}
Tool Call Endpoint Types
Tool wrapping uses function types rather than interfaces. The framework calls the corresponding Wrap method based on the interface the tool implements:
// Standard tools
type InvokableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error)
type StreamableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error)
// Enhanced tools (using ToolArgument/ToolResult)
type EnhancedInvokableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error)
type EnhancedStreamableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error)
💡 Each Wrap method is only called when the tool implements the corresponding interface. For example, if a tool only implements
InvokableTool, onlyWrapInvokableToolCallwill be called —WrapStreamableToolCallwill not.
Execution Order
Model Call Lifecycle (from outer to inner)
AgentMiddleware.BeforeChatModel- ChatModelAgentMiddleware.BeforeModelRewriteState
retryModelWrapper(internal — retry on failure)eventSenderModelWrapperpreprocessing (internal — prepare event sending)- ChatModelAgentMiddleware.WrapModel preprocessing (first registered → runs first)
callbackInjectionModelWrapper(internal)- Model.Generate / Stream
callbackInjectionModelWrapperpostprocessing- ChatModelAgentMiddleware.WrapModel postprocessing (first registered → runs last)
eventSenderModelWrapperpostprocessingretryModelWrapperpostprocessing- ChatModelAgentMiddleware.AfterModelRewriteState
AgentMiddleware.AfterChatModel
Tool Call Lifecycle (from outer to inner)
eventSenderToolHandler(internal — sends tool result event)ToolsConfig.ToolCallMiddlewaresAgentMiddleware.WrapToolCall- ChatModelAgentMiddleware.WrapXxxToolCall (first registered → outermost)
- Tool.InvokableRun / StreamableRun
WrapModel Usage Recommendations
| ✅ Recommended Uses | ❌ Not Recommended |
| Model call retry logic | Modifying input messages (not persisted, breaks prompt cache) |
| Model failover (backup models) | Modifying tool list (should modify state.ToolInfosin BeforeModelRewriteState) |
| Sending custom events (e.g., streaming progress) | |
| Processing/transforming response streams, modifying call parameters |
Run-Local Storage API
Store and retrieve key-value pairs during the current agent Run(). Values are compatible with interrupt/resume — they are serialized and persisted with checkpoints.
func SetRunLocalValue(ctx context.Context, key string, value any) error
func GetRunLocalValue(ctx context.Context, key string) (any, bool, error)
func DeleteRunLocalValue(ctx context.Context, key string) error
💡 Custom types must be registered in
init()viaschema.RegisterName[T]()to ensure correct gob serialization. These functions can only be called withinChatModelAgentMiddlewarecallbacks.
Example: Sharing State Across Callbacks
func init() {
schema.RegisterName[*ToolStats]("mypackage.ToolStats")
}
type ToolStats struct {
Count int
Name string
}
type MyMiddleware struct {
*adk.BaseChatModelAgentMiddleware
}
// Record stats after tool call
func (m *MyMiddleware) WrapInvokableToolCall(ctx context.Context, endpoint adk.InvokableToolCallEndpoint, tCtx *adk.ToolContext) (adk.InvokableToolCallEndpoint, error) {
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
result, err := endpoint(ctx, args, opts...)
_ = adk.SetRunLocalValue(ctx, "last_tool", &ToolStats{Count: 1, Name: tCtx.Name})
return result, err
}, nil
}
// Read stats after model call
func (m *MyMiddleware) AfterModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, mc *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) {
if val, found, _ := adk.GetRunLocalValue(ctx, "last_tool"); found {
if stats, ok := val.(*ToolStats); ok {
log.Printf("Last tool: %s (count=%d)", stats.Name, stats.Count)
}
}
return ctx, state, nil
}
SendEvent API
Send a custom AgentEvent to the event stream during agent execution. Callers iterating over the event stream will receive it:
func SendEvent(ctx context.Context, event *AgentEvent) error
Can only be called within ChatModelAgentMiddleware callbacks.
State Type
💡
Stateis kept exported only for checkpoint backward compatibility. Do not use it directly — useChatModelAgentStateinChatModelAgentMiddlewarecallbacks, and replace the formerState.ExtrawithSetRunLocalValue/GetRunLocalValue. Usage ofcompose.ProcessState[*State]will stop working in v1.0.0.
Migration Guide
Migrating from compose.ProcessState[*State]
Before:
compose.ProcessState(ctx, func(_ context.Context, st *adk.State) error {
st.Extra["myKey"] = myValue
return nil
})
After:
// Write
if err := adk.SetRunLocalValue(ctx, "myKey", myValue); err != nil {
return ctx, state, err
}
// Read
if val, found, err := adk.GetRunLocalValue(ctx, "myKey"); err == nil && found {
// use val
}
Adapting to AfterAgent (new in v0.9)
AfterAgent is called after the agent terminates successfully (final answer or return-directly tool result), and can be used for post-processing:
func (m *MyMiddleware) AfterAgent(ctx context.Context, state *adk.ChatModelAgentState) (context.Context, error) {
log.Printf("Agent completed with %d messages", len(state.Messages))
// Auditing, statistics, cleanup, etc.
return ctx, nil
}
💡
AfterAgentis called in registration order (same asBeforeAgent). If any handler returns an error, subsequent handlers are not called (fail-fast), and the error is sent to the event stream.
Adapting to ToolInfos / DeferredToolInfos (new in v0.9)
ChatModelAgentState added ToolInfos and DeferredToolInfos fields, replacing ModelContext.Tools as the source of truth for tool configuration:
func (m *MyMiddleware) BeforeModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, mc *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) {
// Dynamically filter tools
filtered := make([]*schema.ToolInfo, 0, len(state.ToolInfos))
for _, t := range state.ToolInfos {
if shouldInclude(t.Name) {
filtered = append(filtered, t)
}
}
state.ToolInfos = filtered
return ctx, state, nil
}