diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 9ab10deb3..72cb0ae35 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -19,28 +19,48 @@ import ( const orchestrationGuidance = `## Orchestration -You are the conductor, not the performer. Prefer delegation over doing everything inline. +You are the conductor, not the performer. **Your primary job is to delegate, not to implement.** -Use **spawn** (non-blocking) when: -- Tasks can run in parallel or in the background -- Multiple independent tasks can run simultaneously — spawn each one -- You don't need the result to decide the next step -- The operation is long-running (builds, fetches, analysis, file processing) +### spawn (non-blocking) — default choice +- Tasks that can run in parallel or in the background +- Multiple independent tasks — spawn each one simultaneously +- Long-running operations (implementation, test suites, analysis) +- Any work that involves more than 2-3 tool calls -Use **subagent** (blocking) when: -- You need the result before you can continue -- Correctness of the next step depends on the outcome +### subagent (blocking) — when you need the answer now +- You need the result before deciding the next step +- Correctness of your next action depends on the outcome -Do inline only when: -- It's a single fast tool call (read a file, quick search) -- Delegation overhead clearly outweighs the benefit +### Inline — only for trivial operations +- A single fast tool call (read one file, quick search) +- Overhead of delegation clearly outweighs the benefit -Default bias: if a task involves more than 2-3 tool calls or can run independently, delegate it. -When you spawn, immediately plan what comes next — blocking means you've stopped thinking. -Fork aggressively: explore multiple directions simultaneously. +### Presets +| preset | role | can write | can exec | +|--------|------|-----------|----------| +| scout | explore, investigate | no | no | +| analyst | analyze, run tests | no | go test/vet, git | +| coder | implement + verify | yes (sandbox) | test/lint/fmt | +| worker | build + install | yes (sandbox) | build/package mgr | +| coordinator | orchestrate others | yes (sandbox) | general + spawn | + +### Examples + +Investigate code structure: + spawn(task: "Examine pkg/auth/ and report middleware pattern and entry points", preset: "scout", label: "auth-scout") + +Implement a feature: + spawn(task: "Implement rate limiter in pkg/ratelimit/ with tests. Run go test to verify.", preset: "coder", label: "rate-limiter") + +Get a blocking answer: + subagent(task: "Read pkg/config/config.go and list all SubagentsConfig fields", label: "config-check") + +Parallel exploration: + spawn(task: "Analyze error handling patterns in pkg/providers/", preset: "analyst", label: "error-patterns") + spawn(task: "List all HTTP endpoints in pkg/miniapp/", preset: "scout", label: "endpoints") After spawning, record the assignment in ## Orchestration > Delegated in MEMORY.md. -When results come back, synthesize and decide the next fork.` +When results come back, synthesize findings and decide the next fork.` type ContextBuilder struct { workspace string @@ -123,9 +143,23 @@ func (cb *ContextBuilder) getIdentity() string { ` } + // Conditional identity and plan executing rule for orchestration mode + identity := "a helpful AI assistant" + executingRule := `Work through the current Phase's steps. + Mark each "- [x]" via edit_file. The system will auto-advance phases.` + if cb.orchestrationEnabled { + identity = "a conductor AI agent that orchestrates subagents" + executingRule = `Delegate the current Phase's steps to subagents using spawn. + For each step: spawn a subagent with the appropriate preset (scout for investigation, + coder for implementation, analyst for review). Spawn multiple independent steps in parallel. + When a subagent completes, mark "- [x]" via edit_file and record findings in + ## Orchestration > Findings in MEMORY.md. + Only do a step inline if it's a single quick tool call (e.g., reading one file).` + } + return fmt.Sprintf(prompt+`# picoclaw 🦞 -You are picoclaw, a helpful AI assistant. +You are picoclaw, %s. ## Workspace Your workspace is at: %s @@ -148,8 +182,7 @@ Your workspace is at: %s After each answer, use edit_file to save findings to ## Context in memory/MEMORY.md. When you have enough information, add ## Phase sections with "- [ ]" checkbox steps, and ## Commands section below the header. Then change > Status: to "review". - If Status is "review": The plan is awaiting user approval. Do NOT change Status yourself. - - If Status is "executing": Work through the current Phase's steps. - Mark each "- [x]" via edit_file. The system will auto-advance phases. + - If Status is "executing": %s - Plan format (header is written by the system — do NOT delete it): # Active Plan > Task: @@ -176,7 +209,7 @@ Your workspace is at: %s - For architecture/flow, use arrow text: CLI → Pipeline → Adapters 5. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.`, - workspacePath, workspacePath, workspacePath, workspacePath, toolsSection) + identity, workspacePath, workspacePath, workspacePath, workspacePath, toolsSection, executingRule) } func (cb *ContextBuilder) buildToolsSection() string { diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index c88701f24..d8910d6be 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -302,6 +302,9 @@ func registerSharedTools( return registry.CanSpawnSubagent(currentAgentID, targetAgentID) }) agent.Tools.Register(spawnTool) + // Register blocking subagent tool alongside spawn + subagentTool := tools.NewSubagentTool(subagentManager) + agent.Tools.Register(subagentTool) } // Update context builder with the complete tools registry @@ -1259,6 +1262,18 @@ func buildPlanReminder(planStatus string) (providers.Message, bool) { return providers.Message{Role: "user", Content: content}, true } +// buildOrchReminder returns a reminder to use spawn/subagent during plan execution. +// Fires on first iteration and every 3rd iteration to reinforce delegation behavior. +func buildOrchReminder(iteration int) (providers.Message, bool) { + if iteration != 1 && iteration%3 != 0 { + return providers.Message{}, false + } + content := "[System] ORCHESTRATION mode active. Delegate plan steps to subagents using spawn (async) or subagent (blocking). " + + "Do NOT implement steps inline unless they are trivial single-tool-call tasks. " + + "Spawn multiple independent steps in parallel for maximum throughput." + return providers.Message{Role: "user", Content: content}, true +} + // cdPrefixPattern matches "cd /some/path && " at the start of a shell command. // Group 1 captures the target directory path. var cdPrefixPattern = regexp.MustCompile(`^cd\s+(\S+)\s*&&\s*`) @@ -2594,6 +2609,18 @@ func (al *AgentLoop) runLLMIteration( } } + // Inject orchestration nudge during plan execution to encourage spawn usage. + if planSnapshot == "executing" && agent.Subagents != nil && agent.Subagents.Enabled { + if reminder, ok := buildOrchReminder(iteration); ok { + messages = append(messages, reminder) + logger.DebugCF("agent", "Injected orchestration nudge", + map[string]interface{}{ + "agent_id": agent.ID, + "iteration": iteration, + }) + } + } + // Refresh system prompt: tool execution may have changed workDir, // memory, plan status, etc. Update messages[0] so the next LLM // call sees the current state. diff --git a/pkg/agent/memory.go b/pkg/agent/memory.go index 7245f3952..6c49d599f 100644 --- a/pkg/agent/memory.go +++ b/pkg/agent/memory.go @@ -630,6 +630,14 @@ func (ms *MemoryStore) getPlanContextFrom(content string) string { sb.WriteString("\n") } + // Orchestration section: conductor's delegation tracking (Delegated/Findings/Decisions) + orchContent := ms.extractSection(content, "Orchestration") + if orchContent != "" { + sb.WriteString("### Orchestration\n") + sb.WriteString(orchContent) + sb.WriteString("\n") + } + return sb.String() }