/* * 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" "encoding/json" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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" "github.com/cloudwego/eino/schema" ) // TestChatModelAgentRun tests the Run method of ChatModelAgent func TestChatModelAgentRun(t *testing.T) { // Basic test for Run method t.Run("BasicFunctionality", func(t *testing.T) { ctx := context.Background() // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Set up expectations for the mock model cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil). Times(1) // Create a ChatModelAgent agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for unit testing", Instruction: "You are a helpful assistant.", Model: cm, }) assert.NoError(t, err) assert.NotNil(t, agent) // Run the agent input := &AgentInput{ Messages: []Message{ schema.UserMessage("Hello, who are you?"), }, } iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get the event from the iterator event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.Nil(t, event.Err) assert.NotNil(t, event.Output) assert.NotNil(t, event.Output.MessageOutput) // Verify the message content msg := event.Output.MessageOutput.Message assert.NotNil(t, msg) assert.Equal(t, "Hello, I am an AI assistant.", msg.Content) // No more events _, ok = iterator.Next() assert.False(t, ok) }) t.Run("BasicChatModelWithAgentMiddleware", func(t *testing.T) { ctx := context.Background() // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Set up expectations for the mock model cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil). Times(1) afterChatModelExecuted := false // Create a ChatModelAgent agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for unit testing", Instruction: "You are a helpful assistant.", Model: cm, Middlewares: []AgentMiddleware{ { BeforeChatModel: func(ctx context.Context, state *ChatModelAgentState) error { state.Messages = append(state.Messages, schema.UserMessage("m")) return nil }, AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { assert.Len(t, state.Messages, 4) afterChatModelExecuted = true return nil }, }, }, }) assert.NoError(t, err) assert.NotNil(t, agent) // Run the agent input := &AgentInput{ Messages: []Message{ schema.UserMessage("Hello, who are you?"), }, } iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get the event from the iterator event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.Nil(t, event.Err) assert.NotNil(t, event.Output) assert.NotNil(t, event.Output.MessageOutput) // Verify the message content msg := event.Output.MessageOutput.Message assert.NotNil(t, msg) assert.Equal(t, "Hello, I am an AI assistant.", msg.Content) // No more events _, ok = iterator.Next() assert.False(t, ok) assert.True(t, afterChatModelExecuted) }) t.Run("AfterChatModel_NoTools_ModifyDoesNotAffectEvent", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("original content", nil), nil). Times(1) var capturedMessages []*schema.Message agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for AfterChatModel NoTools scenario", Instruction: "You are a helpful assistant.", Model: cm, Middlewares: []AgentMiddleware{ { AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { capturedMessages = make([]*schema.Message, len(state.Messages)) copy(capturedMessages, state.Messages) state.Messages = append(state.Messages, schema.AssistantMessage("appended content", nil)) return nil }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("Hello")}, } iterator := agent.Run(ctx, input) event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.Nil(t, event.Err) assert.NotNil(t, event.Output) assert.NotNil(t, event.Output.MessageOutput) msg := event.Output.MessageOutput.Message assert.NotNil(t, msg) assert.Equal(t, "original content", msg.Content) _, ok = iterator.Next() assert.False(t, ok) assert.Len(t, capturedMessages, 3) }) t.Run("AfterChatModel_ReAct_ModifyAffectsFlow", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("first response with tool call", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: "test_tool", Arguments: "{}"}}, }), nil } return schema.AssistantMessage("final response", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() toolCalled := false testTool := &fakeToolForTest{tarCount: 0} agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for AfterChatModel ReAct scenario", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{testTool}, }, }, Middlewares: []AgentMiddleware{ { AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { lastMsg := state.Messages[len(state.Messages)-1] if len(lastMsg.ToolCalls) > 0 { toolCalled = true state.Messages[len(state.Messages)-1] = schema.AssistantMessage("modified to remove tool call", nil) } return nil }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("Hello")}, } iterator := agent.Run(ctx, input) var events []*AgentEvent for { event, ok := iterator.Next() if !ok { break } events = append(events, event) } assert.True(t, toolCalled) assert.Equal(t, 1, generateCount) assert.Equal(t, 1, len(events)) event := events[0] assert.NotNil(t, event.Output) assert.NotNil(t, event.Output.MessageOutput) assert.Equal(t, "first response with tool call", event.Output.MessageOutput.Message.Content) assert.Len(t, event.Output.MessageOutput.Message.ToolCalls, 1) }) t.Run("AfterChatModel_ReAct_AppendToolCall_AffectsFlow", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("first response no tool", nil), nil } return schema.AssistantMessage("final response", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() testTool := &fakeToolForTest{tarCount: 0} agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for AfterChatModel ReAct append tool call", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{testTool}, }, }, Middlewares: []AgentMiddleware{ { AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { if generateCount == 1 { state.Messages[len(state.Messages)-1] = schema.AssistantMessage("modified with tool call", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: "test_tool", Arguments: "{}"}}, }) } return nil }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("Hello")}, } iterator := agent.Run(ctx, input) var events []*AgentEvent for { event, ok := iterator.Next() if !ok { break } events = append(events, event) } assert.Equal(t, 2, generateCount) assert.Equal(t, 3, len(events)) event0 := events[0] assert.NotNil(t, event0.Output) assert.NotNil(t, event0.Output.MessageOutput) assert.Equal(t, "first response no tool", event0.Output.MessageOutput.Message.Content) assert.Empty(t, event0.Output.MessageOutput.Message.ToolCalls) event2 := events[2] assert.NotNil(t, event2.Output) assert.NotNil(t, event2.Output.MessageOutput) assert.Equal(t, "final response", event2.Output.MessageOutput.Message.Content) }) // Test with streaming output t.Run("StreamOutput", func(t *testing.T) { ctx := context.Background() // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Create a stream reader for the mock response sr := schema.StreamReaderFromArray([]*schema.Message{ schema.AssistantMessage("Hello", nil), schema.AssistantMessage(", I am", nil), schema.AssistantMessage(" an AI assistant.", nil), }) // Set up expectations for the mock model cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). Return(sr, nil). Times(1) // Create a ChatModelAgent agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for unit testing", Instruction: "You are a helpful assistant.", Model: cm, }) assert.NoError(t, err) assert.NotNil(t, agent) // Run the agent with streaming enabled input := &AgentInput{ Messages: []Message{schema.UserMessage("Hello, who are you?")}, EnableStreaming: true, } iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get the event from the iterator event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.Nil(t, event.Err) assert.NotNil(t, event.Output) assert.NotNil(t, event.Output.MessageOutput) assert.True(t, event.Output.MessageOutput.IsStreaming) // No more events _, ok = iterator.Next() assert.False(t, ok) }) // Test error handling t.Run("ErrorHandling", func(t *testing.T) { ctx := context.Background() // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Set up expectations for the mock model to return an error cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(nil, errors.New("model error")). Times(1) // Create a ChatModelAgent agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for unit testing", Instruction: "You are a helpful assistant.", Model: cm, }) assert.NoError(t, err) assert.NotNil(t, agent) // Run the agent input := &AgentInput{ Messages: []Message{schema.UserMessage("Hello, who are you?")}, } iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get the event from the iterator, should contain an error event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.NotNil(t, event.Err) assert.Contains(t, event.Err.Error(), "model error") // No more events _, ok = iterator.Next() assert.False(t, ok) }) // Test with tools t.Run("WithTools", func(t *testing.T) { ctx := context.Background() // Create a fake tool for testing fakeTool := &fakeToolForTest{ tarCount: 1, } info, err := fakeTool.Info(ctx) assert.NoError(t, err) // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Set up expectations for the mock model cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ { ID: "tool-call-1", Function: schema.FunctionCall{ Name: info.Name, Arguments: `{"name": "test user"}`, }, }}), nil). Times(1) cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("Task completed", nil), nil). Times(1) cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() // Create a ChatModelAgent with tools agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for unit testing", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{fakeTool}, }, }, }) assert.NoError(t, err) assert.NotNil(t, agent) // Run the agent input := &AgentInput{ Messages: []Message{schema.UserMessage("Use the test tool")}, } iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get events from the iterator // First event should be the model output with tool call event1, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event1) assert.Nil(t, event1.Err) assert.NotNil(t, event1.Output) assert.NotNil(t, event1.Output.MessageOutput) assert.Equal(t, schema.Assistant, event1.Output.MessageOutput.Role) // Second event should be the tool output event2, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event2) assert.Nil(t, event2.Err) assert.NotNil(t, event2.Output) assert.NotNil(t, event2.Output.MessageOutput) assert.Equal(t, schema.Tool, event2.Output.MessageOutput.Role) // Third event should be the final model output event3, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event3) assert.Nil(t, event3.Err) assert.NotNil(t, event3.Output) assert.NotNil(t, event3.Output.MessageOutput) assert.Equal(t, schema.Assistant, event3.Output.MessageOutput.Role) // No more events _, ok = iterator.Next() assert.False(t, ok) }) t.Run("WrapToolCall_ToolMiddleware", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) fakeTool := &fakeToolForTest{tarCount: 1} info, err := fakeTool.Info(ctx) assert.NoError(t, err) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("calling tool", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}}, }), nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() var capturedToolName string var capturedArgs string middlewareInvoked := false agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test WrapToolCall middleware", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{fakeTool}, }, }, Middlewares: []AgentMiddleware{ { WrapToolCall: compose.ToolMiddleware{ Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { middlewareInvoked = true capturedToolName = input.Name capturedArgs = input.Arguments return next(ctx, input) } }, }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("use tool")}, } iterator := agent.Run(ctx, input) var events []*AgentEvent for { event, ok := iterator.Next() if !ok { break } events = append(events, event) } assert.True(t, middlewareInvoked, "WrapToolCall middleware should have been invoked") assert.Equal(t, info.Name, capturedToolName) assert.Equal(t, `{"name":"test"}`, capturedArgs) assert.Equal(t, 3, len(events)) assert.Equal(t, schema.Assistant, events[0].Output.MessageOutput.Role) assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) assert.Equal(t, schema.Assistant, events[2].Output.MessageOutput.Role) assert.Equal(t, "done", events[2].Output.MessageOutput.Message.Content) }) t.Run("WrapToolCall_MultipleMiddlewares_Order", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) fakeTool := &fakeToolForTest{tarCount: 1} info, err := fakeTool.Info(ctx) assert.NoError(t, err) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("calling tool", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}}, }), nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() var order []string agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test WrapToolCall middleware order", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{fakeTool}, }, }, Middlewares: []AgentMiddleware{ { WrapToolCall: compose.ToolMiddleware{ Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { order = append(order, "mw1_before") out, err := next(ctx, input) order = append(order, "mw1_after") return out, err } }, }, }, { WrapToolCall: compose.ToolMiddleware{ Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { order = append(order, "mw2_before") out, err := next(ctx, input) order = append(order, "mw2_after") return out, err } }, }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("use tool")}, } iterator := agent.Run(ctx, input) for { _, ok := iterator.Next() if !ok { break } } // First registered middleware is outermost assert.Equal(t, []string{"mw1_before", "mw2_before", "mw2_after", "mw1_after"}, order) }) t.Run("WrapToolCall_ModifyResult", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) fakeTool := &fakeToolForTest{tarCount: 1} info, err := fakeTool.Info(ctx) assert.NoError(t, err) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("calling tool", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}}, }), nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test WrapToolCall modify result", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{fakeTool}, }, }, Middlewares: []AgentMiddleware{ { WrapToolCall: compose.ToolMiddleware{ Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { out, err := next(ctx, input) if err != nil { return out, err } out.Result = "modified: " + out.Result return out, nil } }, }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("use tool")}, } iterator := agent.Run(ctx, input) var events []*AgentEvent for { event, ok := iterator.Next() if !ok { break } events = append(events, event) } assert.Equal(t, 3, len(events)) // Tool result event should have the modified content toolEvent := events[1] assert.Equal(t, schema.Tool, toolEvent.Output.MessageOutput.Role) assert.Contains(t, toolEvent.Output.MessageOutput.Message.Content, "modified:") }) t.Run("WrapToolCall_StreamableMiddleware", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) streamTool := &fakeStreamToolForTest{tarCount: 1} info, err := streamTool.Info(ctx) assert.NoError(t, err) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("calling tool", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}}, }), nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() middlewareInvoked := false agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test Streamable WrapToolCall middleware", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{streamTool}, }, }, Middlewares: []AgentMiddleware{ { WrapToolCall: compose.ToolMiddleware{ Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { middlewareInvoked = true return next(ctx, input) } }, }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("use tool")}, } iterator := agent.Run(ctx, input) var events []*AgentEvent for { event, ok := iterator.Next() if !ok { break } events = append(events, event) } assert.True(t, middlewareInvoked, "Streamable WrapToolCall middleware should have been invoked") assert.Equal(t, 3, len(events)) assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) }) t.Run("WrapToolCall_EnhancedInvokableMiddleware", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) enhTool := &fakeEnhancedInvokableToolForTest{} info, err := enhTool.Info(ctx) assert.NoError(t, err) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("calling tool", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}}, }), nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() middlewareInvoked := false var capturedToolName string agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test EnhancedInvokable WrapToolCall middleware", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{enhTool}, }, }, Middlewares: []AgentMiddleware{ { WrapToolCall: compose.ToolMiddleware{ EnhancedInvokable: func(next compose.EnhancedInvokableToolEndpoint) compose.EnhancedInvokableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { middlewareInvoked = true capturedToolName = input.Name return next(ctx, input) } }, }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("use tool")}, } iterator := agent.Run(ctx, input) var events []*AgentEvent for { event, ok := iterator.Next() if !ok { break } events = append(events, event) } assert.True(t, middlewareInvoked, "EnhancedInvokable WrapToolCall middleware should have been invoked") assert.Equal(t, info.Name, capturedToolName) assert.Equal(t, 3, len(events)) assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) }) t.Run("WrapToolCall_EnhancedStreamableMiddleware", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) enhStreamTool := &fakeEnhancedStreamableToolForTest{} info, err := enhStreamTool.Info(ctx) assert.NoError(t, err) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return schema.AssistantMessage("calling tool", []schema.ToolCall{ {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}}, }), nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() middlewareInvoked := false agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test EnhancedStreamable WrapToolCall middleware", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{enhStreamTool}, }, }, Middlewares: []AgentMiddleware{ { WrapToolCall: compose.ToolMiddleware{ EnhancedStreamable: func(next compose.EnhancedStreamableToolEndpoint) compose.EnhancedStreamableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { middlewareInvoked = true return next(ctx, input) } }, }, }, }, }) assert.NoError(t, err) input := &AgentInput{ Messages: []Message{schema.UserMessage("use tool")}, } iterator := agent.Run(ctx, input) var events []*AgentEvent for { event, ok := iterator.Next() if !ok { break } events = append(events, event) } assert.True(t, middlewareInvoked, "EnhancedStreamable WrapToolCall middleware should have been invoked") assert.Equal(t, 3, len(events)) assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) }) } // TestExitTool tests the Exit tool functionality func TestExitTool(t *testing.T) { ctx := context.Background() // Create a mock controller ctrl := gomock.NewController(t) defer ctrl.Finish() // Create a mock chat model cm := mockModel.NewMockToolCallingChatModel(ctrl) // Set up expectations for the mock model // First call: model generates a message with Exit tool call cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("I'll exit with a final result", []schema.ToolCall{ { ID: "tool-call-1", Function: schema.FunctionCall{ Name: "exit", Arguments: `{"final_result": "This is the final result"}`}, }, }), nil). Times(1) // Model should implement WithTools cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() // Create an agent with the Exit tool agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent with Exit tool", Instruction: "You are a helpful assistant.", Model: cm, Exit: &ExitTool{}, }) assert.NoError(t, err) assert.NotNil(t, agent) // Run the agent input := &AgentInput{ Messages: []Message{ schema.UserMessage("Please exit with a final result"), }, } iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // First event: model output with tool call event1, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event1) assert.Nil(t, event1.Err) assert.NotNil(t, event1.Output) assert.NotNil(t, event1.Output.MessageOutput) assert.Equal(t, schema.Assistant, event1.Output.MessageOutput.Role) // Second event: tool output (Exit) event2, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event2) assert.Nil(t, event2.Err) assert.NotNil(t, event2.Output) assert.NotNil(t, event2.Output.MessageOutput) assert.Equal(t, schema.Tool, event2.Output.MessageOutput.Role) // Verify the action is Exit assert.NotNil(t, event2.Action) assert.True(t, event2.Action.Exit) // Verify the final result assert.Equal(t, "This is the final result", event2.Output.MessageOutput.Message.Content) // No more events _, ok = iterator.Next() assert.False(t, ok) } func TestParallelReturnDirectlyToolCall(t *testing.T) { ctx := context.Background() // Create a mock controller ctrl := gomock.NewController(t) defer ctrl.Finish() // Create a mock chat model cm := mockModel.NewMockToolCallingChatModel(ctrl) // Set up expectations for the mock model // First call: model generates a message with Exit tool call cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("I'll exit with a final result", []schema.ToolCall{ { ID: "tool-call-1", Function: schema.FunctionCall{Name: "tool1"}, }, { ID: "tool-call-2", Function: schema.FunctionCall{Name: "tool2"}, }, { ID: "tool-call-3", Function: schema.FunctionCall{Name: "tool3"}, }, }), nil). Times(1) // Model should implement WithTools cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() // Create an agent with the Exit tool agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent with Exit tool", Instruction: "You are a helpful assistant.", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{ &myTool{name: "tool1", desc: "tool1", waitTime: time.Millisecond}, &myTool{name: "tool2", desc: "tool2", waitTime: 10 * time.Millisecond}, &myTool{name: "tool3", desc: "tool3", waitTime: 100 * time.Millisecond}, }, }, ReturnDirectly: map[string]bool{ "tool1": true, }, }, }) assert.NoError(t, err) assert.NotNil(t, agent) r := NewRunner(ctx, RunnerConfig{ Agent: agent, }) iter := r.Query(ctx, "") times := 0 for { e, ok := iter.Next() if !ok { assert.Equal(t, 4, times) break } if times == 3 { assert.Equal(t, "tool1", e.Output.MessageOutput.Message.ToolName) } times++ } } func TestConcurrentSameToolSendToolGenActionUsesToolCallID(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) defer ctrl.Finish() cm := mockModel.NewMockToolCallingChatModel(ctrl) cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("tools", []schema.ToolCall{ {ID: "id1", Function: schema.FunctionCall{Name: "action_tool", Arguments: "A"}}, {ID: "id2", Function: schema.FunctionCall{Name: "action_tool", Arguments: "B"}}, }), 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: "Agent with action tool", Instruction: "", Model: cm, ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{actionTool{}}}}, }) assert.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}}) seen := map[string]bool{} for { e, ok := iter.Next() if !ok { break } if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.Message != nil && e.Output.MessageOutput.Message.Role == schema.Tool { if e.Action != nil && e.Action.CustomizedAction != nil { if s, ok := e.Action.CustomizedAction.(string); ok { seen[s] = true } } } } assert.True(t, seen["A"]) assert.True(t, seen["B"]) } type myTool struct { name string desc string waitTime time.Duration } func (m *myTool) Info(ctx context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{ Name: m.name, Desc: m.desc, }, nil } func (m *myTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { time.Sleep(m.waitTime) return "success", nil } type actionTool struct{} func (a actionTool) Info(ctx context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{Name: "action_tool", Desc: "action tool"}, nil } func (a actionTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { _ = SendToolGenAction(ctx, "action_tool", &AgentAction{CustomizedAction: argumentsInJSON}) return "ok", nil } type streamActionTool struct{} func (s streamActionTool) Info(ctx context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{Name: "action_tool_stream", Desc: "action stream tool"}, nil } func (s streamActionTool) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) { _ = SendToolGenAction(ctx, "action_tool_stream", &AgentAction{CustomizedAction: argumentsInJSON}) sr, sw := schema.Pipe[string](1) go func() { defer sw.Close() _ = sw.Send("o", nil) _ = sw.Send("k", nil) }() return sr, nil } type legacyStreamActionTool struct{} func (s legacyStreamActionTool) Info(ctx context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{Name: "legacy_action_tool_stream", Desc: "legacy action stream tool"}, nil } func (s legacyStreamActionTool) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) { _ = compose.ProcessState(ctx, func(ctx context.Context, st *State) error { st.setToolGenAction("legacy_action_tool_stream", &AgentAction{CustomizedAction: argumentsInJSON}) return nil }) sr, sw := schema.Pipe[string](1) go func() { defer sw.Close() _ = sw.Send("o", nil) _ = sw.Send("k", nil) }() return sr, nil } // TestChatModelAgentOutputKey tests the outputKey configuration and setOutputToSession function func TestChatModelAgentOutputKey(t *testing.T) { // Test outputKey configuration - stores output in session t.Run("OutputKeyStoresInSession", func(t *testing.T) { for i := 0; i < 1000; i++ { } ctx := context.Background() // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Set up expectations for the mock model cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil). Times(1) // Create a ChatModelAgent with outputKey configured agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for unit testing", Instruction: "You are a helpful assistant.", Model: cm, OutputKey: "agent_output", // This should store output in session }) assert.NoError(t, err) assert.NotNil(t, agent) // Initialize a run context to enable session storage input := &AgentInput{ Messages: []Message{ schema.UserMessage("Hello, who are you?"), }, } ctx, runCtx := initRunCtx(ctx, "TestAgent", input) assert.NotNil(t, runCtx) assert.NotNil(t, runCtx.Session) // Run the agent iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get the event from the iterator event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.Nil(t, event.Err) // Verify the message content msg := event.Output.MessageOutput.Message assert.Equal(t, "Hello, I am an AI assistant.", msg.Content) // Verify that the output was stored in the session time.AfterFunc(100*time.Millisecond, func() { sessionValues := GetSessionValues(ctx) assert.Contains(t, sessionValues, "agent_output") assert.Equal(t, "Hello, I am an AI assistant.", sessionValues["agent_output"]) }) // No more events _, ok = iterator.Next() assert.False(t, ok) }) // Test outputKey configuration with streaming - stores concatenated output in session t.Run("OutputKeyWithStreamingStoresInSession", func(t *testing.T) { ctx := context.Background() // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Create a stream reader for the mock response sr := schema.StreamReaderFromArray([]*schema.Message{ schema.AssistantMessage("Hello", nil), schema.AssistantMessage(", I am", nil), schema.AssistantMessage(" an AI assistant.", nil), }) // Set up expectations for the mock model cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). Return(sr, nil). Times(1) // Create a ChatModelAgent with outputKey configured agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent for unit testing", Instruction: "You are a helpful assistant.", Model: cm, OutputKey: "agent_output", // This should store concatenated stream in session }) assert.NoError(t, err) assert.NotNil(t, agent) // Initialize a run context to enable session storage input := &AgentInput{ Messages: []Message{schema.UserMessage("Hello, who are you?")}, EnableStreaming: true, } ctx, runCtx := initRunCtx(ctx, "TestAgent", input) assert.NotNil(t, runCtx) assert.NotNil(t, runCtx.Session) // Run the agent iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get the event from the iterator event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.Nil(t, event.Err) assert.True(t, event.Output.MessageOutput.IsStreaming) time.AfterFunc(100*time.Millisecond, func() { // Verify that the concatenated output was stored in the session sessionValues := GetSessionValues(ctx) assert.Contains(t, sessionValues, "agent_output") assert.Equal(t, "Hello, I am an AI assistant.", sessionValues["agent_output"]) }) // No more events _, ok = iterator.Next() assert.False(t, ok) }) // Test setOutputToSession function directly - regular message t.Run("SetOutputToSessionRegularMessage", func(t *testing.T) { ctx := context.Background() // Initialize a run context to enable session storage input := &AgentInput{ Messages: []Message{schema.UserMessage("test")}, } ctx, runCtx := initRunCtx(ctx, "TestAgent", input) assert.NotNil(t, runCtx) assert.NotNil(t, runCtx.Session) // Test with a regular message msg := schema.AssistantMessage("Test response", nil) err := setOutputToSession(ctx, msg, nil, "test_output") assert.NoError(t, err) // Verify the message content was stored sessionValues := GetSessionValues(ctx) assert.Contains(t, sessionValues, "test_output") assert.Equal(t, "Test response", sessionValues["test_output"]) }) // Test setOutputToSession function directly - streaming message t.Run("SetOutputToSessionStreamingMessage", func(t *testing.T) { ctx := context.Background() // Initialize a run context to enable session storage input := &AgentInput{ Messages: []Message{schema.UserMessage("test")}, } ctx, runCtx := initRunCtx(ctx, "TestAgent", input) assert.NotNil(t, runCtx) assert.NotNil(t, runCtx.Session) // Test with a streaming message sr := schema.StreamReaderFromArray([]*schema.Message{ schema.AssistantMessage("Stream", nil), schema.AssistantMessage(" response", nil), schema.AssistantMessage(" content", nil), }) err := setOutputToSession(ctx, nil, sr, "test_output") assert.NoError(t, err) // Verify the concatenated stream content was stored sessionValues := GetSessionValues(ctx) assert.Contains(t, sessionValues, "test_output") assert.Equal(t, "Stream response content", sessionValues["test_output"]) }) // Test setOutputToSession function directly - error case t.Run("SetOutputToSessionErrorCase", func(t *testing.T) { ctx := context.Background() // Initialize a run context to enable session storage input := &AgentInput{ Messages: []Message{schema.UserMessage("test")}, } ctx, runCtx := initRunCtx(ctx, "TestAgent", input) assert.NotNil(t, runCtx) assert.NotNil(t, runCtx.Session) // Test with an invalid stream (simulate error) // Create a stream that will fail when concatenated sr := schema.StreamReaderFromArray([]*schema.Message{ schema.AssistantMessage("test", nil), }) // Close the stream to simulate an error condition sr.Close() // This should return an error because the stream is closed err := setOutputToSession(ctx, nil, sr, "test_output") // Note: The actual behavior may vary depending on the stream implementation // Some streams may not error when closed, so we'll accept either outcome if err != nil { // If there's an error, verify nothing was stored sessionValues := GetSessionValues(ctx) assert.NotContains(t, sessionValues, "test_output") } else { // If no error, verify the content was stored sessionValues := GetSessionValues(ctx) assert.Contains(t, sessionValues, "test_output") assert.Equal(t, "test", sessionValues["test_output"]) } }) // Test outputKey with React workflow (tools enabled) t.Run("OutputKeyWithReactWorkflow", func(t *testing.T) { ctx := context.Background() // Create a mock chat model ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) // Create a simple tool for testing fakeTool := &fakeToolForTest{ tarCount: 1, } // Set up expectations for the mock model - it will generate a final response cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("Final response from React workflow", nil), nil). Times(1) // Model should implement WithTools cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() // Create a ChatModelAgent with outputKey and tools configured agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent with tools", Instruction: "You are a helpful assistant.", Model: cm, OutputKey: "agent_output", ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{fakeTool}, }, }, }) assert.NoError(t, err) assert.NotNil(t, agent) // Initialize a run context to enable session storage input := &AgentInput{ Messages: []Message{schema.UserMessage("Use the tool")}, } ctx, runCtx := initRunCtx(ctx, "TestAgent", input) assert.NotNil(t, runCtx) assert.NotNil(t, runCtx.Session) // Run the agent iterator := agent.Run(ctx, input) assert.NotNil(t, iterator) // Get the event from the iterator event, ok := iterator.Next() assert.True(t, ok) assert.NotNil(t, event) assert.Nil(t, event.Err) // Verify the message content msg := event.Output.MessageOutput.Message assert.Equal(t, "Final response from React workflow", msg.Content) // Verify that the output was stored in the session time.AfterFunc(time.Millisecond*10, func() { sessionValues := GetSessionValues(ctx) assert.Contains(t, sessionValues, "agent_output") assert.Equal(t, "Final response from React workflow", sessionValues["agent_output"]) }) // No more events _, ok = iterator.Next() assert.False(t, ok) }) } func TestConcurrentSameStreamToolSendToolGenActionUsesToolCallID(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) defer ctrl.Finish() cm := mockModel.NewMockToolCallingChatModel(ctrl) cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("tools", []schema.ToolCall{ {ID: "sid1", Function: schema.FunctionCall{Name: "action_tool_stream", Arguments: "SA"}}, {ID: "sid2", Function: schema.FunctionCall{Name: "action_tool_stream", Arguments: "SB"}}, }), 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: "Agent with stream action tool", Instruction: "", Model: cm, ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{streamActionTool{}}}}, }) assert.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}}) seen := map[string]bool{} for { e, ok := iter.Next() if !ok { break } if e.Output != nil && e.Output.MessageOutput != nil { if e.Output.MessageOutput.IsStreaming { if e.Action != nil && e.Action.CustomizedAction != nil { if s, ok := e.Action.CustomizedAction.(string); ok { seen[s] = true } } } } } assert.True(t, seen["SA"]) assert.True(t, seen["SB"]) } func TestStreamToolLegacyNameKeyFallback(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) defer ctrl.Finish() cm := mockModel.NewMockToolCallingChatModel(ctrl) cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("tools", []schema.ToolCall{ {ID: "lsid1", Function: schema.FunctionCall{Name: "legacy_action_tool_stream", Arguments: "LA"}}, }), 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: "Agent with legacy stream action tool", Instruction: "", Model: cm, ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{legacyStreamActionTool{}}}}, }) assert.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}}) found := false for { e, ok := iter.Next() if !ok { break } if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.IsStreaming { if e.Action != nil && e.Action.CustomizedAction != nil { if s, ok := e.Action.CustomizedAction.(string); ok { found = (s == "LA") } } } } assert.True(t, found) } func TestChatModelAgent_ToolResultMiddleware_EmitsFinalResult(t *testing.T) { originalResult := "original_result" modifiedResult := "modified_by_middleware" resultModifyingMiddleware := compose.ToolMiddleware{ Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { output, err := next(ctx, input) if err != nil { return nil, err } output.Result = modifiedResult return output, nil } }, Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { output, err := next(ctx, input) if err != nil { return nil, err } output.Result = schema.StreamReaderFromArray([]string{modifiedResult}) return output, nil } }, } t.Run("Invoke", func(t *testing.T) { ctx := context.Background() testTool := &simpleToolForMiddlewareTest{name: "test_tool", result: originalResult} ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) info, err := testTool.Info(ctx) assert.NoError(t, err) cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("", []schema.ToolCall{ { ID: "tool-call-1", Function: schema.FunctionCall{ Name: info.Name, Arguments: `{"input": "test"}`, }, }, }), nil). Times(1) cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.AssistantMessage("final response", nil), nil). Times(1) cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "test_agent", Description: "test agent with middleware", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{testTool}, ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware}, }, }, }) assert.NoError(t, err) r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false, CheckPointStore: newBridgeStore()}) it := r.Run(ctx, []Message{schema.UserMessage("call the tool")}) var toolResultEvents []*AgentEvent for { ev, ok := it.Next() if !ok { break } if ev.Output != nil && ev.Output.MessageOutput != nil && ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Role == schema.Tool { toolResultEvents = append(toolResultEvents, ev) } } assert.NotEmpty(t, toolResultEvents, "should have at least one tool result event") for _, ev := range toolResultEvents { assert.Equal(t, modifiedResult, ev.Output.MessageOutput.Message.Content, "tool result event should contain the middleware-modified result, not the original") assert.NotEqual(t, originalResult, ev.Output.MessageOutput.Message.Content, "tool result event should NOT contain the original result") } }) t.Run("Stream", func(t *testing.T) { ctx := context.Background() testTool := &simpleToolForMiddlewareTest{name: "test_tool_stream", result: originalResult} ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) info, err := testTool.Info(ctx) assert.NoError(t, err) cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.StreamReaderFromArray([]*schema.Message{ schema.AssistantMessage("", []schema.ToolCall{ { ID: "tool-call-1", Function: schema.FunctionCall{ Name: info.Name, Arguments: `{"input": "test"}`, }, }, }), }), nil). Times(1) cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). Return(schema.StreamReaderFromArray([]*schema.Message{ schema.AssistantMessage("final response", nil), }), nil). Times(1) cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "test_agent", Description: "test agent with middleware", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{testTool}, ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware}, }, }, }) assert.NoError(t, err) r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true, CheckPointStore: newBridgeStore()}) it := r.Run(ctx, []Message{schema.UserMessage("call the tool")}) var toolResultContents []string for { ev, ok := it.Next() if !ok { break } if ev.Output != nil && ev.Output.MessageOutput != nil { if ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Role == schema.Tool { toolResultContents = append(toolResultContents, ev.Output.MessageOutput.Message.Content) } if ev.Output.MessageOutput.IsStreaming && ev.Output.MessageOutput.MessageStream != nil && ev.Output.MessageOutput.Role == schema.Tool { var msgs []*schema.Message for { msg, err := ev.Output.MessageOutput.MessageStream.Recv() if err != nil { break } msgs = append(msgs, msg) } if len(msgs) > 0 { concated, err := schema.ConcatMessages(msgs) if err == nil { toolResultContents = append(toolResultContents, concated.Content) } } } } } assert.NotEmpty(t, toolResultContents, "should have at least one tool result event") for _, content := range toolResultContents { assert.Equal(t, modifiedResult, content, "tool result event should contain the middleware-modified result, not the original") assert.NotEqual(t, originalResult, content, "tool result event should NOT contain the original result") } }) } type simpleToolForMiddlewareTest struct { name string result string } func (s *simpleToolForMiddlewareTest) Info(_ context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{ Name: s.name, Desc: "simple tool", ParamsOneOf: schema.NewParamsOneOfByParams( map[string]*schema.ParameterInfo{ "input": { Desc: "input", Required: true, Type: schema.String, }, }), }, nil } func (s *simpleToolForMiddlewareTest) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { return s.result, nil } 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 }) } type errorTool struct { infoErr error } func (e *errorTool) Info(_ context.Context) (*schema.ToolInfo, error) { return nil, e.infoErr } func (e *errorTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { return "", nil } func TestChatModelAgent_PrepareExecContextError(t *testing.T) { t.Run("Run_WithToolInfoError_ReturnsError", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) expectedErr := errors.New("tool info error") errTool := &errorTool{infoErr: expectedErr} agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{errTool}, }, }, }) assert.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) event, ok := iter.Next() assert.True(t, ok) assert.NotNil(t, event.Err) assert.Contains(t, event.Err.Error(), "tool info error") _, ok = iter.Next() assert.False(t, ok) }) t.Run("Resume_WithToolInfoError_ReturnsError", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) cm := mockModel.NewMockToolCallingChatModel(ctrl) expectedErr := errors.New("tool info error for resume") errTool := &errorTool{infoErr: expectedErr} agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "Test agent", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{errTool}, }, }, }) assert.NoError(t, err) iter := agent.Resume(ctx, &ResumeInfo{ InterruptState: []byte("dummy"), EnableStreaming: false, }) event, ok := iter.Next() assert.True(t, ok) assert.NotNil(t, event.Err) assert.Contains(t, event.Err.Error(), "tool info error for resume") _, ok = iter.Next() assert.False(t, ok) }) } // fakeEnhancedInvokableToolForTest implements tool.EnhancedInvokableTool for testing. type fakeEnhancedInvokableToolForTest struct{} func (t *fakeEnhancedInvokableToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{ Name: "test_enhanced_invokable_tool", Desc: "test enhanced invokable tool", }, nil } func (t *fakeEnhancedInvokableToolForTest) InvokableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { return &schema.ToolResult{ Parts: []schema.ToolOutputPart{ {Type: schema.ToolPartTypeText, Text: "enhanced invokable result"}, }, }, nil } // fakeEnhancedStreamableToolForTest implements tool.EnhancedStreamableTool for testing. type fakeEnhancedStreamableToolForTest struct{} func (t *fakeEnhancedStreamableToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{ Name: "test_enhanced_streamable_tool", Desc: "test enhanced streamable tool", }, nil } func (t *fakeEnhancedStreamableToolForTest) StreamableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { sr := schema.StreamReaderFromArray([]*schema.ToolResult{ {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "enhanced streamable result"}}}, }) return sr, nil } func TestPreprocessComposeCheckpoint_MigrateErrorIsReturned(t *testing.T) { in := []byte("prefix\u0015" + stateGobNameV080 + "suffix") _, err := preprocessComposeCheckpoint(in) assert.Error(t, err) } func TestNewChatModelAgent_FailoverConfigValidation(t *testing.T) { ctx := context.Background() cm := &fakeChatModel{ callbacksEnabled: true, generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { return schema.AssistantMessage("ok", nil), nil }, stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok", nil)}), nil }, } t.Run("missing GetFailoverModel", func(t *testing.T) { _, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "test", Model: cm, ModelFailoverConfig: &ModelFailoverConfig[*schema.Message]{ ShouldFailover: func(context.Context, *schema.Message, error) bool { return true }, }, }) assert.Error(t, err) assert.Contains(t, err.Error(), "ModelFailoverConfig.GetFailoverModel") }) t.Run("missing ShouldFailover", func(t *testing.T) { _, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "TestAgent", Description: "test", Model: cm, ModelFailoverConfig: &ModelFailoverConfig[*schema.Message]{ GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { return cm, nil, nil }, }, }) assert.Error(t, err) assert.Contains(t, err.Error(), "ModelFailoverConfig.ShouldFailover") }) } // aliasCaptureTool captures the raw arguments JSON received by the tool. type aliasCaptureTool struct { name string params map[string]*schema.ParameterInfo receivedArgs string } func (t *aliasCaptureTool) Info(_ context.Context) (*schema.ToolInfo, error) { return &schema.ToolInfo{ Name: t.name, Desc: t.name + " tool", ParamsOneOf: schema.NewParamsOneOfByParams(t.params), }, nil } func (t *aliasCaptureTool) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { t.receivedArgs = argumentsInJSON return "ok", nil } func TestToolAliasesPropagation(t *testing.T) { t.Run("prepareExecContext_propagates_ToolAliases", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) captureTool := &aliasCaptureTool{ name: "grep", params: map[string]*schema.ParameterInfo{ "pattern": {Type: schema.String, Desc: "regex pattern"}, "path": {Type: schema.String, Desc: "search path"}, }, } cm := mockModel.NewMockToolCallingChatModel(ctrl) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return &schema.Message{ Role: schema.Assistant, ToolCalls: []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "grep", Arguments: `{"grep_content": "TODO", "path": "/src"}`, }, }, }, }, nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "test", Instruction: "test", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{captureTool}, ToolAliases: map[string]compose.ToolAliasConfig{ "grep": { ArgumentsAliases: map[string][]string{ "pattern": {"grep_content"}, }, }, }, }, }, }) require.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search for TODOs")}}) for { _, ok := iter.Next() if !ok { break } } require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called") var args map[string]any err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) require.NoError(t, err) assert.Equal(t, "TODO", args["pattern"], "alias 'grep_content' should be remapped to 'pattern'") assert.NotContains(t, args, "grep_content", "alias key should not be present after remapping") assert.Equal(t, "/src", args["path"]) }) t.Run("applyBeforeAgent_preserves_ToolAliases_when_handler_modifies_tools", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) captureTool := &aliasCaptureTool{ name: "grep", params: map[string]*schema.ParameterInfo{ "pattern": {Type: schema.String, Desc: "regex pattern"}, }, } extraTool := &aliasCaptureTool{ name: "extra_tool", params: map[string]*schema.ParameterInfo{ "input": {Type: schema.String}, }, } cm := mockModel.NewMockToolCallingChatModel(ctrl) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return &schema.Message{ Role: schema.Assistant, ToolCalls: []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "grep", Arguments: `{"grep_content": "FIXME"}`, }, }, }, }, nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() handler := &testToolsHandler{ BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, tools: []tool.BaseTool{extraTool}, } agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "test", Instruction: "test", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{captureTool}, ToolAliases: map[string]compose.ToolAliasConfig{ "grep": { ArgumentsAliases: map[string][]string{ "pattern": {"grep_content"}, }, }, }, }, }, Handlers: []ChatModelAgentMiddleware{handler}, }) require.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search for FIXMEs")}}) for { _, ok := iter.Next() if !ok { break } } require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called") var args map[string]any err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) require.NoError(t, err) assert.Equal(t, "FIXME", args["pattern"], "alias 'grep_content' should be remapped to 'pattern' even after handler rebuild") assert.NotContains(t, args, "grep_content", "alias key should not be present after remapping") }) t.Run("name_alias_propagated_through_prepareExecContext", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) captureTool := &aliasCaptureTool{ name: "grep", params: map[string]*schema.ParameterInfo{ "pattern": {Type: schema.String}, }, } cm := mockModel.NewMockToolCallingChatModel(ctrl) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return &schema.Message{ Role: schema.Assistant, ToolCalls: []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "search_content", Arguments: `{"pattern": "TODO"}`, }, }, }, }, nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "test", Instruction: "test", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{captureTool}, ToolAliases: map[string]compose.ToolAliasConfig{ "grep": { NameAliases: []string{"search_content"}, }, }, }, }, }) require.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search")}}) for { _, ok := iter.Next() if !ok { break } } require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called via name alias 'search_content'") var args map[string]any err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) require.NoError(t, err) assert.Equal(t, "TODO", args["pattern"]) }) t.Run("handler_adds_tool_matching_preexisting_ToolAliases_with_no_initial_tools", func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) captureTool := &aliasCaptureTool{ name: "grep", params: map[string]*schema.ParameterInfo{ "pattern": {Type: schema.String, Desc: "regex pattern"}, }, } cm := mockModel.NewMockToolCallingChatModel(ctrl) generateCount := 0 cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { generateCount++ if generateCount == 1 { return &schema.Message{ Role: schema.Assistant, ToolCalls: []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "grep", Arguments: `{"grep_content": "BUG"}`, }, }, }, }, nil } return schema.AssistantMessage("done", nil), nil }).AnyTimes() cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() handler := &testToolsHandler{ BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, tools: []tool.BaseTool{captureTool}, } agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ Name: "test", Instruction: "test", Model: cm, ToolsConfig: ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ ToolAliases: map[string]compose.ToolAliasConfig{ "grep": { ArgumentsAliases: map[string][]string{ "pattern": {"grep_content"}, }, }, }, }, }, Handlers: []ChatModelAgentMiddleware{handler}, }) require.NoError(t, err) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("find bugs")}}) for { _, ok := iter.Next() if !ok { break } } require.NotEmpty(t, captureTool.receivedArgs, "tool added by handler should have been called") var args map[string]any err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) require.NoError(t, err) assert.Equal(t, "BUG", args["pattern"], "alias 'grep_content' should be remapped to 'pattern' for handler-added tool") assert.NotContains(t, args, "grep_content") }) }