Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d284118
refactor(adk): migrate from agent middleware to handlers
shentongmartin Dec 30, 2025
0b349e5
refactor(adk): rename handler interfaces for clarity and consistency
shentongmartin Dec 30, 2025
6e03599
refactor(adk): move BeforeAgent to runtime and support dynamic tools …
shentongmartin Jan 4, 2026
647bf43
fix(adk): return error instead of silently ignoring tool.Info failure…
shentongmartin Jan 4, 2026
6797bba
docs(adk): restore documentation comments for exported types and func…
shentongmartin Jan 4, 2026
ddac2cd
refactor(adk): simplify AgentHandler interface
shentongmartin Jan 4, 2026
5bfe310
refactor(adk): replace GetToolMiddleware with ToolCallHandler interface
shentongmartin Jan 4, 2026
5910cb9
refactor(adk): rename AgentConfig to AgentRunContext and clean up
shentongmartin Jan 4, 2026
4ff8b2d
refactor(adk): rename ToolCallHandler to ToolCallWrapper and fix Befo…
shentongmartin Jan 4, 2026
0599f9e
refactor(adk): move RuntimeReturnDirectly from context to State
shentongmartin Jan 4, 2026
6082fb3
refactor(adk): decouple AgentMiddleware from AgentHandler interface
shentongmartin Jan 5, 2026
55689c9
fix(adk): use getRunFunc in Resume for correct graph topology
shentongmartin Jan 5, 2026
8a50fd3
perf(adk): pre-compute middleware contributions at instantiation time
shentongmartin Jan 5, 2026
3427b51
test(adk): add test for dynamic tool execution
shentongmartin Jan 5, 2026
262481f
fix(adk): include middleware tools in buildRunFunc graph topology dec…
shentongmartin Jan 5, 2026
72ad562
perf(adk): conditionally add return-directly branch based on tool con…
shentongmartin Jan 5, 2026
4ceaeba
refactor(adk): move initialTools computation to prepareBuildContext
shentongmartin Jan 5, 2026
b5e6223
refactor(adk): remove redundant middleware pre-computed fields
shentongmartin Jan 5, 2026
0d11723
refactor(adk): improve buildNoToolsRunFunc code style and fix message…
shentongmartin Jan 6, 2026
ca6ba1c
perf(adk): cache buildContext to avoid redundant computation
shentongmartin Jan 6, 2026
e6e5f5c
perf(adk): add fast path in getRunFunc when no handlers
shentongmartin Jan 6, 2026
15dc865
refactor(adk): dissolve graphConfig type into buildContext
shentongmartin Jan 6, 2026
b89549a
refactor(adk): remove derived fields from buildContext
shentongmartin Jan 6, 2026
93ae691
refactor(adk): inline hasReturnDirectlyTool and isRuntimeCompatible
shentongmartin Jan 7, 2026
3e247d1
refactor(adk): make runFunc a template accepting instruction, returnD…
shentongmartin Jan 7, 2026
7ddd1ab
refactor(adk): remove initialTools field from buildContext
shentongmartin Jan 7, 2026
f594f5b
refactor(adk): change returnDirectly maps from map[string]bool to map…
shentongmartin Jan 7, 2026
fe04542
refactor(adk): simplify reactConfig and encapsulate runtime values in…
shentongmartin Jan 7, 2026
5650246
refactor(adk): return new buildContext from applyBeforeAgent instead …
shentongmartin Jan 7, 2026
d81da1f
refactor(adk): remove ToolMeta and put tools/returnDirectly directly …
shentongmartin Jan 7, 2026
c183ee4
refactor(adk): simplify buildContext and getRunFunc
shentongmartin Jan 7, 2026
1a50c49
refactor(adk): move error/interrupt handling from callbacks to runFunc
shentongmartin Jan 7, 2026
8838655
refactor(adk): move tool result sending from callbacks to middleware …
shentongmartin Jan 7, 2026
d94ffe2
refactor(adk): replace ToolsNode callbacks with StreamStatePostHandler
shentongmartin Jan 7, 2026
eddf345
refactor: replace callback-based event sending with ModelCallWrapper …
shentongmartin Jan 8, 2026
e9edf35
refactor(adk): add callback injection for models without IsCallbacksE…
shentongmartin Jan 8, 2026
1c44f8d
refactor(adk): move chain instantiation outside runFunc
shentongmartin Jan 8, 2026
eb08a57
refactor(adk): fix ReturnDirectlyEvent persistence on interrupt
shentongmartin Jan 8, 2026
b6e7a71
refactor(adk): rename wrap_chatmodel.go to wrappers.go and consolidat…
shentongmartin Jan 8, 2026
74123f0
refactor(adk): replace slices.Clone with cloneSlice for Go 1.18 compa…
shentongmartin Jan 12, 2026
08b1220
refactor(adk): improve test coverage for chatmodel.go, wrappers.go, a…
shentongmartin Jan 12, 2026
46191e9
refactor(adk): improve error messages and add middleware order docume…
shentongmartin Jan 12, 2026
ba420f4
refactor(adk): embed wrapper interfaces in AgentHandler for consistency
shentongmartin Jan 13, 2026
65763e4
docs(adk): add comprehensive documentation for AgentHandler vs AgentM…
shentongmartin Jan 13, 2026
ed28f4e
refactor(adk): replace ModelCallWrapper interface with WrapModel meth…
shentongmartin Jan 13, 2026
7c65de8
refactor(adk): replace ToolCallWrapper interface with WrapTool method…
shentongmartin Jan 13, 2026
c80c5ac
refactor(adk): change BaseAgentHandler to pointer receiver
shentongmartin Jan 13, 2026
a90d222
refactor(adk): rename AgentHandler to HandlerMiddleware
shentongmartin Jan 13, 2026
9dc7807
refactor(adk): implement cached reflection for HandlerMiddleware
shentongmartin Jan 13, 2026
c663b19
refactor(adk): remove handler helper functions
shentongmartin Jan 13, 2026
14f4248
refactor(adk): remove dead code in retryChatModel
shentongmartin Jan 14, 2026
2bae96b
refactor(adk): optimize WrapTool to construction-time wrapping
shentongmartin Jan 14, 2026
ec65a64
refactor(adk): convert BeforeModelRewriteHistory/AfterModelRewriteHis…
shentongmartin Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
894 changes: 439 additions & 455 deletions adk/chatmodel.go

Large diffs are not rendered by default.

118 changes: 116 additions & 2 deletions adk/chatmodel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"

"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
mockModel "github.com/cloudwego/eino/internal/mock/components/model"
Expand Down Expand Up @@ -456,8 +457,8 @@ func TestParallelReturnDirectlyToolCall(t *testing.T) {
&myTool{name: "tool3", desc: "tool3", waitTime: 100 * time.Millisecond},
},
},
ReturnDirectly: map[string]bool{
"tool1": true,
ReturnDirectly: map[string]struct{}{
"tool1": {},
},
},
})
Expand Down Expand Up @@ -1183,3 +1184,116 @@ func (s *simpleToolForMiddlewareTest) InvokableRun(_ context.Context, _ string,
func (s *simpleToolForMiddlewareTest) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) {
return schema.StreamReaderFromArray([]string{s.result}), nil
}

func TestGetComposeOptions(t *testing.T) {
t.Run("WithChatModelOptions", func(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
cm := mockModel.NewMockToolCallingChatModel(ctrl)

var capturedTemperature float32
cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) {
options := model.GetCommonOptions(&model.Options{}, opts...)
if options.Temperature != nil {
capturedTemperature = *options.Temperature
}
return schema.AssistantMessage("response", nil), nil
}).Times(1)

agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
Name: "TestAgent",
Description: "Test agent",
Model: cm,
})
assert.NoError(t, err)

temp := float32(0.7)
iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}},
WithChatModelOptions([]model.Option{model.WithTemperature(temp)}))
for {
_, ok := iter.Next()
if !ok {
break
}
}

assert.Equal(t, temp, capturedTemperature, "Temperature should be passed through WithChatModelOptions")
})

t.Run("WithToolOptions", func(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
cm := mockModel.NewMockToolCallingChatModel(ctrl)

var toolOptionsCaptured bool
testTool := &toolOptionCapturingTool{
name: "test_tool",
onRun: func(opts []tool.Option) {
if len(opts) > 0 {
toolOptionsCaptured = true
}
},
}
info, _ := testTool.Info(ctx)

cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes()
cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
Return(schema.AssistantMessage("Using tool", []schema.ToolCall{
{ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}},
}), nil).Times(1)
cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
Return(schema.AssistantMessage("done", nil), nil).Times(1)

agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
Name: "TestAgent",
Description: "Test agent",
Model: cm,
ToolsConfig: ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{testTool},
},
},
})
assert.NoError(t, err)

iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}},
WithToolOptions([]tool.Option{testToolOption("test_value")}))
for {
_, ok := iter.Next()
if !ok {
break
}
}

assert.True(t, toolOptionsCaptured, "Tool options should be passed through WithToolOptions")
})


}

type toolOptionCapturingTool struct {
name string
onRun func(opts []tool.Option)
}

func (t *toolOptionCapturingTool) Info(_ context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{Name: t.name, Desc: t.name + " description"}, nil
}

func (t *toolOptionCapturingTool) InvokableRun(_ context.Context, _ string, opts ...tool.Option) (string, error) {
if t.onRun != nil {
t.onRun(opts)
}
return t.name + " result", nil
}

type testToolOptions struct {
value string
}

func testToolOption(value string) tool.Option {
return tool.WrapImplSpecificOptFn(func(o *testToolOptions) {
o.value = value
})
}
139 changes: 139 additions & 0 deletions adk/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package adk

import (
"context"
"reflect"

"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
)

// AgentContext contains runtime information passed to handlers before each agent run.
// Handlers can modify Instruction, Tools, and ReturnDirectly to customize agent behavior.
type AgentContext struct {
Instruction string
Tools []tool.BaseTool
ReturnDirectly map[string]struct{}
}

// HandlerMiddleware defines the interface for customizing agent behavior.
//
// Why HandlerMiddleware instead of AgentMiddleware?
//
// AgentMiddleware is a struct type, which has inherent limitations:
// - Struct types are closed: users cannot add new methods to extend functionality
// - The framework only recognizes AgentMiddleware's fixed fields, so even if users
// embed AgentMiddleware in a custom struct and add methods, the framework cannot
// call those methods (config.Middlewares is []AgentMiddleware, not a user type)
// - Callbacks in AgentMiddleware only return error, cannot return modified context
//
// HandlerMiddleware is an interface type, which is open for extension:
// - Users can implement custom handlers with arbitrary internal state and methods
// - All methods return (context.Context, ..., error), allowing context propagation
// - Configuration is centralized in struct fields rather than scattered in closures
//
// HandlerMiddleware vs AgentMiddleware:
// - Use AgentMiddleware for simple, static additions (extra instruction/tools)
// - Use HandlerMiddleware for dynamic behavior, context modification, or call wrapping
// - AgentMiddleware is kept for backward compatibility with existing users
// - Both can be used together; middlewares are applied first, then handlers
//
// Use *BaseHandlerMiddleware as an embedded struct to provide default no-op
// implementations for all methods.
type HandlerMiddleware interface {
// BeforeAgent is called before each agent run, allowing modification of
// the agent's instruction and tools configuration.
BeforeAgent(ctx context.Context, runCtx *AgentContext) (context.Context, *AgentContext, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent 运行前切面要基于这个定义吗?但我看这里没有 agent input 相关信息

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你加上就行,我只加了之前有的


// BeforeModelRewriteHistory is called before each model invocation.
// The returned messages are persisted to the agent's internal state and passed to the model.
// The returned context is propagated to the model call and subsequent handlers.
BeforeModelRewriteHistory(ctx context.Context, messages []Message) (context.Context, []Message, error)

// AfterModelRewriteHistory is called after each model invocation.
// The input messages include the model's response as the last message.
// The returned messages are persisted to the agent's internal state.
AfterModelRewriteHistory(ctx context.Context, messages []Message) (context.Context, []Message, error)

// WrapTool wraps a tool with custom behavior.
// Return the input tool unchanged if no wrapping is needed.
// Called at construction time (or after BeforeAgent if tools are modified dynamically).
WrapTool(ctx context.Context, t tool.BaseTool) (tool.BaseTool, error)

// WrapModel wraps a chat model with custom behavior.
// Return the input model unchanged if no wrapping is needed.
// Called once when the agent is built, not per-call.
WrapModel(ctx context.Context, m model.BaseChatModel) (model.BaseChatModel, error)
}

// BaseHandlerMiddleware provides default no-op implementations for HandlerMiddleware.
// Embed *BaseHandlerMiddleware in custom handlers to only override the methods you need.
type BaseHandlerMiddleware struct{}

func (b *BaseHandlerMiddleware) WrapTool(_ context.Context, t tool.BaseTool) (tool.BaseTool, error) {
return t, nil
}

func (b *BaseHandlerMiddleware) WrapModel(_ context.Context, m model.BaseChatModel) (model.BaseChatModel, error) {
return m, nil
}

func (b *BaseHandlerMiddleware) BeforeAgent(ctx context.Context, runCtx *AgentContext) (context.Context, *AgentContext, error) {
return ctx, runCtx, nil
}

func (b *BaseHandlerMiddleware) BeforeModelRewriteHistory(ctx context.Context, messages []Message) (context.Context, []Message, error) {
return ctx, messages, nil
}

func (b *BaseHandlerMiddleware) AfterModelRewriteHistory(ctx context.Context, messages []Message) (context.Context, []Message, error) {
return ctx, messages, nil
}

type handlerInfo struct {
handler HandlerMiddleware
hasBeforeAgent bool
hasBeforeModelRewriteHistory bool
hasAfterModelRewriteHistory bool
hasWrapTool bool
hasWrapModel bool
}

var baseHandlerMiddlewareType = reflect.TypeOf(&BaseHandlerMiddleware{})

func isMethodOverridden(handler HandlerMiddleware, methodName string) bool {
handlerType := reflect.TypeOf(handler)
handlerMethod, ok1 := handlerType.MethodByName(methodName)
baseMethod, ok2 := baseHandlerMiddlewareType.MethodByName(methodName)
if !ok1 || !ok2 {
return true
}
return handlerMethod.Func.Pointer() != baseMethod.Func.Pointer()
}

func newHandlerInfo(h HandlerMiddleware) handlerInfo {
return handlerInfo{
handler: h,
hasBeforeAgent: isMethodOverridden(h, "BeforeAgent"),
hasBeforeModelRewriteHistory: isMethodOverridden(h, "BeforeModelRewriteHistory"),
hasAfterModelRewriteHistory: isMethodOverridden(h, "AfterModelRewriteHistory"),
hasWrapTool: isMethodOverridden(h, "WrapTool"),
hasWrapModel: isMethodOverridden(h, "WrapModel"),
}
}
Loading