refactor: extract instance.go fork fields into instanceExt embedded struct

Move fork-specific AgentInstance fields and worktree methods to instance_ext.go:
- SubagentMgr, Subagents, SkillsFilter
- Interview staleness tracking fields
- Per-session worktree isolation (worktrees map, all worktree methods)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-13 16:06:52 +09:00
parent 6fc07c4409
commit e99ff8b96b
2 changed files with 128 additions and 105 deletions

View file

@ -7,7 +7,6 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/git" "github.com/sipeed/picoclaw/pkg/git"
@ -20,6 +19,8 @@ import (
// AgentInstance represents a fully configured agent with its own workspace, // AgentInstance represents a fully configured agent with its own workspace,
// session manager, context builder, and tool registry. // session manager, context builder, and tool registry.
type AgentInstance struct { type AgentInstance struct {
instanceExt // fork-specific fields (see instance_ext.go)
ID string ID string
Name string Name string
Model string Model string
@ -37,8 +38,6 @@ type AgentInstance struct {
Sessions *session.LegacyAdapter Sessions *session.LegacyAdapter
ContextBuilder *ContextBuilder ContextBuilder *ContextBuilder
Tools *tools.ToolRegistry Tools *tools.ToolRegistry
Subagents *config.SubagentsConfig
SkillsFilter []string
Candidates []providers.FallbackCandidate Candidates []providers.FallbackCandidate
PlanModel string PlanModel string
PlanFallbacks []string PlanFallbacks []string
@ -51,18 +50,6 @@ type AgentInstance struct {
// LightCandidates holds the resolved provider candidates for the light model. // LightCandidates holds the resolved provider candidates for the light model.
// Pre-computed at agent creation to avoid repeated model_list lookups at runtime. // Pre-computed at agent creation to avoid repeated model_list lookups at runtime.
LightCandidates []providers.FallbackCandidate LightCandidates []providers.FallbackCandidate
// SubagentMgr is set during registerSharedTools when orchestration is enabled.
// Used by runAgentLoop to wait for spawned subagents before worktree cleanup.
SubagentMgr *tools.SubagentManager
// Interview staleness tracking: consecutive turns where MEMORY.md was not updated.
interviewStaleCount int
interviewMemoryLen int
// Per-session worktree isolation
worktrees map[string]*git.WorktreeInfo // sessionKey → worktree
worktreeMu sync.RWMutex
} }
// Close releases resources held by the agent instance. // Close releases resources held by the agent instance.
@ -295,6 +282,10 @@ func NewAgentInstance(
} }
return &AgentInstance{ return &AgentInstance{
instanceExt: instanceExt{
Subagents: subagents,
SkillsFilter: skillsFilter,
},
ID: agentID, ID: agentID,
Name: agentName, Name: agentName,
Model: model, Model: model,
@ -312,8 +303,6 @@ func NewAgentInstance(
Sessions: sessionsManager, Sessions: sessionsManager,
ContextBuilder: contextBuilder, ContextBuilder: contextBuilder,
Tools: toolsRegistry, Tools: toolsRegistry,
Subagents: subagents,
SkillsFilter: skillsFilter,
Candidates: candidates, Candidates: candidates,
PlanModel: planModel, PlanModel: planModel,
PlanFallbacks: planFallbacks, PlanFallbacks: planFallbacks,
@ -370,94 +359,6 @@ func resolvePlanFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentDe
return defaults.PlanModelFallbacks return defaults.PlanModelFallbacks
} }
// ActivateWorktree creates a worktree for a session.
// projectDir is the git repository to create the worktree in.
// If empty, falls back to ai.Workspace.
// Worktree path: <workspace>/.worktrees/<branch-basename>/
func (ai *AgentInstance) ActivateWorktree(sessionKey, taskName, projectDir string) (*git.WorktreeInfo, error) {
if projectDir == "" {
projectDir = ai.Workspace
}
repoRoot := git.FindRepoRoot(projectDir)
if repoRoot == "" {
return nil, fmt.Errorf("directory is not a git repository: %s", projectDir)
}
branchName := git.SanitizeBranchName(taskName)
baseName := git.BranchBaseName(branchName)
wtPath := filepath.Join(ai.Workspace, ".worktrees", baseName)
wt, err := git.CreateWorktree(repoRoot, wtPath, branchName)
if err != nil {
return nil, err
}
ai.worktreeMu.Lock()
if ai.worktrees == nil {
ai.worktrees = make(map[string]*git.WorktreeInfo)
}
ai.worktrees[sessionKey] = wt
ai.worktreeMu.Unlock()
return wt, nil
}
// DeactivateWorktree safe-disposes the session's worktree.
func (ai *AgentInstance) DeactivateWorktree(sessionKey, commitMsg string, discard bool) (*git.DisposeResult, error) {
ai.worktreeMu.Lock()
wt, ok := ai.worktrees[sessionKey]
if ok {
delete(ai.worktrees, sessionKey)
}
ai.worktreeMu.Unlock()
if !ok || wt == nil {
return nil, nil
}
repoRoot := git.FindRepoRoot(ai.Workspace)
if repoRoot == "" {
return nil, fmt.Errorf("workspace is not a git repository")
}
// Even on discard, SafeDispose auto-commits first for safety
if commitMsg != "" && git.HasUncommittedChanges(wt.Path) {
_ = git.AutoCommit(wt.Path, commitMsg)
}
result := git.SafeDispose(repoRoot, wt)
return &result, nil
}
// GetWorktree returns the session's active worktree, or nil.
func (ai *AgentInstance) GetWorktree(sessionKey string) *git.WorktreeInfo {
ai.worktreeMu.RLock()
defer ai.worktreeMu.RUnlock()
return ai.worktrees[sessionKey]
}
// IsInWorktree returns true if the session has an active worktree.
func (ai *AgentInstance) IsInWorktree(sessionKey string) bool {
return ai.GetWorktree(sessionKey) != nil
}
// EffectiveWorkspace returns worktree path for session, or original Workspace.
func (ai *AgentInstance) EffectiveWorkspace(sessionKey string) string {
if wt := ai.GetWorktree(sessionKey); wt != nil {
return wt.Path
}
return ai.Workspace
}
// GetWorktreeBranch returns the branch name for the session's worktree, or "".
func (ai *AgentInstance) GetWorktreeBranch(sessionKey string) string {
if wt := ai.GetWorktree(sessionKey); wt != nil {
return wt.Branch
}
return ""
}
func compilePatterns(patterns []string) []*regexp.Regexp { func compilePatterns(patterns []string) []*regexp.Regexp {
compiled := make([]*regexp.Regexp, 0, len(patterns)) compiled := make([]*regexp.Regexp, 0, len(patterns))
for _, p := range patterns { for _, p := range patterns {

122
pkg/agent/instance_ext.go Normal file
View file

@ -0,0 +1,122 @@
package agent
import (
"fmt"
"sync"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/git"
"github.com/sipeed/picoclaw/pkg/tools"
)
// instanceExt holds fork-specific fields for AgentInstance.
// Embedded in AgentInstance so existing field access continues to work.
type instanceExt struct {
// SubagentMgr is set during registerSharedTools when orchestration is enabled.
// Used by runAgentLoop to wait for spawned subagents before worktree cleanup.
SubagentMgr *tools.SubagentManager
Subagents *config.SubagentsConfig
SkillsFilter []string
// Interview staleness tracking: consecutive turns where MEMORY.md was not updated.
interviewStaleCount int
interviewMemoryLen int
// Per-session worktree isolation
worktrees map[string]*git.WorktreeInfo // sessionKey → worktree
worktreeMu sync.RWMutex
}
// ActivateWorktree creates a worktree for a session.
// projectDir is the git repository to create the worktree in.
// If empty, falls back to ai.Workspace.
// Worktree path: <workspace>/.worktrees/<branch-basename>/
func (ai *AgentInstance) ActivateWorktree(sessionKey, taskName, projectDir string) (*git.WorktreeInfo, error) {
if projectDir == "" {
projectDir = ai.Workspace
}
repoRoot := git.FindRepoRoot(projectDir)
if repoRoot == "" {
return nil, fmt.Errorf("directory is not a git repository: %s", projectDir)
}
branchName := git.SanitizeBranchName(taskName)
baseName := git.BranchBaseName(branchName)
wtPath := ai.worktreePath(baseName)
wt, err := git.CreateWorktree(repoRoot, wtPath, branchName)
if err != nil {
return nil, err
}
ai.worktreeMu.Lock()
if ai.worktrees == nil {
ai.worktrees = make(map[string]*git.WorktreeInfo)
}
ai.worktrees[sessionKey] = wt
ai.worktreeMu.Unlock()
return wt, nil
}
// worktreePath returns the standard path for a worktree under the workspace.
func (ai *AgentInstance) worktreePath(baseName string) string {
return ai.Workspace + "/.worktrees/" + baseName
}
// DeactivateWorktree safe-disposes the session's worktree.
func (ai *AgentInstance) DeactivateWorktree(sessionKey, commitMsg string, discard bool) (*git.DisposeResult, error) {
ai.worktreeMu.Lock()
wt, ok := ai.worktrees[sessionKey]
if ok {
delete(ai.worktrees, sessionKey)
}
ai.worktreeMu.Unlock()
if !ok || wt == nil {
return nil, nil
}
repoRoot := git.FindRepoRoot(ai.Workspace)
if repoRoot == "" {
return nil, fmt.Errorf("workspace is not a git repository")
}
// Even on discard, SafeDispose auto-commits first for safety
if commitMsg != "" && git.HasUncommittedChanges(wt.Path) {
_ = git.AutoCommit(wt.Path, commitMsg)
}
result := git.SafeDispose(repoRoot, wt)
return &result, nil
}
// GetWorktree returns the session's active worktree, or nil.
func (ai *AgentInstance) GetWorktree(sessionKey string) *git.WorktreeInfo {
ai.worktreeMu.RLock()
defer ai.worktreeMu.RUnlock()
return ai.worktrees[sessionKey]
}
// IsInWorktree returns true if the session has an active worktree.
func (ai *AgentInstance) IsInWorktree(sessionKey string) bool {
return ai.GetWorktree(sessionKey) != nil
}
// EffectiveWorkspace returns worktree path for session, or original Workspace.
func (ai *AgentInstance) EffectiveWorkspace(sessionKey string) string {
if wt := ai.GetWorktree(sessionKey); wt != nil {
return wt.Path
}
return ai.Workspace
}
// GetWorktreeBranch returns the branch name for the session's worktree, or "".
func (ai *AgentInstance) GetWorktreeBranch(sessionKey string) string {
if wt := ai.GetWorktree(sessionKey); wt != nil {
return wt.Branch
}
return ""
}