Skip to content

Commit 681453c

Browse files
authored
agent: allow dynamic toolsets (#795)
1 parent edc95b4 commit 681453c

File tree

9 files changed

+741
-55
lines changed

9 files changed

+741
-55
lines changed

agent/llmagent/llm_agent.go

Lines changed: 143 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,18 @@ func defaultCodeExecutor() codeexecutor.CodeExecutor {
4747

4848
// LLMAgent is an agent that uses an LLM to generate responses.
4949
type LLMAgent struct {
50-
name string
51-
mu sync.RWMutex
52-
model model.Model
53-
models map[string]model.Model // Registered models for switching
54-
description string
55-
instruction string
56-
systemPrompt string
57-
genConfig model.GenerationConfig
58-
flow flow.Flow
59-
tools []tool.Tool // All tools (user tools + framework tools)
60-
userToolNames map[string]bool // Names of tools explicitly registered by user via WithTools and WithToolSets
50+
name string
51+
mu sync.RWMutex
52+
model model.Model
53+
models map[string]model.Model // Registered models for switching
54+
description string
55+
instruction string
56+
systemPrompt string
57+
genConfig model.GenerationConfig
58+
flow flow.Flow
59+
tools []tool.Tool // All tools (user tools + framework tools)
60+
userToolNames map[string]bool // Names of tools explicitly registered
61+
// via WithTools and WithToolSets.
6162
codeExecutor codeexecutor.CodeExecutor
6263
planner planner.Planner
6364
subAgents []agent.Agent // Sub-agents that can be delegated to
@@ -587,21 +588,34 @@ func (a *LLMAgent) Info() agent.Info {
587588
}
588589
}
589590

590-
// Tools implements the agent.Agent interface.
591-
// It returns the list of tools available to the agent, including transfer tools.
592-
func (a *LLMAgent) Tools() []tool.Tool {
591+
// getAllToolsLocked builds the full tool list (user tools plus framework
592+
// tools like transfer_to_agent) under the caller's read lock. It always
593+
// returns a fresh slice so callers can safely use it after releasing the
594+
// lock without data races.
595+
func (a *LLMAgent) getAllToolsLocked() []tool.Tool {
596+
tools := make([]tool.Tool, len(a.tools))
597+
copy(tools, a.tools)
598+
593599
if len(a.subAgents) == 0 {
594-
return a.tools
600+
return tools
595601
}
596602

597-
// Create agent info for sub-agents.
598603
agentInfos := make([]agent.Info, len(a.subAgents))
599604
for i, subAgent := range a.subAgents {
600605
agentInfos[i] = subAgent.Info()
601606
}
602607

603608
transferTool := transfer.New(agentInfos)
604-
return append(a.tools, transferTool)
609+
return append(tools, transferTool)
610+
}
611+
612+
// Tools implements the agent.Agent interface. It returns the list of
613+
// tools available to the agent, including transfer tools.
614+
func (a *LLMAgent) Tools() []tool.Tool {
615+
a.mu.RLock()
616+
defer a.mu.RUnlock()
617+
618+
return a.getAllToolsLocked()
605619
}
606620

607621
// SubAgents returns the list of sub-agents for this agent.
@@ -620,20 +634,24 @@ func (a *LLMAgent) FindSubAgent(name string) agent.Agent {
620634
return nil
621635
}
622636

623-
// UserTools returns the list of tools that were explicitly registered by the user
624-
// via WithTools and WithToolSets options.
637+
// UserTools returns the list of tools that were explicitly registered
638+
// by the user via WithTools and WithToolSets options.
625639
//
626640
// User tools (can be filtered):
627641
// - Tools registered via WithTools
628642
// - Tools registered via WithToolSets
629643
//
630644
// Framework tools (never filtered, not included in this list):
631-
// - knowledge_search / agentic_knowledge_search (auto-added when WithKnowledge is set)
645+
// - knowledge_search / agentic_knowledge_search (auto-added when
646+
// WithKnowledge is set)
632647
// - transfer_to_agent (auto-added when WithSubAgents is set)
633648
//
634-
// This method is used by the tool filtering logic to distinguish user tools from framework tools.
649+
// This method is used by the tool filtering logic to distinguish user
650+
// tools from framework tools.
635651
func (a *LLMAgent) UserTools() []tool.Tool {
636-
// Filter user tools from all tools
652+
a.mu.RLock()
653+
defer a.mu.RUnlock()
654+
637655
userTools := make([]tool.Tool, 0, len(a.userToolNames))
638656
for _, t := range a.tools {
639657
if a.userToolNames[t.Declaration().Name] {
@@ -645,20 +663,29 @@ func (a *LLMAgent) UserTools() []tool.Tool {
645663

646664
// FilterTools filters the list of tools based on the provided filter function.
647665
func (a *LLMAgent) FilterTools(ctx context.Context) []tool.Tool {
648-
filteredTools := make([]tool.Tool, 0, len(a.tools))
666+
a.mu.RLock()
667+
tools := a.getAllToolsLocked()
668+
userToolNames := make(map[string]bool, len(a.userToolNames))
669+
for name, isUser := range a.userToolNames {
670+
userToolNames[name] = isUser
671+
}
672+
filter := a.option.toolFilter
673+
a.mu.RUnlock()
649674

650-
for _, t := range a.Tools() {
651-
if !a.userToolNames[t.Declaration().Name] {
652-
filteredTools = append(filteredTools, t)
675+
filtered := make([]tool.Tool, 0, len(tools))
676+
for _, t := range tools {
677+
name := t.Declaration().Name
678+
if !userToolNames[name] {
679+
filtered = append(filtered, t)
653680
continue
654681
}
655-
// Apply user tool filter
656-
if a.option.toolFilter == nil || a.option.toolFilter(ctx, t) {
657-
filteredTools = append(filteredTools, t)
682+
683+
if filter == nil || filter(ctx, t) {
684+
filtered = append(filtered, t)
658685
}
659686
}
660687

661-
return filteredTools
688+
return filtered
662689
}
663690

664691
// CodeExecutor returns the code executor used by this agent.
@@ -668,6 +695,92 @@ func (a *LLMAgent) CodeExecutor() codeexecutor.CodeExecutor {
668695
return a.codeExecutor
669696
}
670697

698+
// refreshToolsLocked recomputes the aggregated tool list and user tool
699+
// tracking map from the current options. Caller must hold a.mu.Lock.
700+
func (a *LLMAgent) refreshToolsLocked() {
701+
tools, userToolNames := registerTools(&a.option)
702+
a.tools = tools
703+
a.userToolNames = userToolNames
704+
}
705+
706+
// AddToolSet adds or replaces a tool set at runtime in a
707+
// concurrency-safe way. If another ToolSet with the same Name()
708+
// already exists, it will be replaced. Subsequent invocations of the
709+
// agent will see the updated tool list without recreating the agent.
710+
func (a *LLMAgent) AddToolSet(toolSet tool.ToolSet) {
711+
if toolSet == nil {
712+
return
713+
}
714+
715+
name := toolSet.Name()
716+
717+
a.mu.Lock()
718+
defer a.mu.Unlock()
719+
720+
replaced := false
721+
for i, ts := range a.option.ToolSets {
722+
if name != "" && ts.Name() == name {
723+
a.option.ToolSets[i] = toolSet
724+
replaced = true
725+
break
726+
}
727+
}
728+
if !replaced {
729+
a.option.ToolSets = append(a.option.ToolSets, toolSet)
730+
}
731+
732+
a.refreshToolsLocked()
733+
}
734+
735+
// RemoveToolSet removes all tool sets whose Name() matches the given
736+
// name. It returns true if at least one ToolSet was removed. Tools
737+
// from the removed tool sets will no longer be exposed on future
738+
// invocations.
739+
func (a *LLMAgent) RemoveToolSet(name string) bool {
740+
a.mu.Lock()
741+
defer a.mu.Unlock()
742+
743+
if len(a.option.ToolSets) == 0 {
744+
return false
745+
}
746+
747+
dst := a.option.ToolSets[:0]
748+
removed := false
749+
for _, ts := range a.option.ToolSets {
750+
if ts.Name() == name {
751+
removed = true
752+
continue
753+
}
754+
dst = append(dst, ts)
755+
}
756+
if !removed {
757+
return false
758+
}
759+
a.option.ToolSets = dst
760+
761+
a.refreshToolsLocked()
762+
763+
return true
764+
}
765+
766+
// SetToolSets replaces the agent ToolSets with the provided slice in a
767+
// concurrency-safe way. Subsequent invocations will see tools from
768+
// exactly these ToolSets plus framework tools (knowledge, skills).
769+
func (a *LLMAgent) SetToolSets(toolSets []tool.ToolSet) {
770+
a.mu.Lock()
771+
defer a.mu.Unlock()
772+
773+
if len(toolSets) == 0 {
774+
a.option.ToolSets = nil
775+
} else {
776+
copied := make([]tool.ToolSet, len(toolSets))
777+
copy(copied, toolSets)
778+
a.option.ToolSets = copied
779+
}
780+
781+
a.refreshToolsLocked()
782+
}
783+
671784
// SetModel sets the model for this agent in a concurrency-safe way.
672785
// This allows callers to manage multiple models externally and switch
673786
// dynamically during runtime.

0 commit comments

Comments
 (0)