fix: make LLM actually use spawn/subagent in orchestration mode

The LLM never called spawn even with --orchestration enabled due to
five compounding issues:

1. Identity said "helpful assistant" instead of "conductor" — reframed
   conditionally when orchestration is active
2. Plan executing instruction said "work through steps" — replaced with
   "delegate steps via spawn" in orchestration mode
3. SubagentTool (blocking) was referenced in guidance but never registered
   — now wired up alongside SpawnTool
4. No iteration reminder to use spawn — added orchestration nudge that
   fires on iteration 1 and every 3rd iteration during plan execution
5. Guidance was abstract with no examples — added preset table and
   concrete spawn/subagent call examples
6. Orchestration section in MEMORY.md was dropped from plan context —
   now extracted via existing extractSection()

All changes gated behind orchestrationEnabled / Subagents.Enabled.
Non-orchestration mode is completely unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-25 10:41:50 +09:00
parent ff0a6362f5
commit f96f535c78
3 changed files with 88 additions and 20 deletions

View file

@ -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: <description>
@ -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 {

View file

@ -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.

View file

@ -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()
}