Reduce upstream merge friction by extracting fork-specific code from files that upstream also modifies. loop.go is reduced from 6522 to 3165 lines by moving self-contained functions to dedicated files: - loop_task.go: activeTask, status display, tool log formatting - loop_plan.go: plan mode, interview filtering, worktree commands - loop_orch.go: orchestration reporting, system messages, peer extraction - loop_commands.go: slash commands, session commands, skill commands - loop_streaming.go: streaming display, repetition detection, reasoning - loop_session.go: session locks, GC, summarization - loop_info.go: getters (startup info, plan info, context info) Similarly for tools/: - base.go → base.go (Tool interface only) + base_ext.go (AsyncExecutor, StatusProvider, context helpers) - registry.go → registry.go (core CRUD) + registry_ext.go (ExecuteWithContext, ToProviderDefs, GetSummaries) config/: - SubagentsConfig, PeerMatch moved to config_ext.go - Fix missing "strings" import (pre-existing bug) No behavior changes. All code stays in the same Go packages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
281 lines
5.9 KiB
Go
281 lines
5.9 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
"github.com/sipeed/picoclaw/pkg/skills"
|
|
"github.com/sipeed/picoclaw/pkg/stats"
|
|
"github.com/sipeed/picoclaw/pkg/tools"
|
|
"github.com/sipeed/picoclaw/pkg/utils"
|
|
)
|
|
|
|
func (al *AgentLoop) GetStartupInfo() map[string]any {
|
|
info := make(map[string]any)
|
|
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
if agent == nil {
|
|
return info
|
|
}
|
|
|
|
// Tools info
|
|
|
|
toolsList := agent.Tools.List()
|
|
|
|
toolsMap := map[string]any{
|
|
"count": len(toolsList),
|
|
|
|
"names": toolsList,
|
|
}
|
|
|
|
// Report web search provider if registered
|
|
|
|
if t, ok := agent.Tools.Get("web_search"); ok {
|
|
if wst, ok := t.(*tools.WebSearchTool); ok {
|
|
toolsMap["web_search_provider"] = wst.ProviderName()
|
|
}
|
|
}
|
|
|
|
info["tools"] = toolsMap
|
|
|
|
// Skills info
|
|
|
|
info["skills"] = agent.ContextBuilder.GetSkillsInfo()
|
|
|
|
// Agents info
|
|
|
|
info["agents"] = map[string]any{
|
|
"count": len(al.registry.ListAgentIDs()),
|
|
|
|
"ids": al.registry.ListAgentIDs(),
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// ListSkills returns all available skills from the default agent.
|
|
|
|
func (al *AgentLoop) ListSkills() []skills.SkillInfo {
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
if agent == nil {
|
|
return nil
|
|
}
|
|
|
|
return agent.ContextBuilder.ListSkills()
|
|
}
|
|
|
|
// GetPlanInfo returns plan state from the default agent's memory store.
|
|
|
|
func (al *AgentLoop) GetPlanInfo() (hasPlan bool, status string, currentPhase, totalPhases int, display string, memory string) {
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
if agent == nil {
|
|
return false, "", 0, 0, "No agent available.", ""
|
|
}
|
|
|
|
mem := agent.ContextBuilder.Memory()
|
|
|
|
if mem == nil {
|
|
return false, "", 0, 0, "No memory store.", ""
|
|
}
|
|
|
|
hasPlan = mem.HasActivePlan()
|
|
|
|
status = mem.GetPlanStatus()
|
|
|
|
currentPhase = mem.GetCurrentPhase()
|
|
|
|
totalPhases = mem.GetTotalPhases()
|
|
|
|
display = mem.FormatPlanDisplay()
|
|
|
|
memory = mem.ReadLongTerm()
|
|
|
|
return hasPlan, status, currentPhase, totalPhases, display, memory
|
|
}
|
|
|
|
// GetPlanStatus returns the current plan status ("interviewing", "executing", "review", etc.) or "".
|
|
|
|
func (al *AgentLoop) GetPlanStatus() string {
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
if agent == nil {
|
|
return ""
|
|
}
|
|
|
|
return agent.ContextBuilder.GetPlanStatus()
|
|
}
|
|
|
|
// GetPlanPhases returns structured phase/step data from the default agent's plan.
|
|
|
|
func (al *AgentLoop) GetPlanPhases() []PlanPhase {
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
if agent == nil {
|
|
return nil
|
|
}
|
|
|
|
mem := agent.ContextBuilder.Memory()
|
|
|
|
if mem == nil {
|
|
return nil
|
|
}
|
|
|
|
return mem.GetPlanPhases()
|
|
}
|
|
|
|
// GetActiveSessions returns currently active sessions for the mini app API.
|
|
|
|
func (al *AgentLoop) GetActiveSessions() []SessionEntry {
|
|
return al.sessions.ListActive()
|
|
}
|
|
|
|
// GetSessionStats returns the current session statistics snapshot, or nil if stats tracking is disabled.
|
|
|
|
func (al *AgentLoop) GetSessionStats() *stats.Stats {
|
|
if al.stats == nil {
|
|
return nil
|
|
}
|
|
|
|
s := al.stats.GetStats()
|
|
|
|
return &s
|
|
}
|
|
|
|
// GetContextInfo returns the bootstrap file resolution and directory context for the default agent.
|
|
|
|
func (al *AgentLoop) GetContextInfo() (workDir, planWorkDir, workspace string, bootstrap []BootstrapFileInfo) {
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
if agent == nil {
|
|
return "", "", "", nil
|
|
}
|
|
|
|
workspace = agent.Workspace
|
|
|
|
planWorkDir = agent.ContextBuilder.GetPlanWorkDir()
|
|
|
|
// Use the most recent active session's touch_dir (tool-detected project directory)
|
|
|
|
if active := al.sessions.ListActive(); len(active) > 0 && active[0].TouchDir != "" {
|
|
workDir = active[0].TouchDir
|
|
} else {
|
|
workDir = agent.ContextBuilder.workDir
|
|
}
|
|
|
|
bootstrap = agent.ContextBuilder.ResolveBootstrapPaths()
|
|
|
|
return workDir, planWorkDir, workspace, bootstrap
|
|
}
|
|
|
|
// GetSystemPrompt returns the system prompt last sent to the LLM.
|
|
|
|
// If the prompt is dirty (state changed since last capture), it rebuilds
|
|
|
|
// from current state. Falls back to building if no LLM call has occurred yet.
|
|
|
|
func (al *AgentLoop) GetSystemPrompt() string {
|
|
if !al.promptDirty.Load() {
|
|
if v := al.lastSystemPrompt.Load(); v != nil {
|
|
return v.(string)
|
|
}
|
|
}
|
|
|
|
// Rebuild from current state
|
|
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
if agent == nil {
|
|
return ""
|
|
}
|
|
|
|
prompt := agent.ContextBuilder.BuildSystemPrompt()
|
|
|
|
al.lastSystemPrompt.Store(prompt)
|
|
|
|
al.promptDirty.Store(false)
|
|
|
|
return prompt
|
|
}
|
|
|
|
// formatMessagesForLog formats messages for logging
|
|
|
|
func formatMessagesForLog(messages []providers.Message) string {
|
|
if len(messages) == 0 {
|
|
return "[]"
|
|
}
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("[\n")
|
|
|
|
for i, msg := range messages {
|
|
fmt.Fprintf(&sb, " [%d] Role: %s\n", i, msg.Role)
|
|
|
|
if len(msg.ToolCalls) > 0 {
|
|
sb.WriteString(" ToolCalls:\n")
|
|
|
|
for _, tc := range msg.ToolCalls {
|
|
fmt.Fprintf(&sb, " - ID: %s, Type: %s, Name: %s\n", tc.ID, tc.Type, tc.Name)
|
|
|
|
args := tc.Arguments
|
|
|
|
if len(args) == 0 && tc.Function != nil {
|
|
args = tc.Function.Arguments
|
|
}
|
|
|
|
if len(args) > 0 {
|
|
argsJSON, _ := json.Marshal(args)
|
|
|
|
fmt.Fprintf(&sb, " Arguments: %s\n", utils.Truncate(string(argsJSON), 200))
|
|
}
|
|
}
|
|
}
|
|
|
|
if msg.Content != "" {
|
|
content := utils.Truncate(msg.Content, 200)
|
|
|
|
fmt.Fprintf(&sb, " Content: %s\n", content)
|
|
}
|
|
|
|
if msg.ToolCallID != "" {
|
|
fmt.Fprintf(&sb, " ToolCallID: %s\n", msg.ToolCallID)
|
|
}
|
|
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
sb.WriteString("]")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// formatToolsForLog formats tool definitions for logging
|
|
|
|
func formatToolsForLog(toolDefs []providers.ToolDefinition) string {
|
|
if len(toolDefs) == 0 {
|
|
return "[]"
|
|
}
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("[\n")
|
|
|
|
for i, tool := range toolDefs {
|
|
fmt.Fprintf(&sb, " [%d] Type: %s, Name: %s\n", i, tool.Type, tool.Function.Name)
|
|
|
|
fmt.Fprintf(&sb, " Description: %s\n", tool.Function.Description)
|
|
|
|
if len(tool.Function.Parameters) > 0 {
|
|
fmt.Fprintf(&sb, " Parameters: %s\n", utils.Truncate(string(tool.Function.Parameters), 200))
|
|
}
|
|
}
|
|
|
|
sb.WriteString("]")
|
|
|
|
return sb.String()
|
|
}
|