/* * Copyright 2026 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 compose import ( "context" "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/schema" ) type searchArgs struct { Query string `json:"query"` } func TestToolNameAliases(t *testing.T) { ctx := context.Background() // Create test tool searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search for information", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string", Desc: "Search query"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "search result", nil }) // Configure aliases config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"search_v1", "query", "find"}, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Test calling tool with alias input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "search_v1", // Using alias Arguments: `{"query": "test"}`, }, }, }) output, err := node.Invoke(ctx, input) require.NoError(t, err) require.Len(t, output, 1) assert.Equal(t, "call_1", output[0].ToolCallID) assert.Contains(t, output[0].Content, "search result") } type searchArgsWithLimit struct { Query string `json:"query"` Limit int `json:"limit"` } func TestArgumentsAliases(t *testing.T) { ctx := context.Background() receivedArgs := "" searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search for information", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, "limit": {Type: "integer"}, }), }, func(ctx context.Context, args *searchArgsWithLimit) (string, error) { b, _ := json.Marshal(args) receivedArgs = string(b) return "result", nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { ArgumentsAliases: map[string][]string{ "query": {"q", "search_term"}, "limit": {"max_results", "count"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Use alias parameters input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "search", Arguments: `{"q": "test", "max_results": 10}`, // Using aliases }, }, }) _, err = node.Invoke(ctx, input) require.NoError(t, err) // Verify tool received canonical parameter names var args map[string]any err = json.Unmarshal([]byte(receivedArgs), &args) require.NoError(t, err) assert.Equal(t, "test", args["query"]) assert.Equal(t, float64(10), args["limit"]) assert.NotContains(t, args, "q") assert.NotContains(t, args, "max_results") } type emptyArgs struct{} func TestAliasConflict(t *testing.T) { ctx := context.Background() tool1 := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { return "result", nil }) tool2 := newTool(&schema.ToolInfo{Name: "query", Desc: "Query"}, func(ctx context.Context, args *emptyArgs) (string, error) { return "result", nil }) t.Run("tool name alias conflict", func(t *testing.T) { config := &ToolsNodeConfig{ Tools: []tool.BaseTool{tool1, tool2}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, }, "query": { NameAliases: []string{"find"}, // Conflict: find already used by search }, }, } _, err := NewToolNode(ctx, config) require.Error(t, err) assert.Contains(t, err.Error(), "conflicts with an alias already registered for") }) t.Run("tool name alias conflicts with canonical name", func(t *testing.T) { config := &ToolsNodeConfig{ Tools: []tool.BaseTool{tool1, tool2}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"query"}, // Conflict: "query" is tool2's canonical name }, }, } _, err := NewToolNode(ctx, config) require.Error(t, err) assert.Contains(t, err.Error(), "conflicts with existing tool's canonical name") }) t.Run("argument alias conflict", func(t *testing.T) { config := &ToolsNodeConfig{ Tools: []tool.BaseTool{tool1}, ToolAliases: map[string]ToolAliasConfig{ "search": { ArgumentsAliases: map[string][]string{ "query": {"q"}, "limit": {"q"}, // Conflict: q maps to multiple parameters }, }, }, } _, err := NewToolNode(ctx, config) require.Error(t, err) assert.Contains(t, err.Error(), "conflicting arg alias") }) t.Run("arg alias conflicts with existing schema property", func(t *testing.T) { searchWithParams := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, "limit": {Type: "integer"}, }), }, func(ctx context.Context, args *emptyArgs) (string, error) { return "result", nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchWithParams}, ToolAliases: map[string]ToolAliasConfig{ "search": { ArgumentsAliases: map[string][]string{ "limit": {"query"}, // "query" is already a schema property }, }, }, } _, err := NewToolNode(ctx, config) require.Error(t, err) assert.Contains(t, err.Error(), "conflicts with existing schema property") }) } func TestArgumentsAliasesWithHandler(t *testing.T) { ctx := context.Background() executionOrder := []string{} searchTool := newTool(&schema.ToolInfo{ Name: "search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { executionOrder = append(executionOrder, "tool_invoke") return "result", nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, ArgumentsAliases: map[string][]string{ "query": {"q"}, }, }, }, ToolArgumentsHandler: func(ctx context.Context, name, args string) (string, error) { executionOrder = append(executionOrder, "args_handler") // Handler receives the original model-returned name (alias) assert.Equal(t, "search", name) // Verify alias remapping has already been done var m map[string]any err := json.Unmarshal([]byte(args), &m) require.NoError(t, err) assert.Contains(t, m, "query") assert.NotContains(t, m, "q") return args, nil }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Call with alias name "find" and alias arg "q" input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "test"}`, }, }, }) _, err = node.Invoke(ctx, input) require.NoError(t, err) // Verify execution order: alias remapping → ToolArgumentsHandler → tool execution assert.Equal(t, []string{"args_handler", "tool_invoke"}, executionOrder) } func TestNonExistentToolInAliasConfig(t *testing.T) { ctx := context.Background() tool1 := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { return "result", nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{tool1}, ToolAliases: map[string]ToolAliasConfig{ "non_existent_tool": { // Non-existent tool NameAliases: []string{"alias1"}, }, }, } // Should not error — non-existent tool alias configs are silently skipped node, err := NewToolNode(ctx, config) require.NoError(t, err) // The existing tool should still work normally input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "search", Arguments: `{}`, }, }, }) output, err := node.Invoke(ctx, input) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "result") } type weatherArgs struct { Location string `json:"location"` } func TestToolAliasesE2E(t *testing.T) { ctx := context.Background() // Create multiple tools searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search for information", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, "limit": {Type: "integer"}, }), }, func(ctx context.Context, args *searchArgsWithLimit) (string, error) { return "search result", nil }) weatherTool := newTool(&schema.ToolInfo{ Name: "weather", Desc: "Get weather information", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "location": {Type: "string"}, }), }, func(ctx context.Context, args *weatherArgs) (string, error) { return "weather result", nil }) // Configure aliases for multiple tools config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool, weatherTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"search_v1", "query"}, ArgumentsAliases: map[string][]string{ "query": {"q", "search_term"}, "limit": {"max_results"}, }, }, "weather": { NameAliases: []string{"get_weather"}, ArgumentsAliases: map[string][]string{ "location": {"loc", "city"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Construct message with multiple tool calls using different aliases input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "search_v1", // Tool name alias Arguments: `{"q": "test", "max_results": 5}`, // Parameter aliases }, }, { ID: "call_2", Function: schema.FunctionCall{ Name: "get_weather", // Tool name alias Arguments: `{"city": "Beijing"}`, // Parameter alias }, }, }) output, err := node.Invoke(ctx, input) require.NoError(t, err) require.Len(t, output, 2) // Verify both tools executed successfully assert.Equal(t, "call_1", output[0].ToolCallID) assert.Equal(t, "call_2", output[1].ToolCallID) assert.Contains(t, output[0].Content, "search result") assert.Contains(t, output[1].Content, "weather result") } func TestRemapArgsEdgeCases(t *testing.T) { aliasMap := map[string]string{"q": "query"} t.Run("empty string", func(t *testing.T) { result, err := remapArgs("", aliasMap) assert.NoError(t, err) assert.Equal(t, "", result) }) t.Run("whitespace only", func(t *testing.T) { result, err := remapArgs(" ", aliasMap) assert.NoError(t, err) assert.Equal(t, " ", result) }) t.Run("non-object JSON", func(t *testing.T) { result, err := remapArgs(`"hello"`, aliasMap) assert.NoError(t, err) assert.Equal(t, `"hello"`, result) }) t.Run("JSON array", func(t *testing.T) { result, err := remapArgs(`[1,2,3]`, aliasMap) assert.NoError(t, err) assert.Equal(t, `[1,2,3]`, result) }) t.Run("invalid JSON", func(t *testing.T) { result, err := remapArgs(`{invalid`, aliasMap) assert.NoError(t, err) assert.Equal(t, `{invalid`, result) }) t.Run("alias and canonical both present", func(t *testing.T) { // When both alias "q" and canonical "query" exist, alias is kept as-is (not deleted, not overwritten) result, err := remapArgs(`{"q": "alias_val", "query": "canonical_val"}`, aliasMap) assert.NoError(t, err) var m map[string]any require.NoError(t, json.Unmarshal([]byte(result), &m)) assert.Equal(t, "canonical_val", m["query"]) assert.Equal(t, "alias_val", m["q"]) }) t.Run("unknown fields preserved", func(t *testing.T) { result, err := remapArgs(`{"q": "test", "unknown_field": 42}`, aliasMap) assert.NoError(t, err) var m map[string]any require.NoError(t, json.Unmarshal([]byte(result), &m)) assert.Equal(t, "test", m["query"]) assert.NotContains(t, m, "q") assert.Equal(t, float64(42), m["unknown_field"]) }) } func TestCanonicalNameCallWithAliasConfigured(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "result: " + args.Query, nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, ArgumentsAliases: map[string][]string{ "query": {"q"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Call with canonical name and canonical arg — should work normally input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "search", Arguments: `{"query": "hello"}`, }, }, }) output, err := node.Invoke(ctx, input) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "result: hello") } func TestEmptyAliasValidation(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { return "result", nil }) t.Run("empty name alias", func(t *testing.T) { config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{""}, }, }, } _, err := NewToolNode(ctx, config) require.Error(t, err) assert.Contains(t, err.Error(), "empty name alias") }) t.Run("empty arg alias", func(t *testing.T) { config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { ArgumentsAliases: map[string][]string{ "query": {""}, }, }, }, } _, err := NewToolNode(ctx, config) require.Error(t, err) assert.Contains(t, err.Error(), "empty argument alias") }) t.Run("empty canonical arg key", func(t *testing.T) { config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { ArgumentsAliases: map[string][]string{ "": {"q"}, }, }, }, } _, err := NewToolNode(ctx, config) require.Error(t, err) assert.Contains(t, err.Error(), "empty canonical argument key") }) } func TestNameAliasSameAsCanonical(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { return "result", nil }) // Alias same as canonical name — should be tolerated (skip, no error) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"search", "find"}, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Both canonical and alias should work for _, name := range []string{"search", "find"} { input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: name, Arguments: `{}`, }, }, }) output, err := node.Invoke(ctx, input) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "result") } } func TestToolAliasesWithDynamicToolList(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "search result: " + args.Query, nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, ArgumentsAliases: map[string][]string{ "query": {"q"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Use dynamic ToolList via option — alias should still work input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "dynamic"}`, }, }, }) output, err := node.Invoke(ctx, input, WithToolList(searchTool)) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "search result: dynamic") } func TestToolNameAliasesStream(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search for information", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "stream result: " + args.Query, nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, ArgumentsAliases: map[string][]string{ "query": {"q"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "hello"}`, }, }, }) reader, err := node.Stream(ctx, input) require.NoError(t, err) var chunks [][]*schema.Message for { chunk, err := reader.Recv() if err != nil { break } chunks = append(chunks, chunk) } msgs, err := schema.ConcatMessageArray(chunks) require.NoError(t, err) require.Len(t, msgs, 1) assert.Equal(t, "call_1", msgs[0].ToolCallID) assert.Contains(t, msgs[0].Content, "stream result: hello") } func TestEnhancedToolWithAliases(t *testing.T) { ctx := context.Background() enhancedTool := &enhancedInvokableTool{ info: &schema.ToolInfo{ Name: "search", Desc: "Enhanced search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) { return &schema.ToolResult{ Parts: []schema.ToolOutputPart{ {Type: schema.ToolPartTypeText, Text: "enhanced: " + input.Text}, }, }, nil }, } config := &ToolsNodeConfig{ Tools: []tool.BaseTool{enhancedTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, ArgumentsAliases: map[string][]string{ "query": {"q"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Call with alias name and alias arg input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "test"}`, }, }, }) output, err := node.Invoke(ctx, input) require.NoError(t, err) require.Len(t, output, 1) assert.Equal(t, "call_1", output[0].ToolCallID) // Verify arg alias was remapped: "q" → "query" in the JSON passed to enhanced tool assert.Contains(t, output[0].UserInputMultiContent[0].Text, "enhanced:") } func TestDynamicToolListAliasRemoved(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "search result", nil }) weatherTool := newTool(&schema.ToolInfo{ Name: "weather", Desc: "Weather", }, func(ctx context.Context, args *emptyArgs) (string, error) { return "weather result", nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool, weatherTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) // Dynamic tool list only contains weatherTool — "search" and its alias "find" should not be available input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "find", Arguments: `{}`, }, }, }) _, err = node.Invoke(ctx, input, WithToolList(weatherTool)) require.Error(t, err) assert.Contains(t, err.Error(), "not found") } func TestToolAliasesOptionOverridesGlobal(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "search result: " + args.Query, nil }) weatherTool := newTool(&schema.ToolInfo{ Name: "weather", Desc: "Weather", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "location": {Type: "string"}, }), }, func(ctx context.Context, args *weatherArgs) (string, error) { return "weather result: " + args.Location, nil }) // Global aliases: search has alias "find" config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool, weatherTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, ArgumentsAliases: map[string][]string{ "query": {"q"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) t.Run("opt ToolAliases overrides global in Invoke", func(t *testing.T) { // opt.ToolAliases defines "lookup" as alias for search (not "find") optAliases := map[string]ToolAliasConfig{ "search": { NameAliases: []string{"lookup"}, ArgumentsAliases: map[string][]string{ "query": {"keyword"}, }, }, } // "lookup" should work with opt aliases input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "lookup", Arguments: `{"keyword": "test"}`, }, }, }) output, err := node.Invoke(ctx, input, WithToolList(searchTool), WithToolAliases(optAliases)) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "search result: test") // "find" (global alias) should NOT work when opt.ToolAliases is set input2 := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_2", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "test"}`, }, }, }) _, err = node.Invoke(ctx, input2, WithToolList(searchTool), WithToolAliases(optAliases)) require.Error(t, err) assert.Contains(t, err.Error(), "not found") }) t.Run("opt ToolAliases overrides global in Stream", func(t *testing.T) { optAliases := map[string]ToolAliasConfig{ "search": { NameAliases: []string{"lookup"}, ArgumentsAliases: map[string][]string{ "query": {"keyword"}, }, }, } input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "lookup", Arguments: `{"keyword": "stream_test"}`, }, }, }) reader, err := node.Stream(ctx, input, WithToolList(searchTool), WithToolAliases(optAliases)) require.NoError(t, err) var chunks [][]*schema.Message for { chunk, err := reader.Recv() if err != nil { break } chunks = append(chunks, chunk) } msgs, err := schema.ConcatMessageArray(chunks) require.NoError(t, err) require.Len(t, msgs, 1) assert.Contains(t, msgs[0].Content, "search result: stream_test") }) t.Run("nil opt ToolAliases falls back to global filtered", func(t *testing.T) { // No WithToolAliases — should use global "find" alias, filtered by ToolList input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "fallback"}`, }, }, }) output, err := node.Invoke(ctx, input, WithToolList(searchTool)) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "search result: fallback") }) t.Run("opt ToolAliases only without ToolList replaces global", func(t *testing.T) { // Only WithToolAliases, no WithToolList — should use global tools with opt aliases optAliases := map[string]ToolAliasConfig{ "search": { NameAliases: []string{"lookup"}, ArgumentsAliases: map[string][]string{ "query": {"keyword"}, }, }, } // "lookup" (opt alias) should work input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "lookup", Arguments: `{"keyword": "only_alias"}`, }, }, }) output, err := node.Invoke(ctx, input, WithToolAliases(optAliases)) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "search result: only_alias") // "find" (global alias) should NOT work when opt.ToolAliases replaces global input2 := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_2", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "test"}`, }, }, }) _, err = node.Invoke(ctx, input2, WithToolAliases(optAliases)) require.Error(t, err) assert.Contains(t, err.Error(), "not found") }) t.Run("opt ToolAliases only without ToolList in Stream", func(t *testing.T) { optAliases := map[string]ToolAliasConfig{ "search": { NameAliases: []string{"lookup"}, }, } input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "lookup", Arguments: `{"query": "stream_only_alias"}`, }, }, }) reader, err := node.Stream(ctx, input, WithToolAliases(optAliases)) require.NoError(t, err) var chunks [][]*schema.Message for { chunk, err := reader.Recv() if err != nil { break } chunks = append(chunks, chunk) } msgs, err := schema.ConcatMessageArray(chunks) require.NoError(t, err) require.Len(t, msgs, 1) assert.Contains(t, msgs[0].Content, "search result: stream_only_alias") }) } func TestAliasConfigForToolAddedViaOption(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "search result: " + args.Query, nil }) weatherTool := newTool(&schema.ToolInfo{ Name: "weather", Desc: "Weather", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "location": {Type: "string"}, }), }, func(ctx context.Context, args *weatherArgs) (string, error) { return "weather result: " + args.Location, nil }) // New with only searchTool, but alias config includes weather tool config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, ArgumentsAliases: map[string][]string{ "query": {"q"}, }, }, "weather": { NameAliases: []string{"forecast"}, ArgumentsAliases: map[string][]string{ "location": {"loc"}, }, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) t.Run("weather alias works when tool passed via option", func(t *testing.T) { input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "forecast", Arguments: `{"loc": "Beijing"}`, }, }, }) output, err := node.Invoke(ctx, input, WithToolList(searchTool, weatherTool)) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "weather result: Beijing") }) t.Run("search alias still works with option tool list", func(t *testing.T) { input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "find", Arguments: `{"q": "test"}`, }, }, }) output, err := node.Invoke(ctx, input, WithToolList(searchTool, weatherTool)) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "search result: test") }) } func TestOptionWithToolListAndToolAliases(t *testing.T) { ctx := context.Background() searchTool := newTool(&schema.ToolInfo{ Name: "search", Desc: "Search", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "query": {Type: "string"}, }), }, func(ctx context.Context, args *searchArgs) (string, error) { return "search result: " + args.Query, nil }) weatherTool := newTool(&schema.ToolInfo{ Name: "weather", Desc: "Weather", ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "location": {Type: "string"}, }), }, func(ctx context.Context, args *weatherArgs) (string, error) { return "weather result: " + args.Location, nil }) config := &ToolsNodeConfig{ Tools: []tool.BaseTool{searchTool}, ToolAliases: map[string]ToolAliasConfig{ "search": { NameAliases: []string{"find"}, }, }, } node, err := NewToolNode(ctx, config) require.NoError(t, err) t.Run("opt aliases override global when both tool list and aliases provided", func(t *testing.T) { optAliases := map[string]ToolAliasConfig{ "weather": { NameAliases: []string{"forecast"}, ArgumentsAliases: map[string][]string{ "location": {"loc"}, }, }, } // "forecast" should work via opt aliases input := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_1", Function: schema.FunctionCall{ Name: "forecast", Arguments: `{"loc": "Shanghai"}`, }, }, }) output, err := node.Invoke(ctx, input, WithToolList(searchTool, weatherTool), WithToolAliases(optAliases)) require.NoError(t, err) require.Len(t, output, 1) assert.Contains(t, output[0].Content, "weather result: Shanghai") // "find" (global alias) should NOT work when opt aliases override input2 := schema.AssistantMessage("", []schema.ToolCall{ { ID: "call_2", Function: schema.FunctionCall{ Name: "find", Arguments: `{"query": "test"}`, }, }, }) _, err = node.Invoke(ctx, input2, WithToolList(searchTool, weatherTool), WithToolAliases(optAliases)) require.Error(t, err) assert.Contains(t, err.Error(), "not found") }) }