Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions pkg/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ func registerSharedTools(
if (spawnEnabled || spawnStatusEnabled) && cfg.Tools.IsToolEnabled("subagent") {
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace)
subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
// Clone the parent's tool registry so subagents can use all
// tools registered so far (file, web, etc.) but NOT spawn/
// spawn_status which are added below — preventing recursive
// subagent spawning.
subagentManager.SetTools(agent.Tools.Clone())
if spawnEnabled {
spawnTool := tools.NewSpawnTool(subagentManager)
currentAgentID := agentID
Expand Down
21 changes: 21 additions & 0 deletions pkg/tools/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,27 @@ func (r *ToolRegistry) List() []string {
return r.sortedToolNames()
}

// Clone creates an independent copy of the registry containing the same tool
// entries (shallow copy of each ToolEntry). This is used to give subagents a
// snapshot of the parent agent's tools without sharing the same registry —
// tools registered on the parent after cloning (e.g. spawn, spawn_status)
// will NOT be visible to the clone, preventing recursive subagent spawning.
func (r *ToolRegistry) Clone() *ToolRegistry {
r.mu.RLock()
defer r.mu.RUnlock()
clone := &ToolRegistry{
tools: make(map[string]*ToolEntry, len(r.tools)),
}
for name, entry := range r.tools {
clone.tools[name] = &ToolEntry{
Tool: entry.Tool,
IsCore: entry.IsCore,
TTL: entry.TTL,
}
}
return clone
}

// Count returns the number of registered tools.
func (r *ToolRegistry) Count() int {
r.mu.RLock()
Expand Down
65 changes: 65 additions & 0 deletions pkg/tools/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,71 @@ func TestToolToSchema(t *testing.T) {
}
}

func TestToolRegistry_Clone(t *testing.T) {
r := NewToolRegistry()
r.Register(newMockTool("read_file", "reads files"))
r.Register(newMockTool("exec", "runs commands"))
r.Register(newMockTool("web_search", "searches the web"))

clone := r.Clone()

// Clone should have the same tools
if clone.Count() != 3 {
t.Errorf("expected clone to have 3 tools, got %d", clone.Count())
}
for _, name := range []string{"read_file", "exec", "web_search"} {
if _, ok := clone.Get(name); !ok {
t.Errorf("expected clone to have tool %q", name)
}
}

// Registering on parent should NOT affect clone
r.Register(newMockTool("spawn", "spawns subagent"))
if r.Count() != 4 {
t.Errorf("expected parent to have 4 tools, got %d", r.Count())
}
if clone.Count() != 3 {
t.Errorf("expected clone to still have 3 tools after parent mutation, got %d", clone.Count())
}
if _, ok := clone.Get("spawn"); ok {
t.Error("expected clone NOT to have 'spawn' tool registered on parent after cloning")
}

// Registering on clone should NOT affect parent
clone.Register(newMockTool("custom", "custom tool"))
if clone.Count() != 4 {
t.Errorf("expected clone to have 4 tools, got %d", clone.Count())
}
if _, ok := r.Get("custom"); ok {
t.Error("expected parent NOT to have 'custom' tool registered on clone")
}
}

func TestToolRegistry_Clone_Empty(t *testing.T) {
r := NewToolRegistry()
clone := r.Clone()
if clone.Count() != 0 {
t.Errorf("expected empty clone, got count %d", clone.Count())
}
}

func TestToolRegistry_Clone_PreservesHiddenToolState(t *testing.T) {
r := NewToolRegistry()
r.RegisterHidden(newMockTool("mcp_tool", "dynamic MCP tool"))

clone := r.Clone()

// Hidden tools with TTL=0 should not be gettable (same behavior as parent)
if _, ok := clone.Get("mcp_tool"); ok {
t.Error("expected hidden tool with TTL=0 to be invisible in clone")
}

// But the entry should exist (count includes hidden tools)
if clone.Count() != 1 {
t.Errorf("expected clone count 1 (hidden entry exists), got %d", clone.Count())
}
}

func TestToolRegistry_ConcurrentAccess(t *testing.T) {
r := NewToolRegistry()
var wg sync.WaitGroup
Expand Down
Loading