Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions agent/llmagent/llm_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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...)
}
Expand Down
21 changes: 21 additions & 0 deletions agent/llmagent/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// 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 {
Expand Down
6 changes: 5 additions & 1 deletion agent/llmagent/surface_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
17 changes: 14 additions & 3 deletions agent/llmagent/tool_activation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -329,19 +329,22 @@ func (a *LLMAgent) applyToolActivation(
toolSets,
rules,
filter,
toolSetToolNameModes,
)
}

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(
Expand Down Expand Up @@ -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),
Expand All @@ -704,6 +708,7 @@ func applyToolActivationRecords(
activeSets,
onlyNames,
filter,
toolSetToolNameModes,
)
if len(activatedTools) == 0 && len(onlyNames) == 0 {
return tools, userToolNames, externalToolNames
Expand Down Expand Up @@ -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{}
Expand All @@ -814,6 +820,7 @@ func expandActivatedTools(
toolSet,
acceptedToolNames,
filter,
toolSetToolNameModes,
)
if len(tools) == 0 {
log.DebugfContext(
Expand Down Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions agent/llmagent/tool_activation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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"),
Expand Down Expand Up @@ -472,6 +515,7 @@ func TestToolActivationIncludeReplacesExternalToolWithSameName(t *testing.T) {
},
nil,
nil,
nil,
)
require.Len(t, out, 1)
activated, ok := out[0].(*itool.NamedTool)
Expand Down Expand Up @@ -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]))
Expand All @@ -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,
))
}

Expand Down
76 changes: 76 additions & 0 deletions agent/llmagent/tool_name_mode.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading