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:
parent
6fc07c4409
commit
e99ff8b96b
2 changed files with 128 additions and 105 deletions
|
|
@ -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
122
pkg/agent/instance_ext.go
Normal 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 ""
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue