diff --git a/agent/llmagent/llm_agent.go b/agent/llmagent/llm_agent.go index fab15d6701..96a92a82d0 100644 --- a/agent/llmagent/llm_agent.go +++ b/agent/llmagent/llm_agent.go @@ -161,6 +161,9 @@ func New(name string, opts ...Option) *LLMAgent { if err := validateAndNormalizeToolActivationOptions(&options); err != nil { panic(fmt.Sprintf("Invalid LLMAgent configuration: %v", err)) } + if err := validateAndNormalizeToolSetToolNameModes(&options); err != nil { + panic(fmt.Sprintf("Invalid LLMAgent configuration: %v", err)) + } // Register tools from both tools and toolsets, including knowledge search tool if provided. // Also track which tools are user-registered (via WithTools) for filtering purposes. @@ -893,7 +896,10 @@ func appendStaticToolSetTools( ctx := context.Background() for _, toolSet := range options.ToolSets { - namedToolSet := itool.NewNamedToolSet(toolSet) + namedToolSet := itool.NewNamedToolSetWithMode( + toolSet, + toolSetToolNameMode(options.toolSetToolNameModes, toolSet), + ) for _, t := range namedToolSet.Tools(ctx) { allTools = append(allTools, t) userToolNames[t.Declaration().Name] = true @@ -2083,7 +2089,10 @@ func (a *LLMAgent) getAllToolsLockedWithContext( if a.option.RefreshToolSetsOnRun && len(a.option.ToolSets) > 0 { dynamic := make([]tool.Tool, 0) for _, toolSet := range a.option.ToolSets { - namedToolSet := itool.NewNamedToolSet(toolSet) + namedToolSet := itool.NewNamedToolSetWithMode( + toolSet, + toolSetToolNameMode(a.option.toolSetToolNameModes, toolSet), + ) setTools := namedToolSet.Tools(ctx) dynamic = append(dynamic, setTools...) } diff --git a/agent/llmagent/option.go b/agent/llmagent/option.go index 4775e5719d..0ad81106bb 100644 --- a/agent/llmagent/option.go +++ b/agent/llmagent/option.go @@ -12,6 +12,7 @@ package llmagent import ( "reflect" + "strings" "trpc.group/trpc-go/trpc-agent-go/agent" "trpc.group/trpc-go/trpc-agent-go/agent/extension" @@ -295,6 +296,9 @@ type Options struct { Tools []tool.Tool // ToolSets is the list of tool sets available to the agent. ToolSets []tool.ToolSet + // toolSetToolNameModes configures model-facing tool names by ToolSet name. + // ToolSet.Name remains the stable identity used by activation and policy. + toolSetToolNameModes map[string]tool.ToolSetToolNameMode // activatableToolSets is the list of tool sets available for runtime activation. activatableToolSets []tool.ToolSet // toolActivationRules stores runtime tool activation rules. @@ -935,6 +939,23 @@ func WithToolSets(toolSets []tool.ToolSet) Option { } } +// WithToolSetToolNameMode sets how tools from the named ToolSet are exposed to +// the model. ToolSetToolNameModeQualified is the default and exposes names as +// {toolSetName}_{toolName}; ToolSetToolNameModeOriginal keeps the tool declarations' +// original names. The ToolSet name itself is unchanged and this option applies +// to both ToolSets and activatable ToolSets. Callers selecting original names +// must ensure that those names are unique across the model request. +// New panics during agent construction if the ToolSet name is blank, the mode +// is unsupported, or no registered ToolSet has the given name. +func WithToolSetToolNameMode(toolSetName string, mode tool.ToolSetToolNameMode) Option { + return func(opts *Options) { + if opts.toolSetToolNameModes == nil { + opts.toolSetToolNameModes = make(map[string]tool.ToolSetToolNameMode) + } + opts.toolSetToolNameModes[strings.TrimSpace(toolSetName)] = mode + } +} + // WithActivatableToolSets sets tool sets that may be activated at runtime. // These tool sets are not visible until an activation rule matches. func WithActivatableToolSets(toolSets []tool.ToolSet) Option { diff --git a/agent/llmagent/surface_runtime.go b/agent/llmagent/surface_runtime.go index 42da2eada3..0be72cc2b1 100644 --- a/agent/llmagent/surface_runtime.go +++ b/agent/llmagent/surface_runtime.go @@ -483,6 +483,7 @@ func (a *LLMAgent) userToolsForInvocation( } baseTools := append([]tool.Tool(nil), a.option.Tools...) toolSets := append([]tool.ToolSet(nil), a.option.ToolSets...) + toolSetToolNameModes := a.option.toolSetToolNameModes a.mu.RUnlock() if patchedTools, ok := patch.Tools(); ok { @@ -500,7 +501,10 @@ func (a *LLMAgent) userToolsForInvocation( userTools := append([]tool.Tool(nil), baseTools...) userToolNames = collectUserToolNames(baseTools) for _, toolSet := range toolSets { - namedToolSet := itool.NewNamedToolSet(toolSet) + namedToolSet := itool.NewNamedToolSetWithMode( + toolSet, + toolSetToolNameMode(toolSetToolNameModes, toolSet), + ) for _, t := range namedToolSet.Tools(ctx) { userTools = append(userTools, t) userToolNames[t.Declaration().Name] = true diff --git a/agent/llmagent/tool_activation.go b/agent/llmagent/tool_activation.go index d8b66ec3f4..6652507ba6 100644 --- a/agent/llmagent/tool_activation.go +++ b/agent/llmagent/tool_activation.go @@ -319,7 +319,7 @@ func (a *LLMAgent) applyToolActivation( userToolNames map[string]bool, externalToolNames map[string]bool, ) ([]tool.Tool, map[string]bool, map[string]bool) { - toolSets, rules, filter := a.toolActivationInputs() + toolSets, rules, filter, toolSetToolNameModes := a.toolActivationInputs() return applyToolActivationRecords( ctx, inv, @@ -329,6 +329,7 @@ func (a *LLMAgent) applyToolActivation( toolSets, rules, filter, + toolSetToolNameModes, ) } @@ -336,12 +337,14 @@ func (a *LLMAgent) toolActivationInputs() ( []tool.ToolSet, []toolActivationRule, func(context.Context, tool.Tool) bool, + map[string]tool.ToolSetToolNameMode, ) { a.mu.RLock() defer a.mu.RUnlock() return append([]tool.ToolSet(nil), a.option.activatableToolSets...), append([]toolActivationRule(nil), a.option.toolActivationRules...), - a.option.toolFilter + a.option.toolFilter, + a.option.toolSetToolNameModes } func (a *LLMAgent) handleToolActivationPostToolResult( @@ -683,6 +686,7 @@ func applyToolActivationRecords( toolSets []tool.ToolSet, rules []toolActivationRule, filter func(context.Context, tool.Tool) bool, + toolSetToolNameModes map[string]tool.ToolSetToolNameMode, ) ([]tool.Tool, map[string]bool, map[string]bool) { records := mergeToolActivationRecords( invocationToolActivationRecords(inv), @@ -704,6 +708,7 @@ func applyToolActivationRecords( activeSets, onlyNames, filter, + toolSetToolNameModes, ) if len(activatedTools) == 0 && len(onlyNames) == 0 { return tools, userToolNames, externalToolNames @@ -801,6 +806,7 @@ func expandActivatedTools( active []tool.ToolSet, only map[string]bool, filter func(context.Context, tool.Tool) bool, + toolSetToolNameModes map[string]tool.ToolSetToolNameMode, ) []tool.Tool { out := make([]tool.Tool, 0) acceptedToolNames := map[string]bool{} @@ -814,6 +820,7 @@ func expandActivatedTools( toolSet, acceptedToolNames, filter, + toolSetToolNameModes, ) if len(tools) == 0 { log.DebugfContext( @@ -846,8 +853,12 @@ func expandOneToolActivationSet( toolSet tool.ToolSet, acceptedToolNames map[string]bool, filter func(context.Context, tool.Tool) bool, + toolSetToolNameModes map[string]tool.ToolSetToolNameMode, ) []tool.Tool { - namedToolSet := itool.NewNamedToolSet(toolSet) + namedToolSet := itool.NewNamedToolSetWithMode( + toolSet, + toolSetToolNameMode(toolSetToolNameModes, toolSet), + ) tools := namedToolSet.Tools(ctx) if len(tools) == 0 { return nil diff --git a/agent/llmagent/tool_activation_test.go b/agent/llmagent/tool_activation_test.go index 5d5badc87d..65a60d7960 100644 --- a/agent/llmagent/tool_activation_test.go +++ b/agent/llmagent/tool_activation_test.go @@ -236,6 +236,49 @@ func TestToolActivationSkillLoadUpdatesNextModelRequestTools(t *testing.T) { require.Contains(t, requests[1].Tools, "browser_open") } +func TestToolActivationOriginalToolNames(t *testing.T) { + repo, err := skill.NewFSRepository( + createNamedTestSkill(t, "research", "research skill"), + ) + require.NoError(t, err) + mockModel := &activationSequenceModel{ + responses: []*model.Response{ + activationToolCallResponse(t, "call-1", "research"), + activationFinalResponse("done"), + }, + } + agt := New( + "agent", + WithModel(mockModel), + WithSkills(repo), + WithActivatableToolSets([]tool.ToolSet{ + activationToolSet{ + name: "github", + tools: []tool.Tool{activationTool{name: "search"}}, + }, + }), + WithToolSetToolNameMode("github", tool.ToolSetToolNameModeOriginal), + WithToolActivationOnSkillLoad("research", []string{"github"}), + ) + + inv := &agent.Invocation{ + InvocationID: "inv", + Session: session.NewSession("app", "user", "session"), + Message: model.NewUserMessage("load research"), + } + events, err := agt.Run(context.Background(), inv) + require.NoError(t, err) + for range events { + } + + requests := mockModel.Requests() + require.Len(t, requests, 2) + require.NotContains(t, requests[0].Tools, "search") + require.NotContains(t, requests[0].Tools, "github_search") + require.Contains(t, requests[1].Tools, "search") + require.NotContains(t, requests[1].Tools, "github_search") +} + func TestToolActivationSessionLifetimeVisibleInNextInvocation(t *testing.T) { repo, err := skill.NewFSRepository( createNamedTestSkill(t, "research", "research skill"), @@ -472,6 +515,7 @@ func TestToolActivationIncludeReplacesExternalToolWithSameName(t *testing.T) { }, nil, nil, + nil, ) require.Len(t, out, 1) activated, ok := out[0].(*itool.NamedTool) @@ -859,6 +903,7 @@ func TestToolActivationExpansionSkipsDuplicatesAndFilteredTools(t *testing.T) { func(_ context.Context, tl tool.Tool) bool { return toolActivationToolName(tl) != "safe_skip" }, + nil, ) require.Len(t, tools, 1) require.Equal(t, "safe_browse", toolActivationToolName(tools[0])) @@ -871,12 +916,14 @@ func TestToolActivationExpansionSkipsDuplicatesAndFilteredTools(t *testing.T) { }, accepted, nil, + nil, )) require.Empty(t, expandOneToolActivationSet( ctx, activationToolSet{name: "empty"}, map[string]bool{}, nil, + nil, )) } diff --git a/agent/llmagent/tool_name_mode.go b/agent/llmagent/tool_name_mode.go new file mode 100644 index 0000000000..11aa88e927 --- /dev/null +++ b/agent/llmagent/tool_name_mode.go @@ -0,0 +1,76 @@ +// +// Tencent is pleased to support the open source community by making trpc-agent-go available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// trpc-agent-go is licensed under the Apache License Version 2.0. +// + +package llmagent + +import ( + "fmt" + "strings" + + "trpc.group/trpc-go/trpc-agent-go/tool" +) + +func validateAndNormalizeToolSetToolNameModes(options *Options) error { + if options == nil || len(options.toolSetToolNameModes) == 0 { + return nil + } + normalized := make(map[string]tool.ToolSetToolNameMode, len(options.toolSetToolNameModes)) + registeredNames := registeredToolSetNames(options) + for rawName, mode := range options.toolSetToolNameModes { + name := strings.TrimSpace(rawName) + if name == "" { + return fmt.Errorf("tool set name for tool name mode must not be empty") + } + switch mode { + case tool.ToolSetToolNameModeQualified, tool.ToolSetToolNameModeOriginal: + default: + return fmt.Errorf("unsupported tool name mode %d for tool set %q", mode, name) + } + if !registeredNames[name] { + return fmt.Errorf("tool set %q is not registered", name) + } + normalized[name] = mode + } + options.toolSetToolNameModes = normalized + return nil +} + +func registeredToolSetNames(options *Options) map[string]bool { + names := make(map[string]bool) + if options == nil { + return names + } + for _, toolSets := range [][]tool.ToolSet{ + options.ToolSets, + options.activatableToolSets, + } { + for _, toolSet := range toolSets { + if toolSet == nil { + continue + } + if name := strings.TrimSpace(toolSet.Name()); name != "" { + names[name] = true + } + } + } + return names +} + +func toolSetToolNameMode( + toolSetNameModes map[string]tool.ToolSetToolNameMode, + toolSet tool.ToolSet, +) tool.ToolSetToolNameMode { + if toolSet == nil { + return tool.ToolSetToolNameModeQualified + } + name := strings.TrimSpace(toolSet.Name()) + if mode, ok := toolSetNameModes[name]; ok { + return mode + } + return tool.ToolSetToolNameModeQualified +} diff --git a/agent/llmagent/tool_name_mode_test.go b/agent/llmagent/tool_name_mode_test.go new file mode 100644 index 0000000000..5ca04161e8 --- /dev/null +++ b/agent/llmagent/tool_name_mode_test.go @@ -0,0 +1,86 @@ +// +// Tencent is pleased to support the open source community by making trpc-agent-go available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// trpc-agent-go is licensed under the Apache License Version 2.0. +// + +package llmagent + +import ( + "testing" + + "github.com/stretchr/testify/require" + "trpc.group/trpc-go/trpc-agent-go/tool" +) + +func TestLLMAgent_ToolSetToolNameMode(t *testing.T) { + agent := New( + "name-mode-agent", + WithToolSets([]tool.ToolSet{ + dummyToolSet{name: "github"}, + }), + WithToolSetToolNameMode("github", tool.ToolSetToolNameModeOriginal), + ) + + names := make(map[string]bool) + for _, tl := range agent.Tools() { + names[tl.Declaration().Name] = true + } + require.True(t, names[testKnowledgeToolName]) + require.False(t, names["github_"+testKnowledgeToolName]) +} + +func TestLLMAgent_RefreshToolSetToolNameMode(t *testing.T) { + agent := New( + "name-mode-agent", + WithToolSets([]tool.ToolSet{ + &dynamicToolSet{ + name: "github", + tools: []tool.Tool{ + dummyTool{decl: &tool.Declaration{Name: "search"}}, + }, + }, + }), + WithRefreshToolSetsOnRun(true), + WithToolSetToolNameMode("github", tool.ToolSetToolNameModeOriginal), + ) + + names := make(map[string]bool) + for _, tl := range agent.Tools() { + names[tl.Declaration().Name] = true + } + require.True(t, names["search"]) + require.False(t, names["github_search"]) +} + +func TestWithToolSetToolNameModeValidation(t *testing.T) { + require.PanicsWithValue(t, + "Invalid LLMAgent configuration: tool set name for tool name mode must not be empty", + func() { + _ = New( + "name-mode-agent", + WithToolSetToolNameMode(" ", tool.ToolSetToolNameModeOriginal), + ) + }, + ) + require.PanicsWithValue(t, + "Invalid LLMAgent configuration: unsupported tool name mode 99 for tool set \"github\"", + func() { + _ = New( + "name-mode-agent", + WithToolSetToolNameMode("github", tool.ToolSetToolNameMode(99)), + ) + }, + ) + require.PanicsWithValue(t, + "Invalid LLMAgent configuration: tool set \"missing\" is not registered", + func() { + _ = New( + "name-mode-agent", + WithToolSetToolNameMode("missing", tool.ToolSetToolNameModeOriginal), + ) + }, + ) +} diff --git a/docs/mkdocs/en/tool.md b/docs/mkdocs/en/tool.md index d91674f3b4..f89ca5821e 100644 --- a/docs/mkdocs/en/tool.md +++ b/docs/mkdocs/en/tool.md @@ -968,10 +968,47 @@ agent := llmagent.New("mcp-assistant", ### Tool Name Prefixing -When an MCP ToolSet is wired into an `LLMAgent` via `WithToolSets`, the -framework wraps it with `NamedToolSet`. The model sees each remote tool under -`{toolSetName}_{remoteToolName}` while the underlying MCP `tools/call` still -uses the original remote name. +When a ToolSet (including an MCP ToolSet) is wired into an `LLMAgent` via +`WithToolSets` or `WithActivatableToolSets`, the framework wraps it with +`NamedToolSet`. By default, a ToolSet named `github` whose tool declaration is +`search` is exposed to the model as `github_search`. The underlying Tool still +receives the original tool name. + +Use `llmagent.WithToolSetToolNameMode` to configure the model-facing names for +an individual registered ToolSet: + +```go +import ( + "trpc.group/trpc-go/trpc-agent-go/agent/llmagent" + "trpc.group/trpc-go/trpc-agent-go/tool" +) + +// githubToolSet is any tool.ToolSet implementation whose Name() is "github". +agent := llmagent.New("assistant", + llmagent.WithToolSets([]tool.ToolSet{githubToolSet}), + llmagent.WithToolSetToolNameMode( + "github", + tool.ToolSetToolNameModeOriginal, + ), +) +``` + +The available modes are: + +- `tool.ToolSetToolNameModeQualified` (default): exposes names as + `{toolSetName}_{toolName}`. +- `tool.ToolSetToolNameModeOriginal`: exposes each Tool's original declaration + name without the ToolSet prefix. + +The option also applies to activatable ToolSets and ToolSets refreshed after +their tool list changes. It changes only the model-visible declaration name; +`ToolSet.Name()` remains the identity used for activation, policy, and tracing, +and calls still reach the underlying Tool. When using original names, callers +must ensure that names are unique across all tools visible in a model request. +Agent construction rejects blank or unregistered ToolSet names and unsupported +modes. + +For MCP ToolSets specifically: - Default ToolSet name is `"mcp"`, so a remote tool `search` becomes `mcp_search`. diff --git a/docs/mkdocs/zh/tool.md b/docs/mkdocs/zh/tool.md index b8676d0367..4806274036 100644 --- a/docs/mkdocs/zh/tool.md +++ b/docs/mkdocs/zh/tool.md @@ -928,9 +928,44 @@ agent := llmagent.New("mcp-assistant", ### 工具名前缀 -通过 `WithToolSets` 把 MCP ToolSet 挂到 `LLMAgent` 上时,框架会用 -`NamedToolSet` 包装它。模型侧看到的工具名为 -`{toolSetName}_{远端工具名}`,实际 MCP `tools/call` 仍使用远端原始名称。 +通过 `WithToolSets` 或 `WithActivatableToolSets` 把 ToolSet(包括 MCP +ToolSet)挂到 `LLMAgent` 上时,框架会用 `NamedToolSet` 包装它。默认情况下, +名称为 `github` 的 ToolSet 中声明了 `search` 工具,模型侧看到的是 +`github_search`,底层 Tool 仍会收到原始工具名。 + +可以使用 `llmagent.WithToolSetToolNameMode` 为某个已注册的 ToolSet 配置 +模型侧工具名: + +```go +import ( + "trpc.group/trpc-go/trpc-agent-go/agent/llmagent" + "trpc.group/trpc-go/trpc-agent-go/tool" +) + +// githubToolSet 是任意 Name() 返回 "github" 的 tool.ToolSet 实现。 +agent := llmagent.New("assistant", + llmagent.WithToolSets([]tool.ToolSet{githubToolSet}), + llmagent.WithToolSetToolNameMode( + "github", + tool.ToolSetToolNameModeOriginal, + ), +) +``` + +可选模式如下: + +- `tool.ToolSetToolNameModeQualified`(默认):暴露为 + `{toolSetName}_{toolName}`。 +- `tool.ToolSetToolNameModeOriginal`:使用 Tool 原始声明中的名称,不加 + ToolSet 前缀。 + +该选项同样适用于可激活 ToolSet,以及工具列表刷新后的 ToolSet。它只改变 +模型可见的声明名称;`ToolSet.Name()` 仍作为激活、策略和追踪使用的身份标识, +实际调用仍会转发到底层 Tool。使用原始名称时,调用方需要保证同一次模型请求 +中所有可见工具的名称唯一。Agent 构建时会拒绝空 ToolSet 名称、未注册的 +ToolSet 名称以及不支持的 mode。 + +对于 MCP ToolSet: - 默认 ToolSet 名为 `"mcp"`,远端工具 `search` 会暴露为 `mcp_search`。 - 挂载多个 MCP ToolSet 时,请用 `mcp.WithName(...)` 为每个 ToolSet 设置 diff --git a/internal/tool/tool_test.go b/internal/tool/tool_test.go index cae7227f66..bf1e25a42f 100644 --- a/internal/tool/tool_test.go +++ b/internal/tool/tool_test.go @@ -160,6 +160,19 @@ func TestNamedToolSet_Idempotent(t *testing.T) { require.Same(t, nts, nts2, "idempotent wrapper should be same instance") } +func TestNamedToolSet_ModeOverride(t *testing.T) { + base := &fakeToolSet{ + name: "github", + tools: []tool.Tool{&simpleTool{name: "search", desc: "search"}}, + } + qualified := NewNamedToolSet(base) + original := NewNamedToolSetWithMode(qualified, tool.ToolSetToolNameModeOriginal) + require.NotSame(t, qualified, original) + require.Equal(t, "search", original.Tools(context.Background())[0].Declaration().Name) + require.Same(t, qualified, NewNamedToolSetWithMode(qualified, tool.ToolSetToolNameModeQualified)) + require.Same(t, original, NewNamedToolSet(original)) +} + func TestNamedToolSet_Tools_PrefixingAndPassthrough(t *testing.T) { // With a name, tool names should be prefixed. base := &fakeToolSet{ @@ -178,6 +191,20 @@ func TestNamedToolSet_Tools_PrefixingAndPassthrough(t *testing.T) { require.Equal(t, "write", got2[0].Declaration().Name) } +func TestNamedToolSet_Tools_OriginalNames(t *testing.T) { + base := &fakeToolSet{ + name: "github", + tools: []tool.Tool{&simpleTool{name: "search", desc: "search"}}, + } + + got := NewNamedToolSetWithMode(base, tool.ToolSetToolNameModeOriginal).Tools(context.Background()) + require.Len(t, got, 1) + require.Equal(t, "search", got[0].Declaration().Name) + named, ok := got[0].(*NamedTool) + require.True(t, ok) + require.Equal(t, "github", named.ToolSetName()) +} + func TestNamedTool_OriginalAndCloseAndName(t *testing.T) { base := &fakeToolSet{name: "fs"} nts := NewNamedToolSet(base) diff --git a/internal/tool/toolset.go b/internal/tool/toolset.go index f0012a2d0b..bec4724580 100644 --- a/internal/tool/toolset.go +++ b/internal/tool/toolset.go @@ -16,10 +16,11 @@ import ( "trpc.group/trpc-go/trpc-agent-go/tool" ) -// NamedToolSet wraps a ToolSet to automatically prefix tool names with the toolset name. -// This prevents tool name conflicts when multiple toolsets provide tools with the same name. +// NamedToolSet wraps a ToolSet to qualify tool names with the ToolSet name by +// default. Callers can opt into exposing the original tool names. type NamedToolSet struct { - toolSet tool.ToolSet + toolSet tool.ToolSet + nameMode tool.ToolSetToolNameMode } // NewNamedToolSet creates a new named toolset wrapper. @@ -28,12 +29,40 @@ func NewNamedToolSet(toolSet tool.ToolSet) *NamedToolSet { if t, ok := toolSet.(*NamedToolSet); ok { return t } + return NewNamedToolSetWithMode(toolSet, tool.ToolSetToolNameModeQualified) +} + +// NewNamedToolSetWithMode creates a named ToolSet wrapper with the requested +// model-facing name mode. +func NewNamedToolSetWithMode( + toolSet tool.ToolSet, + nameMode tool.ToolSetToolNameMode, +) *NamedToolSet { + mode := normalizeToolSetToolNameMode(nameMode) + if t, ok := toolSet.(*NamedToolSet); ok { + if t.nameMode == mode { + return t + } + return &NamedToolSet{ + toolSet: t.toolSet, + nameMode: mode, + } + } return &NamedToolSet{ - toolSet: toolSet, + toolSet: toolSet, + nameMode: mode, } } -// Tools returns tools with names prefixed by the toolset name to avoid conflicts. +func normalizeToolSetToolNameMode(mode tool.ToolSetToolNameMode) tool.ToolSetToolNameMode { + if mode == tool.ToolSetToolNameModeOriginal { + return tool.ToolSetToolNameModeOriginal + } + return tool.ToolSetToolNameModeQualified +} + +// Tools returns tools with model-facing names according to the ToolSet's name +// mode. The ToolSet name is retained separately for runtime policy checks. func (s *NamedToolSet) Tools(ctx context.Context) []tool.Tool { tools := s.toolSet.Tools(ctx) @@ -41,18 +70,21 @@ func (s *NamedToolSet) Tools(ctx context.Context) []tool.Tool { if toolSetName == "" { return tools } - - // Create tools with prefixed names to avoid conflicts - prefixedTools := make([]tool.Tool, 0, len(tools)) + // Create tools with model-facing names while retaining the source ToolSet + // name for runtime policy and tracing checks. + namedTools := make([]tool.Tool, 0, len(tools)) for _, t := range tools { - prefixedTool := &NamedTool{ - original: t, - name: toolSetName, + namedTool := &NamedTool{ + original: t, + toolSetName: toolSetName, + } + if s.nameMode == tool.ToolSetToolNameModeQualified { + namedTool.name = toolSetName } - prefixedTools = append(prefixedTools, prefixedTool) + namedTools = append(namedTools, namedTool) } - return prefixedTools + return namedTools } // Close implements the ToolSet interface. @@ -65,10 +97,12 @@ func (s *NamedToolSet) Name() string { return s.toolSet.Name() } -// NamedTool wraps an original tool with a prefixed name to avoid conflicts. +// NamedTool wraps an original tool with a model-facing name and retains the +// source ToolSet identity for runtime policy checks. type NamedTool struct { - original tool.Tool - name string + original tool.Tool + name string + toolSetName string } // NewUnprefixedNamedTool wraps a tool as a NamedTool without adding any name @@ -243,7 +277,8 @@ func toolName(tl tool.Tool) string { return decl.Name } -// Declaration returns the tool declaration with a prefixed name. +// Declaration returns the tool declaration with the configured model-facing +// name. func (t *NamedTool) Declaration() *tool.Declaration { decl := t.original.Declaration() name := decl.Name @@ -295,7 +330,7 @@ func (t *NamedTool) CheckPermission( // ToolSetName returns the source ToolSet name for runtime policy checks. func (t *NamedTool) ToolSetName() string { - return t.name + return t.toolSetName } // Call delegates to the original tool's Call method. diff --git a/tool/name_mode.go b/tool/name_mode.go new file mode 100644 index 0000000000..22005ea5f7 --- /dev/null +++ b/tool/name_mode.go @@ -0,0 +1,24 @@ +// +// Tencent is pleased to support the open source community by making trpc-agent-go available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// trpc-agent-go is licensed under the Apache License Version 2.0. +// + +package tool + +// ToolSetToolNameMode controls how a ToolSet's tools are named when exposed to +// a model. The ToolSet name remains the stable identity used by the host. An +// agent can select the mode for each registered ToolSet without requiring the +// ToolSet implementation to implement any additional interface. +type ToolSetToolNameMode int + +const ( + // ToolSetToolNameModeQualified prefixes each tool name with the ToolSet name. + // This is the default and preserves the existing naming behavior. + ToolSetToolNameModeQualified ToolSetToolNameMode = iota + // ToolSetToolNameModeOriginal exposes each tool using its original declaration + // name without adding the ToolSet name as a prefix. + ToolSetToolNameModeOriginal +)