| name | eino-adk-tool-middleware |
| description | Eino ADK WrapInvokableToolCall middleware pattern for intercepting all tool calls. TRIGGER when: implementing tool-level middleware (sandbox, logging, auth, metrics) in this project's Eino ADK agents. DO NOT TRIGGER when: working with agent-level middleware (timeout, retry) or non-Eino tool systems. |
| origin | learned |
| tags | ["eino","adk","middleware","tool","go"] |
Eino ADK Tool Middleware Pattern
How to implement custom middleware that intercepts every tool invocation in an Eino ADK ChatModelAgent.
When to Activate
- Adding a new cross-cutting concern to all tool calls (security, logging, metrics, caching)
- Need to wrap or replace tool execution for specific tool names
- Building isolation/sandbox/audit layers around tool calls
- The project uses
github.com/cloudwego/eino/adk with ChatModelAgent
Solution / Pattern
Implement adk.ChatModelAgentMiddleware by embedding *adk.BaseChatModelAgentMiddleware and overriding WrapInvokableToolCall:
package agentdef
import (
"context"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
)
var _ adk.ChatModelAgentMiddleware = (*myMiddleware)(nil)
type myMiddleware struct {
*adk.BaseChatModelAgentMiddleware
}
func (m *myMiddleware) 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...)
return result, err
}, nil
}
Registration in resolveADKHandlers
Register the middleware in pkg/agentdef/middlewares.go:
func resolveADKHandlers(ctx context.Context, specs []MiddlewareSpec, ...) ([]adk.ChatModelAgentMiddleware, error) {
var handlers []adk.ChatModelAgentMiddleware
if someCondition {
handlers = append(handlers, &myMiddleware{
BaseChatModelAgentMiddleware: &adk.BaseChatModelAgentMiddleware{},
})
}
return handlers, nil
}
Injection via ChatModelAgentConfig.Handlers
The middleware list is passed to ADK during agent construction:
adkAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: def.Metadata.Name,
Model: chatModel,
Handlers: adkHandlers,
})
Two-Tier Execution Strategy
For middleware that needs different behavior per tool type:
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) {
if toolsRequiringFullIsolation[tCtx.Name] {
return m.backend.Execute(ctx, tCtx.Name, args)
}
return m.executeWrapped(ctx, endpoint, tCtx.Name, args, opts...)
}, nil
}
Key Interface Details (eino v0.9.0-beta.1)
type InvokableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error)
type ToolContext struct {
Name string
CallID string
}
type BaseChatModelAgentMiddleware = TypedBaseChatModelAgentMiddleware[*schema.Message]
Notes
BaseChatModelAgentMiddleware provides no-op defaults for all interface methods; only override what you need.
- Middleware order matters: first registered = outermost wrapper.
WrapInvokableToolCall is called per-tool-call at request time, not at agent construction time.
- For streaming tools, override
WrapStreamableToolCall instead.
- The
resolveADKHandlers function in this project accepts extra parameters (sandbox spec, tool refs) to enable auto-injection logic.