feat: implement preset feature for subagent orchestration

- Add pkg/tools/sandbox.go: Define 5 presets (scout/analyst/coder/worker/coordinator)
  with capability tiers, exec allowlists, and tool restrictions
- Add AllowedToolsForPreset() and SandboxConfigForPreset() factories
- Implement buildPresetRegistry() in SubagentManager to construct isolated tool
  registries per preset with appropriate restrictions
- Add preset parameter to SpawnTool and Spawn() method
- Pass WebSearchToolOptions to NewSubagentManager() constructor
- Add orchestration mode banner to system prompt (main agent only)
- Update all tests to use new SubagentManager and Spawn() signatures

Presets:
- scout: read-only exploration
- analyst: read + limited exec (test/vet/git)
- coder: read/write + test/lint exec
- worker: read/write + build/package manager exec
- coordinator: full exec + can spawn scout-worker presets

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-25 06:27:07 +09:00
parent 0574183659
commit bc3195fc8a
8 changed files with 546 additions and 33 deletions

View file

@ -40,12 +40,13 @@ After spawning, record the assignment in ## Orchestration > Delegated in MEMORY.
When results come back, synthesize and decide the next fork.`
type ContextBuilder struct {
workspace string
workDir string // session-specific working directory (worktree or project subdir)
skillsLoader *skills.SkillsLoader
memory *MemoryStore
tools *tools.ToolRegistry // Direct reference to tool registry
peerNote string // set per-call from loop.go for peer session awareness
workspace string
workDir string // session-specific working directory (worktree or project subdir)
skillsLoader *skills.SkillsLoader
memory *MemoryStore
tools *tools.ToolRegistry // Direct reference to tool registry
peerNote string // set per-call from loop.go for peer session awareness
orchestrationEnabled bool // set from AgentLoop when --orchestration flag is used
}
func getGlobalConfigDir() string {
@ -86,6 +87,11 @@ func (cb *ContextBuilder) SetPeerNote(note string) {
cb.peerNote = note
}
// SetOrchestrationEnabled sets whether orchestration is enabled.
func (cb *ContextBuilder) SetOrchestrationEnabled(enabled bool) {
cb.orchestrationEnabled = enabled
}
func (cb *ContextBuilder) getIdentity() string {
now := time.Now().Format("2006-01-02 15:04 (Monday)")
workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace))
@ -94,7 +100,16 @@ func (cb *ContextBuilder) getIdentity() string {
// Build tools section dynamically
toolsSection := cb.buildToolsSection()
return fmt.Sprintf(`# picoclaw 🦞
// Build prompt with optional orchestration banner
var prompt string
if cb.orchestrationEnabled {
prompt = `/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
O R C H E S T R A M O D E
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
`
}
return fmt.Sprintf(prompt+`# picoclaw 🦞
You are picoclaw, a helpful AI assistant.

View file

@ -261,7 +261,21 @@ func registerSharedTools(
// Spawn tool — only registered when orchestration is explicitly enabled.
if agent.Subagents != nil && agent.Subagents.Enabled {
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus, al.reporter())
webSearchOpts := tools.WebSearchToolOptions{
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
TavilyAPIKey: cfg.Tools.Web.Tavily.APIKey,
TavilyBaseURL: cfg.Tools.Web.Tavily.BaseURL,
TavilyMaxResults: cfg.Tools.Web.Tavily.MaxResults,
TavilyEnabled: cfg.Tools.Web.Tavily.Enabled,
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
PerplexityAPIKey: cfg.Tools.Web.Perplexity.APIKey,
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
}
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus, al.reporter(), webSearchOpts)
subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
spawnTool := tools.NewSpawnTool(subagentManager)
currentAgentID := agentID
@ -273,6 +287,11 @@ func registerSharedTools(
// Update context builder with the complete tools registry
agent.ContextBuilder.SetToolsRegistry(agent.Tools)
// Set orchestration mode if enabled
if agent.Subagents != nil && agent.Subagents.Enabled {
agent.ContextBuilder.SetOrchestrationEnabled(true)
}
}
}

125
pkg/tools/sandbox.go Normal file
View file

@ -0,0 +1,125 @@
package tools
// Preset defines the capability tier for a subagent.
type Preset string
const (
PresetScout Preset = "scout"
PresetAnalyst Preset = "analyst"
PresetCoder Preset = "coder"
PresetWorker Preset = "worker"
PresetCoordinator Preset = "coordinator"
)
// IsValidPreset checks if the given preset is a valid capability tier.
func IsValidPreset(p Preset) bool {
switch p {
case PresetScout, PresetAnalyst, PresetCoder, PresetWorker, PresetCoordinator:
return true
}
return false
}
// ExecPolicy defines which commands are allowed for execution.
type ExecPolicy struct {
AllowPattern string // Prefix-match regex; matched commands are allowed
}
// SandboxConfig describes the sandbox isolation policy for a preset.
type SandboxConfig struct {
Preset Preset
WriteRoot string // Path restriction for write tools; empty = no writes allowed
AllowedTools map[string]bool // Tools that can be used
ExecPolicy *ExecPolicy // nil = exec not allowed
SpawnablePresets []string // Presets that can be spawned; nil = spawn not allowed
}
// SubagentEnvironment provides context for subagent execution.
type SubagentEnvironment struct {
Workspace string // Absolute path to workspace
WorktreeDir string // Absolute path to worktree (write root)
Background string // Additional context/intent
Constraints string // Execution constraints
ContextFiles []string // Files to provide as context
}
// presetExecPatterns maps presets to command allowlist regexes.
var presetExecPatterns = map[Preset]string{
PresetScout: ``, // No exec allowed
PresetAnalyst: `^(go\s+(test|vet)|git\s+(log|diff|status)|curl|wget|grep|find)\b`,
PresetCoder: `^(go\s+(test|vet|fmt)|gofmt|goimports|golangci-lint|prettier|eslint|black|ruff|cargo\s+(test|fmt|clippy)|pnpm\s+(test|run\s+(test|lint|format))|bun\s+(test|run\s+(test|lint|format))|uv\s+run\s+)\b`,
PresetWorker: `^(go\s+|pnpm\s+(install|add|run|test|build)|bun\s+(install|add|run|test|build)|uv\s+(run|sync|add|pip\s+install)|pip\s+install|cargo\s+)\b`,
PresetCoordinator: `^(go\s+|pnpm\s+|bun\s+|curl|wget)\b`,
}
// presetSpawnablePresets maps presets to which presets they can spawn.
var presetSpawnablePresets = map[Preset][]string{
PresetScout: nil,
PresetAnalyst: nil,
PresetCoder: nil,
PresetWorker: nil,
PresetCoordinator: {"scout", "analyst", "coder", "worker"},
}
// AllowedToolsForPreset returns the set of allowed tools for a given preset.
func AllowedToolsForPreset(p Preset) map[string]bool {
// Base tools available to all presets
allowed := map[string]bool{
"read_file": true,
"list_dir": true,
"web_search": true,
"web_fetch": true,
"message": true,
}
// Add analyst+ tools (exec, git, etc.)
if p == PresetAnalyst || p == PresetCoder || p == PresetWorker || p == PresetCoordinator {
allowed["exec"] = true
}
// Add coder/worker/coordinator tools (write, bg_monitor)
if p == PresetCoder || p == PresetWorker || p == PresetCoordinator {
allowed["write_file"] = true
allowed["edit_file"] = true
allowed["append_file"] = true
allowed["bg_monitor"] = true
}
// Add coordinator-only tools (spawn)
if p == PresetCoordinator {
allowed["spawn"] = true
}
return allowed
}
// SandboxConfigForPreset creates a SandboxConfig for the given preset.
func SandboxConfigForPreset(p Preset, writeRoot string) SandboxConfig {
allowed := AllowedToolsForPreset(p)
config := SandboxConfig{
Preset: p,
AllowedTools: allowed,
}
// Only set WriteRoot for presets that have write permissions
if allowed["write_file"] {
config.WriteRoot = writeRoot
}
// Set ExecPolicy if exec is allowed and pattern is non-empty
if allowed["exec"] {
if pattern := presetExecPatterns[p]; pattern != "" {
config.ExecPolicy = &ExecPolicy{
AllowPattern: pattern,
}
}
}
// Set SpawnablePresets if spawn is allowed
if allowed["spawn"] {
config.SpawnablePresets = presetSpawnablePresets[p]
}
return config
}

254
pkg/tools/sandbox_test.go Normal file
View file

@ -0,0 +1,254 @@
package tools
import (
"regexp"
"testing"
)
// TestAllowedToolsForPreset checks that each preset has appropriate tool access.
func TestAllowedToolsForPreset(t *testing.T) {
tests := []struct {
name string
preset Preset
wantRead bool
wantWrite bool
wantExec bool
wantSpawn bool
wantWebSearch bool
}{
{
name: "scout",
preset: PresetScout,
wantRead: true,
wantWrite: false,
wantExec: false,
wantSpawn: false,
wantWebSearch: true,
},
{
name: "analyst",
preset: PresetAnalyst,
wantRead: true,
wantWrite: false,
wantExec: true,
wantSpawn: false,
wantWebSearch: true,
},
{
name: "coder",
preset: PresetCoder,
wantRead: true,
wantWrite: true,
wantExec: true,
wantSpawn: false,
wantWebSearch: true,
},
{
name: "worker",
preset: PresetWorker,
wantRead: true,
wantWrite: true,
wantExec: true,
wantSpawn: false,
wantWebSearch: true,
},
{
name: "coordinator",
preset: PresetCoordinator,
wantRead: true,
wantWrite: true,
wantExec: true,
wantSpawn: true,
wantWebSearch: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
allowed := AllowedToolsForPreset(tt.preset)
// Check read tools
if got := allowed["read_file"]; got != tt.wantRead {
t.Errorf("read_file: got %v, want %v", got, tt.wantRead)
}
if got := allowed["list_dir"]; got != tt.wantRead {
t.Errorf("list_dir: got %v, want %v", got, tt.wantRead)
}
// Check write tools
if got := allowed["write_file"]; got != tt.wantWrite {
t.Errorf("write_file: got %v, want %v", got, tt.wantWrite)
}
// Check exec
if got := allowed["exec"]; got != tt.wantExec {
t.Errorf("exec: got %v, want %v", got, tt.wantExec)
}
// Check spawn
if got := allowed["spawn"]; got != tt.wantSpawn {
t.Errorf("spawn: got %v, want %v", got, tt.wantSpawn)
}
// Check web_search (all presets should have it)
if got := allowed["web_search"]; got != tt.wantWebSearch {
t.Errorf("web_search: got %v, want %v", got, tt.wantWebSearch)
}
})
}
}
// TestSandboxConfigForPreset_Scout checks scout preset isolation.
func TestSandboxConfigForPreset_Scout(t *testing.T) {
config := SandboxConfigForPreset(PresetScout, "/tmp/scout")
if config.Preset != PresetScout {
t.Errorf("Preset: got %v, want %v", config.Preset, PresetScout)
}
if config.WriteRoot != "" {
t.Errorf("WriteRoot: got %q, want empty", config.WriteRoot)
}
if config.ExecPolicy != nil {
t.Errorf("ExecPolicy: got non-nil, want nil")
}
if config.SpawnablePresets != nil {
t.Errorf("SpawnablePresets: got non-nil, want nil")
}
}
// TestSandboxConfigForPreset_Coder checks coder preset isolation.
func TestSandboxConfigForPreset_Coder(t *testing.T) {
config := SandboxConfigForPreset(PresetCoder, "/tmp/coder")
if config.Preset != PresetCoder {
t.Errorf("Preset: got %v, want %v", config.Preset, PresetCoder)
}
if config.WriteRoot != "/tmp/coder" {
t.Errorf("WriteRoot: got %q, want /tmp/coder", config.WriteRoot)
}
if config.ExecPolicy == nil {
t.Errorf("ExecPolicy: got nil, want non-nil")
}
if config.SpawnablePresets != nil {
t.Errorf("SpawnablePresets: got non-nil, want nil")
}
}
// TestSandboxConfigForPreset_Coordinator checks coordinator preset isolation.
func TestSandboxConfigForPreset_Coordinator(t *testing.T) {
config := SandboxConfigForPreset(PresetCoordinator, "/tmp/coord")
if config.Preset != PresetCoordinator {
t.Errorf("Preset: got %v, want %v", config.Preset, PresetCoordinator)
}
if config.WriteRoot != "/tmp/coord" {
t.Errorf("WriteRoot: got %q, want /tmp/coord", config.WriteRoot)
}
if config.ExecPolicy == nil {
t.Errorf("ExecPolicy: got nil, want non-nil")
}
if config.SpawnablePresets == nil {
t.Errorf("SpawnablePresets: got nil, want non-nil")
}
// Verify coordinator cannot spawn itself
canSpawnCoordinator := false
for _, p := range config.SpawnablePresets {
if p == "coordinator" {
canSpawnCoordinator = true
break
}
}
if canSpawnCoordinator {
t.Errorf("Coordinator should not be in SpawnablePresets")
}
}
// TestPresetExecPatterns_Coder validates coder exec allowlist.
func TestPresetExecPatterns_Coder(t *testing.T) {
pattern, ok := presetExecPatterns[PresetCoder]
if !ok || pattern == "" {
t.Fatalf("coder pattern missing or empty")
}
re, err := regexp.Compile(pattern)
if err != nil {
t.Fatalf("failed to compile pattern: %v", err)
}
tests := []struct {
cmd string
wantOK bool
}{
{"go test ./...", true},
{"go vet ./...", true},
{"gofmt -w file.go", true},
{"golangci-lint run", true},
{"go build ./...", false},
{"npm install", false},
{"pnpm test", true},
}
for _, tt := range tests {
gotOK := re.MatchString(tt.cmd)
if gotOK != tt.wantOK {
t.Errorf("cmd %q: got %v, want %v", tt.cmd, gotOK, tt.wantOK)
}
}
}
// TestPresetExecPatterns_Analyst validates analyst exec allowlist.
func TestPresetExecPatterns_Analyst(t *testing.T) {
pattern, ok := presetExecPatterns[PresetAnalyst]
if !ok || pattern == "" {
t.Fatalf("analyst pattern missing or empty")
}
re, err := regexp.Compile(pattern)
if err != nil {
t.Fatalf("failed to compile pattern: %v", err)
}
tests := []struct {
cmd string
wantOK bool
}{
{"go test ./...", true},
{"go vet ./...", true},
{"git log --oneline", true},
{"git diff HEAD", true},
{"grep pattern file", true},
{"curl http://example.com", true},
{"go build ./...", false},
{"npm install", false},
}
for _, tt := range tests {
gotOK := re.MatchString(tt.cmd)
if gotOK != tt.wantOK {
t.Errorf("cmd %q: got %v, want %v", tt.cmd, gotOK, tt.wantOK)
}
}
}
// TestIsValidPreset checks preset validation.
func TestIsValidPreset(t *testing.T) {
tests := []struct {
preset Preset
valid bool
}{
{PresetScout, true},
{PresetAnalyst, true},
{PresetCoder, true},
{PresetWorker, true},
{PresetCoordinator, true},
{Preset("invalid"), false},
{Preset(""), false},
}
for _, tt := range tests {
gotValid := IsValidPreset(tt.preset)
if gotValid != tt.valid {
t.Errorf("preset %q: got %v, want %v", tt.preset, gotValid, tt.valid)
}
}
}

View file

@ -50,6 +50,11 @@ func (t *SpawnTool) Parameters() map[string]any {
"type": "string",
"description": "Optional target agent ID to delegate the task to",
},
"preset": map[string]any{
"type": "string",
"enum": []string{"scout", "analyst", "coder", "worker", "coordinator"},
"description": "Optional capability tier: scout (explore), analyst (analyze), coder (code), worker (build), coordinator (orchestrate)",
},
},
"required": []string{"task"},
}
@ -72,11 +77,16 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResul
label, _ := args["label"].(string)
agentID, _ := args["agent_id"].(string)
preset, _ := args["preset"].(string)
// Check allowlist if targeting a specific agent
if agentID != "" && t.allowlistCheck != nil {
if !t.allowlistCheck(agentID) {
return ErrorResult(fmt.Sprintf("not allowed to spawn agent '%s'", agentID))
// Check allowlist if targeting a specific agent or preset
checkTarget := agentID
if checkTarget == "" && preset != "" {
checkTarget = preset
}
if checkTarget != "" && t.allowlistCheck != nil {
if !t.allowlistCheck(checkTarget) {
return ErrorResult(fmt.Sprintf("not allowed to spawn agent '%s' or preset '%s'", agentID, preset))
}
}
@ -85,7 +95,7 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResul
}
// Pass callback to manager for async completion notification
result, err := t.manager.Spawn(ctx, task, label, agentID, t.originChannel, t.originChatID, t.callback)
result, err := t.manager.Spawn(ctx, task, label, agentID, t.originChannel, t.originChatID, preset, t.callback)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err))
}

View file

@ -31,6 +31,8 @@ type SubagentManager struct {
bus *bus.MessageBus
workspace string
tools *ToolRegistry
webSearchOpts WebSearchToolOptions
execTool *ExecTool // Shared exec tool for all presets
maxIterations int
maxTokens int
temperature float64
@ -45,10 +47,13 @@ func NewSubagentManager(
defaultModel, workspace string,
bus *bus.MessageBus,
reporter orch.AgentReporter,
webSearchOpts WebSearchToolOptions,
) *SubagentManager {
if reporter == nil {
reporter = orch.Noop
}
// Create a shared exec tool for all presets
execTool := NewExecTool(workspace, true)
return &SubagentManager{
tasks: make(map[string]*SubagentTask),
provider: provider,
@ -56,6 +61,8 @@ func NewSubagentManager(
bus: bus,
workspace: workspace,
tools: NewToolRegistry(),
webSearchOpts: webSearchOpts,
execTool: execTool,
maxIterations: 10,
nextID: 1,
reporter: reporter,
@ -89,7 +96,7 @@ func (sm *SubagentManager) RegisterTool(tool Tool) {
func (sm *SubagentManager) Spawn(
ctx context.Context,
task, label, agentID, originChannel, originChatID string,
task, label, agentID, originChannel, originChatID, preset string,
callback AsyncCallback,
) (string, error) {
sm.mu.Lock()
@ -113,7 +120,7 @@ func (sm *SubagentManager) Spawn(
sm.reporter.ReportSpawn(taskID, label, task)
// Start task in background with context cancellation support
go sm.runTask(ctx, subagentTask, callback)
go sm.runTask(ctx, subagentTask, preset, callback)
if label != "" {
return fmt.Sprintf("Spawned subagent '%s' for task: %s", label, task), nil
@ -121,14 +128,31 @@ func (sm *SubagentManager) Spawn(
return fmt.Sprintf("Spawned subagent for task: %s", task), nil
}
func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask, callback AsyncCallback) {
func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask, preset string, callback AsyncCallback) {
task.Status = "running"
// Build system prompt for subagent
// Build system prompt based on preset type
systemPrompt := `You are a subagent. Complete the given task independently and report the result.
You have access to tools - use them as needed to complete your task.
After completing the task, provide a clear summary of what was done.`
// Select prompt based on preset (exploratory vs deliberate)
p := Preset(preset)
if IsValidPreset(p) {
switch p {
case PresetScout, PresetAnalyst:
// Exploratory presets
systemPrompt = `You are an exploratory subagent. Investigate the task and report your findings.
Use your best judgment when encountering ambiguity. Use tools as needed.
Return clear findings and observations.`
case PresetCoder, PresetWorker, PresetCoordinator:
// Deliberate presets
systemPrompt = `You are a deliberate subagent. Complete the task methodically and verify your work.
Before executing significant actions, think through your approach.
After completing, provide a clear summary of what was done and how it was verified.`
}
}
messages := []providers.Message{
{
Role: "system",
@ -153,7 +177,11 @@ After completing the task, provide a clear summary of what was done.`
// Run tool loop with access to tools
sm.mu.RLock()
// Use preset registry if preset is valid, otherwise use default registry
tools := sm.tools
if IsValidPreset(p) {
tools = sm.buildPresetRegistry(p, sm.workspace)
}
maxIter := sm.maxIterations
maxTokens := sm.maxTokens
temperature := sm.temperature
@ -247,6 +275,68 @@ After completing the task, provide a clear summary of what was done.`
}
}
// buildPresetRegistry constructs a ToolRegistry for the given preset with appropriate restrictions.
func (sm *SubagentManager) buildPresetRegistry(preset Preset, writeRoot string) *ToolRegistry {
registry := NewToolRegistry()
config := SandboxConfigForPreset(preset, writeRoot)
readRoot := writeRoot
if readRoot == "" {
readRoot = sm.workspace
}
// Register read_file and list_dir with restrict=true
if config.AllowedTools["read_file"] {
registry.Register(NewReadFileTool(readRoot, true))
}
if config.AllowedTools["list_dir"] {
registry.Register(NewListDirTool(readRoot, true))
}
// Register write tools only if allowed and writeRoot is set
if config.AllowedTools["write_file"] && writeRoot != "" {
registry.Register(NewWriteFileTool(writeRoot, true))
registry.Register(NewEditFileTool(writeRoot, true))
registry.Register(NewAppendFileTool(writeRoot, true))
}
// Register exec and bg_monitor if allowed
if config.AllowedTools["exec"] {
// Use the shared exec tool but set allow patterns
execTool := sm.execTool
if config.ExecPolicy != nil {
_ = execTool.SetAllowPatterns([]string{config.ExecPolicy.AllowPattern})
}
registry.Register(execTool)
if config.AllowedTools["bg_monitor"] {
registry.Register(NewBgMonitorTool(execTool))
}
}
// Register web tools
if config.AllowedTools["web_search"] {
webSearchTool := NewWebSearchTool(sm.webSearchOpts)
if webSearchTool != nil {
registry.Register(webSearchTool)
}
}
if config.AllowedTools["web_fetch"] {
registry.Register(NewWebFetchTool(50000))
}
// Register message tool (always available)
registry.Register(NewMessageTool())
// Register spawn tool only for coordinator preset
if config.AllowedTools["spawn"] && preset == PresetCoordinator {
spawnTool := NewSpawnTool(sm)
registry.Register(spawnTool)
}
return registry
}
func (sm *SubagentManager) GetTask(taskID string) (*SubagentTask, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()

View file

@ -43,7 +43,7 @@ func TestSubagentManager_Spawn_EmitsLifecycleEvents(t *testing.T) {
defer b.Unsubscribe(sub)
provider := &MockLLMProvider{}
mgr := NewSubagentManager(provider, "test-model", "/tmp/test", nil, b)
mgr := NewSubagentManager(provider, "test-model", "/tmp/test", nil, b, WebSearchToolOptions{})
var callbackCalled int32
cb := AsyncCallback(func(_ context.Context, _ *ToolResult) {
@ -52,7 +52,7 @@ func TestSubagentManager_Spawn_EmitsLifecycleEvents(t *testing.T) {
_, err := mgr.Spawn(
context.Background(),
"say hello", "hello-task", "", "cli", "direct",
"say hello", "hello-task", "", "cli", "direct", "",
cb,
)
if err != nil {
@ -143,11 +143,11 @@ func TestSubagentManager_Spawn_SnapshotLiveDuringExecution(t *testing.T) {
defer b.Unsubscribe(sub)
provider := &MockLLMProvider{}
mgr := NewSubagentManager(provider, "test-model", "/tmp/test", nil, b)
mgr := NewSubagentManager(provider, "test-model", "/tmp/test", nil, b, WebSearchToolOptions{})
_, err := mgr.Spawn(
context.Background(),
"any task", "live-test", "", "cli", "direct",
"any task", "live-test", "", "cli", "direct", "",
nil,
)
if err != nil {
@ -188,12 +188,12 @@ func TestSubagentManager_Spawn_CancelledDuringExecution(t *testing.T) {
defer b.Unsubscribe(sub)
bp := newBlockingProvider()
mgr := NewSubagentManager(bp, "test-model", "/tmp/test", nil, b)
mgr := NewSubagentManager(bp, "test-model", "/tmp/test", nil, b, WebSearchToolOptions{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := mgr.Spawn(ctx, "long task", "cancel-me", "", "cli", "direct", nil)
_, err := mgr.Spawn(ctx, "long task", "cancel-me", "", "cli", "direct", "", nil)
if err != nil {
t.Fatalf("Spawn() error: %v", err)
}

View file

@ -48,7 +48,7 @@ func (m *MockLLMProvider) GetContextWindow() int {
func TestSubagentManager_SetLLMOptions_AppliesToRunToolLoop(t *testing.T) {
provider := &MockLLMProvider{}
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop, WebSearchToolOptions{})
manager.SetLLMOptions(2048, 0.6)
tool := NewSubagentTool(manager)
tool.SetContext("cli", "direct")
@ -75,7 +75,7 @@ func TestSubagentManager_SetLLMOptions_AppliesToRunToolLoop(t *testing.T) {
// TestSubagentTool_Name verifies tool name
func TestSubagentTool_Name(t *testing.T) {
provider := &MockLLMProvider{}
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
if tool.Name() != "subagent" {
@ -86,7 +86,7 @@ func TestSubagentTool_Name(t *testing.T) {
// TestSubagentTool_Description verifies tool description
func TestSubagentTool_Description(t *testing.T) {
provider := &MockLLMProvider{}
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
desc := tool.Description()
@ -101,7 +101,7 @@ func TestSubagentTool_Description(t *testing.T) {
// TestSubagentTool_Parameters verifies tool parameters schema
func TestSubagentTool_Parameters(t *testing.T) {
provider := &MockLLMProvider{}
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
params := tool.Parameters()
@ -151,7 +151,7 @@ func TestSubagentTool_Parameters(t *testing.T) {
// TestSubagentTool_SetContext verifies context setting
func TestSubagentTool_SetContext(t *testing.T) {
provider := &MockLLMProvider{}
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
tool.SetContext("test-channel", "test-chat")
@ -165,7 +165,7 @@ func TestSubagentTool_SetContext(t *testing.T) {
func TestSubagentTool_Execute_Success(t *testing.T) {
provider := &MockLLMProvider{}
msgBus := bus.NewMessageBus()
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
tool.SetContext("telegram", "chat-123")
@ -221,7 +221,7 @@ func TestSubagentTool_Execute_Success(t *testing.T) {
func TestSubagentTool_Execute_NoLabel(t *testing.T) {
provider := &MockLLMProvider{}
msgBus := bus.NewMessageBus()
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
ctx := context.Background()
@ -244,7 +244,7 @@ func TestSubagentTool_Execute_NoLabel(t *testing.T) {
// TestSubagentTool_Execute_MissingTask tests error handling for missing task
func TestSubagentTool_Execute_MissingTask(t *testing.T) {
provider := &MockLLMProvider{}
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
ctx := context.Background()
@ -295,7 +295,7 @@ func TestSubagentTool_Execute_NilManager(t *testing.T) {
func TestSubagentTool_Execute_ContextPassing(t *testing.T) {
provider := &MockLLMProvider{}
msgBus := bus.NewMessageBus()
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
// Set context
@ -324,7 +324,7 @@ func TestSubagentTool_ForUserTruncation(t *testing.T) {
// Create a mock provider that returns very long content
provider := &MockLLMProvider{}
msgBus := bus.NewMessageBus()
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop)
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus, orch.Noop, WebSearchToolOptions{})
tool := NewSubagentTool(manager)
ctx := context.Background()