refactor: extract context.go fork additions into separate files

Split ContextBuilder fork-specific code to reduce upstream merge conflicts:
- context_orch.go: orchestrationGuidance constant (~115 lines)
- context_plan.go: 15 plan passthrough methods
- context_ext.go: fork-specific fields (contextBuilderExt embedded struct),
  setter methods, memory accessors, and GetSkillsInfo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-13 16:06:27 +09:00
parent 4fbecbc047
commit ea2d48e907
4 changed files with 271 additions and 258 deletions

View file

@ -16,136 +16,17 @@ import (
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/skills" "github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
) )
const orchestrationGuidance = `## Orchestration
You are the conductor, not the performer. **Your primary job is to delegate, not to implement.**
### spawn (non-blocking) DEFAULT choice
Returns immediately. Use for any task that can run independently.
Call the spawn tool with JSON arguments like this:
Tool: spawn
Arguments: {"task": "Examine pkg/auth/ and report middleware pattern", "preset": "scout", "label": "auth-scout"}
Tool: spawn
Arguments: {"task": "Implement rate limiter in pkg/ratelimit/ with tests", "preset": "coder", "label": "rate-limiter"}
### subagent (blocking) only when you need the answer NOW
Blocks until the subagent finishes. Use only when you cannot proceed without the result.
Does not take a preset it runs with default tools.
Tool: subagent
Arguments: {"task": "Read pkg/config/config.go and list all SubagentsConfig fields", "label": "config-check"}
### When to use which
- spawn: parallel tasks, independent work, implementation, long analysis, >2 tool calls
- subagent: you need the result before your next decision
- inline: single quick tool call where delegation overhead is wasteful
### Presets (for spawn only)
| 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 |
### Parallel spawning
Spawn multiple independent tasks at once do NOT wait between them:
Tool: spawn
Arguments: {"task": "Analyze error handling patterns in pkg/providers/", "preset": "analyst", "label": "error-patterns"}
Tool: spawn
Arguments: {"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 findings and decide the next fork.
### Subagent escalation
Deliberate subagents (coder/worker/coordinator) may ask you questions or submit plans for review.
When a subagent question appears, respond with the appropriate tool:
- answer_subagent: Answer a subagent's clarifying question
- review_subagent_plan: Approve or reject a subagent's execution plan (decision: "approved" or rejection feedback)
### Orchestration Memory
Maintain these sections in MEMORY.md under ## Orchestration:
- **Delegated**: Active subagent assignments (task ID, preset, description)
- **Findings**: Synthesized results from completed subagents
- **Decisions**: Key architectural/implementation decisions made during orchestration`
type ContextBuilder struct { type ContextBuilder struct {
workspace string contextBuilderExt // fork-specific fields (see context_ext.go)
workDir string // session-specific working directory (worktree or project subdir)
skillsLoader *skills.SkillsLoader workspace string
memory *MemoryStore skillsLoader *skills.SkillsLoader
tools *tools.ToolRegistry // Direct reference to tool registry memory *MemoryStore
peerNote string // set per-call from loop.go for peer session awareness toolDiscoveryBM25 bool
orchestrationEnabled bool // set from AgentLoop when --orchestration flag is used toolDiscoveryRegex bool
toolDiscoveryBM25 bool
toolDiscoveryRegex bool
// Cache for system prompt to avoid rebuilding on every call. // Cache for system prompt to avoid rebuilding on every call.
// This fixes issue #607: repeated reprocessing of the entire context. // This fixes issue #607: repeated reprocessing of the entire context.
@ -200,27 +81,6 @@ func NewContextBuilder(workspace string) *ContextBuilder {
} }
} }
// SetToolsRegistry sets the tools registry for dynamic tool summary generation.
func (cb *ContextBuilder) SetToolsRegistry(registry *tools.ToolRegistry) {
cb.tools = registry
}
// SetWorkDir sets the session-specific working directory (e.g., worktree path
// or project subdirectory). Bootstrap files found here take priority over workspace.
func (cb *ContextBuilder) SetWorkDir(dir string) {
cb.workDir = dir
}
// SetPeerNote sets the peer session awareness note for the current call.
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 { func (cb *ContextBuilder) getIdentity() string {
workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace)) workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace))
toolDiscovery := cb.getDiscoveryRule() toolDiscovery := cb.getDiscoveryRule()
@ -1056,114 +916,3 @@ func (cb *ContextBuilder) LoadSkill(name string) (string, bool) {
func (cb *ContextBuilder) ListSkills() []skills.SkillInfo { func (cb *ContextBuilder) ListSkills() []skills.SkillInfo {
return cb.skillsLoader.ListSkills() return cb.skillsLoader.ListSkills()
} }
// Memory returns the underlying MemoryStore for direct plan queries.
func (cb *ContextBuilder) Memory() *MemoryStore {
return cb.memory
}
// ---------- Plan passthrough methods ----------
// ReadMemory reads the long-term memory (MEMORY.md).
func (cb *ContextBuilder) ReadMemory() string {
return cb.memory.ReadLongTerm()
}
// WriteMemory writes content to the long-term memory file.
func (cb *ContextBuilder) WriteMemory(content string) error {
return cb.memory.WriteLongTerm(content)
}
// ClearMemory removes the long-term memory file.
func (cb *ContextBuilder) ClearMemory() error {
return cb.memory.ClearLongTerm()
}
// HasActivePlan returns true if MEMORY.md contains an active plan.
func (cb *ContextBuilder) HasActivePlan() bool {
return cb.memory.HasActivePlan()
}
// GetPlanStatus returns the plan status: "interviewing", "executing", or "".
func (cb *ContextBuilder) GetPlanStatus() string {
return cb.memory.GetPlanStatus()
}
// IsPlanComplete returns true if all steps in all phases are [x].
func (cb *ContextBuilder) IsPlanComplete() bool {
return cb.memory.IsPlanComplete()
}
// IsCurrentPhaseComplete returns true if all steps in the current phase are [x].
func (cb *ContextBuilder) IsCurrentPhaseComplete() bool {
return cb.memory.IsCurrentPhaseComplete()
}
// AdvancePhase increments the current phase number by 1.
func (cb *ContextBuilder) AdvancePhase() error {
return cb.memory.AdvancePhase()
}
// SetCurrentPhase sets the current phase number to n.
func (cb *ContextBuilder) SetCurrentPhase(n int) error {
return cb.memory.SetPhase(n)
}
// GetCurrentPhase returns the current phase number.
func (cb *ContextBuilder) GetCurrentPhase() int {
return cb.memory.GetCurrentPhase()
}
// GetTotalPhases returns the total number of phases in the plan.
func (cb *ContextBuilder) GetTotalPhases() int {
return cb.memory.GetTotalPhases()
}
// FormatPlanDisplay returns a user-facing display of the full plan.
func (cb *ContextBuilder) FormatPlanDisplay() string {
return cb.memory.FormatPlanDisplay()
}
// MarkStep marks a step as done in the specified phase.
func (cb *ContextBuilder) MarkStep(phase, step int) error {
return cb.memory.MarkStep(phase, step)
}
// AddStep appends a new step to the given phase.
func (cb *ContextBuilder) AddStep(phase int, desc string) error {
return cb.memory.AddStep(phase, desc)
}
// ValidatePlanStructure validates plan structure for interview->review transition.
func (cb *ContextBuilder) ValidatePlanStructure() error {
return cb.memory.ValidatePlanStructure()
}
// SetPlanStatus sets the plan status.
func (cb *ContextBuilder) SetPlanStatus(status string) error {
return cb.memory.SetStatus(status)
}
// GetPlanWorkDir returns the WorkDir from the plan metadata, or "".
func (cb *ContextBuilder) GetPlanWorkDir() string {
return cb.memory.GetPlanWorkDir()
}
// GetPlanTaskName returns the task description from the plan metadata, or "".
func (cb *ContextBuilder) GetPlanTaskName() string {
return cb.memory.GetPlanTaskName()
}
// GetSkillsInfo returns information about loaded skills.
func (cb *ContextBuilder) GetSkillsInfo() map[string]any {
allSkills := cb.skillsLoader.ListSkills()
skillNames := make([]string, 0, len(allSkills))
for _, s := range allSkills {
skillNames = append(skillNames, s.Name)
}
return map[string]any{
"total": len(allSkills),
"available": len(allSkills),
"names": skillNames,
}
}

68
pkg/agent/context_ext.go Normal file
View file

@ -0,0 +1,68 @@
package agent
import "github.com/sipeed/picoclaw/pkg/tools"
// contextBuilderExt holds fork-specific fields for ContextBuilder.
// Embedded in ContextBuilder so existing field access (cb.workDir, cb.tools, etc.) continues to work.
// Upstream additions to ContextBuilder won't conflict with these fields.
type contextBuilderExt struct {
workDir string // session-specific working directory (worktree or project subdir)
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
}
// SetToolsRegistry sets the tools registry for dynamic tool summary generation.
func (cb *ContextBuilder) SetToolsRegistry(registry *tools.ToolRegistry) {
cb.tools = registry
}
// SetWorkDir sets the session-specific working directory (e.g., worktree path
// or project subdirectory). Bootstrap files found here take priority over workspace.
func (cb *ContextBuilder) SetWorkDir(dir string) {
cb.workDir = dir
}
// SetPeerNote sets the peer session awareness note for the current call.
func (cb *ContextBuilder) SetPeerNote(note string) {
cb.peerNote = note
}
// SetOrchestrationEnabled sets whether orchestration is enabled.
func (cb *ContextBuilder) SetOrchestrationEnabled(enabled bool) {
cb.orchestrationEnabled = enabled
}
// Memory returns the underlying MemoryStore for direct plan queries.
func (cb *ContextBuilder) Memory() *MemoryStore {
return cb.memory
}
// ReadMemory reads the long-term memory (MEMORY.md).
func (cb *ContextBuilder) ReadMemory() string {
return cb.memory.ReadLongTerm()
}
// WriteMemory writes content to the long-term memory file.
func (cb *ContextBuilder) WriteMemory(content string) error {
return cb.memory.WriteLongTerm(content)
}
// ClearMemory removes the long-term memory file.
func (cb *ContextBuilder) ClearMemory() error {
return cb.memory.ClearLongTerm()
}
// GetSkillsInfo returns information about loaded skills.
func (cb *ContextBuilder) GetSkillsInfo() map[string]any {
allSkills := cb.skillsLoader.ListSkills()
skillNames := make([]string, 0, len(allSkills))
for _, s := range allSkills {
skillNames = append(skillNames, s.Name)
}
return map[string]any{
"total": len(allSkills),
"available": len(allSkills),
"names": skillNames,
}
}

117
pkg/agent/context_orch.go Normal file
View file

@ -0,0 +1,117 @@
package agent
const orchestrationGuidance = `## Orchestration
You are the conductor, not the performer. **Your primary job is to delegate, not to implement.**
### spawn (non-blocking) DEFAULT choice
Returns immediately. Use for any task that can run independently.
Call the spawn tool with JSON arguments like this:
Tool: spawn
Arguments: {"task": "Examine pkg/auth/ and report middleware pattern", "preset": "scout", "label": "auth-scout"}
Tool: spawn
Arguments: {"task": "Implement rate limiter in pkg/ratelimit/ with tests", "preset": "coder", "label": "rate-limiter"}
### subagent (blocking) only when you need the answer NOW
Blocks until the subagent finishes. Use only when you cannot proceed without the result.
Does not take a preset it runs with default tools.
Tool: subagent
Arguments: {"task": "Read pkg/config/config.go and list all SubagentsConfig fields", "label": "config-check"}
### When to use which
- spawn: parallel tasks, independent work, implementation, long analysis, >2 tool calls
- subagent: you need the result before your next decision
- inline: single quick tool call where delegation overhead is wasteful
### Presets (for spawn only)
| 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 |
### Parallel spawning
Spawn multiple independent tasks at once do NOT wait between them:
Tool: spawn
Arguments: {"task": "Analyze error handling patterns in pkg/providers/", "preset": "analyst", "label": "error-patterns"}
Tool: spawn
Arguments: {"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 findings and decide the next fork.
### Subagent escalation
Deliberate subagents (coder/worker/coordinator) may ask you questions or submit plans for review.
When a subagent question appears, respond with the appropriate tool:
- answer_subagent: Answer a subagent's clarifying question
- review_subagent_plan: Approve or reject a subagent's execution plan (decision: "approved" or rejection feedback)
### Orchestration Memory
Maintain these sections in MEMORY.md under ## Orchestration:
- **Delegated**: Active subagent assignments (task ID, preset, description)
- **Findings**: Synthesized results from completed subagents
- **Decisions**: Key architectural/implementation decisions made during orchestration`

79
pkg/agent/context_plan.go Normal file
View file

@ -0,0 +1,79 @@
package agent
// ---------- Plan passthrough methods ----------
// These delegate to MemoryStore and are separated to reduce upstream conflicts.
// HasActivePlan returns true if MEMORY.md contains an active plan.
func (cb *ContextBuilder) HasActivePlan() bool {
return cb.memory.HasActivePlan()
}
// GetPlanStatus returns the plan status: "interviewing", "executing", or "".
func (cb *ContextBuilder) GetPlanStatus() string {
return cb.memory.GetPlanStatus()
}
// IsPlanComplete returns true if all steps in all phases are [x].
func (cb *ContextBuilder) IsPlanComplete() bool {
return cb.memory.IsPlanComplete()
}
// IsCurrentPhaseComplete returns true if all steps in the current phase are [x].
func (cb *ContextBuilder) IsCurrentPhaseComplete() bool {
return cb.memory.IsCurrentPhaseComplete()
}
// AdvancePhase increments the current phase number by 1.
func (cb *ContextBuilder) AdvancePhase() error {
return cb.memory.AdvancePhase()
}
// SetCurrentPhase sets the current phase number to n.
func (cb *ContextBuilder) SetCurrentPhase(n int) error {
return cb.memory.SetPhase(n)
}
// GetCurrentPhase returns the current phase number.
func (cb *ContextBuilder) GetCurrentPhase() int {
return cb.memory.GetCurrentPhase()
}
// GetTotalPhases returns the total number of phases in the plan.
func (cb *ContextBuilder) GetTotalPhases() int {
return cb.memory.GetTotalPhases()
}
// FormatPlanDisplay returns a user-facing display of the full plan.
func (cb *ContextBuilder) FormatPlanDisplay() string {
return cb.memory.FormatPlanDisplay()
}
// MarkStep marks a step as done in the specified phase.
func (cb *ContextBuilder) MarkStep(phase, step int) error {
return cb.memory.MarkStep(phase, step)
}
// AddStep appends a new step to the given phase.
func (cb *ContextBuilder) AddStep(phase int, desc string) error {
return cb.memory.AddStep(phase, desc)
}
// ValidatePlanStructure validates plan structure for interview->review transition.
func (cb *ContextBuilder) ValidatePlanStructure() error {
return cb.memory.ValidatePlanStructure()
}
// SetPlanStatus sets the plan status.
func (cb *ContextBuilder) SetPlanStatus(status string) error {
return cb.memory.SetStatus(status)
}
// GetPlanWorkDir returns the WorkDir from the plan metadata, or "".
func (cb *ContextBuilder) GetPlanWorkDir() string {
return cb.memory.GetPlanWorkDir()
}
// GetPlanTaskName returns the task description from the plan metadata, or "".
func (cb *ContextBuilder) GetPlanTaskName() string {
return cb.memory.GetPlanTaskName()
}