From be2ed5d759450c0ad28b25d92a434c4b63f11d6b Mon Sep 17 00:00:00 2001 From: yinwm Date: Tue, 10 Feb 2026 13:18:23 +0800 Subject: [PATCH 1/4] Add logging to agent loop and tool execution --- pkg/agent/loop.go | 42 ++++++++++++++++++++++++++++++++++++++++++ pkg/tools/registry.go | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 222d46a..c0e19d4 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -17,6 +17,7 @@ import ( "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/session" "github.com/sipeed/picoclaw/pkg/tools" @@ -115,6 +116,14 @@ func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey stri } func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) (string, error) { + logger.InfoCF("agent", "Processing message", + map[string]interface{}{ + "channel": msg.Channel, + "chat_id": msg.ChatID, + "sender_id": msg.SenderID, + "session_key": msg.SessionKey, + }) + history := al.sessions.GetHistory(msg.SessionKey) summary := al.sessions.GetSummary(msg.SessionKey) @@ -131,6 +140,12 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) for iteration < al.maxIterations { iteration++ + logger.DebugCF("agent", "LLM iteration", + map[string]interface{}{ + "iteration": iteration, + "max": al.maxIterations, + }) + toolDefs := al.tools.GetDefinitions() providerToolDefs := make([]providers.ToolDefinition, 0, len(toolDefs)) for _, td := range toolDefs { @@ -150,14 +165,35 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) }) if err != nil { + logger.ErrorCF("agent", "LLM call failed", + map[string]interface{}{ + "iteration": iteration, + "error": err.Error(), + }) return "", fmt.Errorf("LLM call failed: %w", err) } if len(response.ToolCalls) == 0 { finalContent = response.Content + logger.InfoCF("agent", "LLM response without tool calls (direct answer)", + map[string]interface{}{ + "iteration": iteration, + "content_chars": len(finalContent), + }) break } + toolNames := make([]string, 0, len(response.ToolCalls)) + for _, tc := range response.ToolCalls { + toolNames = append(toolNames, tc.Name) + } + logger.InfoCF("agent", "LLM requested tool calls", + map[string]interface{}{ + "tools": toolNames, + "count": len(toolNames), + "iteration": iteration, + }) + assistantMsg := providers.Message{ Role: "assistant", Content: response.Content, @@ -217,6 +253,12 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) al.sessions.Save(al.sessions.GetOrCreate(msg.SessionKey)) + logger.InfoCF("agent", "Message processing completed", + map[string]interface{}{ + "iterations": iteration, + "final_length": len(finalContent), + }) + return finalContent, nil } diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index e87eebe..576d70a 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -4,6 +4,9 @@ import ( "context" "fmt" "sync" + "time" + + "github.com/sipeed/picoclaw/pkg/logger" ) type ToolRegistry struct { @@ -31,11 +34,42 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) { } func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]interface{}) (string, error) { + logger.InfoCF("tool", "Tool execution started", + map[string]interface{}{ + "tool": name, + "args": args, + }) + tool, ok := r.Get(name) if !ok { + logger.ErrorCF("tool", "Tool not found", + map[string]interface{}{ + "tool": name, + }) return "", fmt.Errorf("tool '%s' not found", name) } - return tool.Execute(ctx, args) + + start := time.Now() + result, err := tool.Execute(ctx, args) + duration := time.Since(start) + + if err != nil { + logger.ErrorCF("tool", "Tool execution failed", + map[string]interface{}{ + "tool": name, + "duration": duration.Milliseconds(), + "error": err.Error(), + }) + } else { + logger.InfoCF("tool", "Tool execution completed", + map[string]interface{}{ + "tool": name, + "duration_ms": duration.Milliseconds(), + "result_length": len(result), + }) + } + + return result, err } func (r *ToolRegistry) GetDefinitions() []map[string]interface{} { From 10442732b4c03aefa57feae830929ae6b1e344f9 Mon Sep 17 00:00:00 2001 From: yinwm Date: Tue, 10 Feb 2026 16:05:23 +0800 Subject: [PATCH 2/4] Add memory system, debug mode, and tools --- cmd/picoclaw/main.go | 40 +++++++ pkg/agent/context.go | 101 ++++++++++++++--- pkg/agent/loop.go | 257 +++++++++++++++++++++++++++++++++++++----- pkg/agent/memory.go | 150 ++++++++++++++++++++++++ pkg/tools/edit.go | 42 +++++-- pkg/tools/registry.go | 19 ++++ pkg/tools/subagent.go | 23 +++- 7 files changed, 579 insertions(+), 53 deletions(-) create mode 100644 pkg/agent/memory.go diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 23bb7b9..f5b5135 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -426,6 +426,9 @@ func agentCmd() { args := os.Args[2:] for i := 0; i < len(args); i++ { switch args[i] { + case "--debug", "-d": + logger.SetLevel(logger.DEBUG) + fmt.Println("🔍 Debug mode enabled") case "-m", "--message": if i+1 < len(args) { message = args[i+1] @@ -454,6 +457,15 @@ func agentCmd() { msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) + // Print agent startup info (only for interactive mode) + startupInfo := agentLoop.GetStartupInfo() + logger.InfoCF("agent", "Agent initialized", + map[string]interface{}{ + "tools_count": startupInfo["tools"].(map[string]interface{})["count"], + "skills_total": startupInfo["skills"].(map[string]interface{})["total"], + "skills_available": startupInfo["skills"].(map[string]interface{})["available"], + }) + if message != "" { ctx := context.Background() response, err := agentLoop.ProcessDirect(ctx, message, sessionKey) @@ -555,6 +567,16 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { } func gatewayCmd() { + // Check for --debug flag + args := os.Args[2:] + for _, arg := range args { + if arg == "--debug" || arg == "-d" { + logger.SetLevel(logger.DEBUG) + fmt.Println("🔍 Debug mode enabled") + break + } + } + cfg, err := loadConfig() if err != nil { fmt.Printf("Error loading config: %v\n", err) @@ -570,6 +592,24 @@ func gatewayCmd() { msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) + // Print agent startup info + fmt.Println("\n📦 Agent Status:") + startupInfo := agentLoop.GetStartupInfo() + toolsInfo := startupInfo["tools"].(map[string]interface{}) + skillsInfo := startupInfo["skills"].(map[string]interface{}) + fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"]) + fmt.Printf(" • Skills: %d/%d available\n", + skillsInfo["available"], + skillsInfo["total"]) + + // Log to file as well + logger.InfoCF("agent", "Agent initialized", + map[string]interface{}{ + "tools_count": toolsInfo["count"], + "skills_total": skillsInfo["total"], + "skills_available": skillsInfo["available"], + }) + cronStorePath := filepath.Join(filepath.Dir(getConfigPath()), "cron", "jobs.json") cronService := cron.NewCronService(cronStorePath, nil) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 9ed5733..0870a23 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -4,8 +4,11 @@ import ( "fmt" "os" "path/filepath" + "runtime" + "strings" "time" + "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/skills" ) @@ -13,6 +16,7 @@ import ( type ContextBuilder struct { workspace string skillsLoader *skills.SkillsLoader + memory *MemoryStore } func NewContextBuilder(workspace string) *ContextBuilder { @@ -20,12 +24,14 @@ func NewContextBuilder(workspace string) *ContextBuilder { return &ContextBuilder{ workspace: workspace, skillsLoader: skills.NewSkillsLoader(workspace, builtinSkillsDir), + memory: NewMemoryStore(workspace), } } -func (cb *ContextBuilder) BuildSystemPrompt() string { +func (cb *ContextBuilder) getIdentity() string { now := time.Now().Format("2006-01-02 15:04 (Monday)") workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace)) + runtime := fmt.Sprintf("%s %s, Go %s", runtime.GOOS, runtime.GOARCH, runtime.Version()) return fmt.Sprintf(`# picoclaw 🦞 @@ -39,6 +45,9 @@ You are picoclaw, a helpful AI assistant. You have access to tools that allow yo ## Current Time %s +## Runtime +%s + ## Workspace Your workspace is at: %s - Memory files: %s/memory/MEMORY.md @@ -60,7 +69,49 @@ For normal conversation, just respond with text - do not call the message tool. Always be helpful, accurate, and concise. When using tools, explain what you're doing. When remembering something, write to %s/memory/MEMORY.md`, - now, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath) + now, runtime, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath) +} + +func (cb *ContextBuilder) BuildSystemPrompt() string { + parts := []string{} + + // Core identity section + parts = append(parts, cb.getIdentity()) + + // Bootstrap files + bootstrapContent := cb.LoadBootstrapFiles() + if bootstrapContent != "" { + parts = append(parts, bootstrapContent) + } + + // Skills - progressive loading + // 1. Always skills: load full content + alwaysSkills := cb.skillsLoader.GetAlwaysSkills() + if len(alwaysSkills) > 0 { + alwaysContent := cb.skillsLoader.LoadSkillsForContext(alwaysSkills) + if alwaysContent != "" { + parts = append(parts, "# Active Skills\n\n"+alwaysContent) + } + } + + // 2. Available skills: only show summary + skillsSummary := cb.skillsLoader.BuildSkillsSummary() + if skillsSummary != "" { + parts = append(parts, fmt.Sprintf(`# Skills + +The following skills extend your capabilities. To use a skill, read its SKILL.md file. + +%s`, skillsSummary)) + } + + // Memory context + memoryContext := cb.memory.GetMemoryContext() + if memoryContext != "" { + parts = append(parts, "# Memory\n\n"+memoryContext) + } + + // Join with "---" separator + return strings.Join(parts, "\n\n---\n\n") } func (cb *ContextBuilder) LoadBootstrapFiles() string { @@ -84,24 +135,28 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string { return result } +<<<<<<< HEAD func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary string, currentMessage string, media []string) []providers.Message { +======= +func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary string, currentMessage string, media []string, channel, chatID string) []providers.Message { +>>>>>>> fd1dd87 (Add memory system, debug mode, and tools) messages := []providers.Message{} systemPrompt := cb.BuildSystemPrompt() - bootstrapContent := cb.LoadBootstrapFiles() - if bootstrapContent != "" { - systemPrompt += "\n\n" + bootstrapContent + + // Add Current Session info if provided + if channel != "" && chatID != "" { + systemPrompt += fmt.Sprintf("\n\n## Current Session\nChannel: %s\nChat ID: %s", channel, chatID) } - skillsSummary := cb.skillsLoader.BuildSkillsSummary() - if skillsSummary != "" { - systemPrompt += "\n\n## Available Skills\n\n" + skillsSummary - } - - skillsContent := cb.loadSkills() - if skillsContent != "" { - systemPrompt += "\n\n" + skillsContent - } + // Log system prompt for debugging + logger.InfoCF("agent", "System prompt built", + map[string]interface{}{ + "total_chars": len(systemPrompt), + "total_lines": strings.Count(systemPrompt, "\n") + 1, + "section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1, + }) + logger.DebugCF("agent", "Full system prompt:\n"+systemPrompt, nil) if summary != "" { systemPrompt += "\n\n## Summary of Previous Conversation\n\n" + summary @@ -160,3 +215,21 @@ func (cb *ContextBuilder) loadSkills() string { return "# Skill Definitions\n\n" + content } + +// GetSkillsInfo returns information about loaded skills. +func (cb *ContextBuilder) GetSkillsInfo() map[string]interface{} { + allSkills := cb.skillsLoader.ListSkills(true) + skillNames := make([]string, 0, len(allSkills)) + availableCount := 0 + for _, s := range allSkills { + skillNames = append(skillNames, s.Name) + if s.Available { + availableCount++ + } + } + return map[string]interface{}{ + "total": len(allSkills), + "available": availableCount, + "names": skillNames, + } +} diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index c0e19d4..79b3cb0 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -12,8 +12,7 @@ import ( "fmt" "os" "path/filepath" - "sync" - "time" + "strings" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/config" @@ -28,16 +27,14 @@ type AgentLoop struct { provider providers.LLMProvider workspace string model string - contextWindow int maxIterations int sessions *session.SessionManager contextBuilder *ContextBuilder tools *tools.ToolRegistry running bool - summarizing sync.Map } -func NewAgentLoop(cfg *config.Config, bus *bus.MessageBus, provider providers.LLMProvider) *AgentLoop { +func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers.LLMProvider) *AgentLoop { workspace := cfg.WorkspacePath() os.MkdirAll(workspace, 0755) @@ -51,20 +48,39 @@ func NewAgentLoop(cfg *config.Config, bus *bus.MessageBus, provider providers.LL toolsRegistry.Register(tools.NewWebSearchTool(braveAPIKey, cfg.Tools.Web.Search.MaxResults)) toolsRegistry.Register(tools.NewWebFetchTool(50000)) + // Register message tool + messageTool := tools.NewMessageTool() + messageTool.SetSendCallback(func(channel, chatID, content string) error { + msgBus.PublishOutbound(bus.OutboundMessage{ + Channel: channel, + ChatID: chatID, + Content: content, + }) + return nil + }) + toolsRegistry.Register(messageTool) + + // Register spawn tool + subagentManager := tools.NewSubagentManager(provider, workspace, msgBus) + spawnTool := tools.NewSpawnTool(subagentManager) + toolsRegistry.Register(spawnTool) + + // Register edit file tool + editFileTool := tools.NewEditFileTool(workspace) + toolsRegistry.Register(editFileTool) + sessionsManager := session.NewSessionManager(filepath.Join(filepath.Dir(cfg.WorkspacePath()), "sessions")) return &AgentLoop{ - bus: bus, + bus: msgBus, provider: provider, workspace: workspace, model: cfg.Agents.Defaults.Model, - contextWindow: cfg.Agents.Defaults.MaxTokens, maxIterations: cfg.Agents.Defaults.MaxToolIterations, sessions: sessionsManager, contextBuilder: NewContextBuilder(workspace), tools: toolsRegistry, running: false, - summarizing: sync.Map{}, } } @@ -116,7 +132,9 @@ func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey stri } func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) (string, error) { - logger.InfoCF("agent", "Processing message", + // Add message preview to log + preview := truncate(msg.Content, 80) + logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, preview), map[string]interface{}{ "channel": msg.Channel, "chat_id": msg.ChatID, @@ -124,6 +142,23 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) "session_key": msg.SessionKey, }) + // Route system messages to processSystemMessage + if msg.Channel == "system" { + return al.processSystemMessage(ctx, msg) + } + + // Update tool contexts + if tool, ok := al.tools.Get("message"); ok { + if mt, ok := tool.(*tools.MessageTool); ok { + mt.SetContext(msg.Channel, msg.ChatID) + } + } + if tool, ok := al.tools.Get("spawn"); ok { + if st, ok := tool.(*tools.SpawnTool); ok { + st.SetContext(msg.Channel, msg.ChatID) + } + } + history := al.sessions.GetHistory(msg.SessionKey) summary := al.sessions.GetSummary(msg.SessionKey) @@ -132,6 +167,8 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) summary, msg.Content, nil, + msg.Channel, + msg.ChatID, ) iteration := 0 @@ -213,6 +250,15 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) messages = append(messages, assistantMsg) for _, tc := range response.ToolCalls { + // Log tool call with arguments preview + argsJSON, _ := json.Marshal(tc.Arguments) + argsPreview := truncate(string(argsJSON), 200) + logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview), + map[string]interface{}{ + "tool": tc.Name, + "iteration": iteration, + }) + result, err := al.tools.Execute(ctx, tc.Name, tc.Arguments) if err != nil { result = fmt.Sprintf("Error: %v", err) @@ -233,27 +279,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) al.sessions.AddMessage(msg.SessionKey, "user", msg.Content) al.sessions.AddMessage(msg.SessionKey, "assistant", finalContent) - - // Context compression logic - newHistory := al.sessions.GetHistory(msg.SessionKey) - - // Token Awareness (Dynamic) - // Trigger if history > 20 messages OR estimated tokens > 75% of context window - tokenEstimate := al.estimateTokens(newHistory) - threshold := al.contextWindow * 75 / 100 - - if len(newHistory) > 20 || tokenEstimate > threshold { - if _, loading := al.summarizing.LoadOrStore(msg.SessionKey, true); !loading { - go func() { - defer al.summarizing.Delete(msg.SessionKey) - al.summarizeSession(msg.SessionKey) - }() - } - } - al.sessions.Save(al.sessions.GetOrCreate(msg.SessionKey)) - logger.InfoCF("agent", "Message processing completed", + // Log response preview + responsePreview := truncate(finalContent, 120) + logger.InfoCF("agent", fmt.Sprintf("Response to %s:%s: %s", msg.Channel, msg.SenderID, responsePreview), map[string]interface{}{ "iterations": iteration, "final_length": len(finalContent), @@ -262,6 +292,176 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) return finalContent, nil } +func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMessage) (string, error) { + // Verify this is a system message + if msg.Channel != "system" { + return "", fmt.Errorf("processSystemMessage called with non-system message channel: %s", msg.Channel) + } + + logger.InfoCF("agent", "Processing system message", + map[string]interface{}{ + "sender_id": msg.SenderID, + "chat_id": msg.ChatID, + }) + + // Parse origin from chat_id (format: "channel:chat_id") + var originChannel, originChatID string + if idx := strings.Index(msg.ChatID, ":"); idx > 0 { + originChannel = msg.ChatID[:idx] + originChatID = msg.ChatID[idx+1:] + } else { + // Fallback + originChannel = "cli" + originChatID = msg.ChatID + } + + // Use the origin session for context + sessionKey := fmt.Sprintf("%s:%s", originChannel, originChatID) + + // Update tool contexts to original channel/chatID + if tool, ok := al.tools.Get("message"); ok { + if mt, ok := tool.(*tools.MessageTool); ok { + mt.SetContext(originChannel, originChatID) + } + } + if tool, ok := al.tools.Get("spawn"); ok { + if st, ok := tool.(*tools.SpawnTool); ok { + st.SetContext(originChannel, originChatID) + } + } + + // Build messages with the announce content + history := al.sessions.GetHistory(sessionKey) + summary := al.sessions.GetSummary(sessionKey) + messages := al.contextBuilder.BuildMessages( + history, + summary, + msg.Content, + nil, + originChannel, + originChatID, + ) + + iteration := 0 + var finalContent string + + for iteration < al.maxIterations { + iteration++ + + toolDefs := al.tools.GetDefinitions() + providerToolDefs := make([]providers.ToolDefinition, 0, len(toolDefs)) + for _, td := range toolDefs { + providerToolDefs = append(providerToolDefs, providers.ToolDefinition{ + Type: td["type"].(string), + Function: providers.ToolFunctionDefinition{ + Name: td["function"].(map[string]interface{})["name"].(string), + Description: td["function"].(map[string]interface{})["description"].(string), + Parameters: td["function"].(map[string]interface{})["parameters"].(map[string]interface{}), + }, + }) + } + + response, err := al.provider.Chat(ctx, messages, providerToolDefs, al.model, map[string]interface{}{ + "max_tokens": 8192, + "temperature": 0.7, + }) + + if err != nil { + logger.ErrorCF("agent", "LLM call failed in system message", + map[string]interface{}{ + "iteration": iteration, + "error": err.Error(), + }) + return "", fmt.Errorf("LLM call failed: %w", err) + } + + if len(response.ToolCalls) == 0 { + finalContent = response.Content + break + } + + assistantMsg := providers.Message{ + Role: "assistant", + Content: response.Content, + } + + for _, tc := range response.ToolCalls { + argumentsJSON, _ := json.Marshal(tc.Arguments) + assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{ + ID: tc.ID, + Type: "function", + Function: &providers.FunctionCall{ + Name: tc.Name, + Arguments: string(argumentsJSON), + }, + }) + } + messages = append(messages, assistantMsg) + + for _, tc := range response.ToolCalls { + result, err := al.tools.Execute(ctx, tc.Name, tc.Arguments) + if err != nil { + result = fmt.Sprintf("Error: %v", err) + } + + toolResultMsg := providers.Message{ + Role: "tool", + Content: result, + ToolCallID: tc.ID, + } + messages = append(messages, toolResultMsg) + } + } + + if finalContent == "" { + finalContent = "Background task completed." + } + + // Save to session with system message marker + al.sessions.AddMessage(sessionKey, "user", fmt.Sprintf("[System: %s] %s", msg.SenderID, msg.Content)) + al.sessions.AddMessage(sessionKey, "assistant", finalContent) + al.sessions.Save(al.sessions.GetOrCreate(sessionKey)) + + logger.InfoCF("agent", "System message processing completed", + map[string]interface{}{ + "iterations": iteration, + "final_length": len(finalContent), + }) + + return finalContent, nil +} + +// truncate returns a truncated version of s with at most maxLen characters. +// If the string is truncated, "..." is appended to indicate truncation. +// If the string fits within maxLen, it is returned unchanged. +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + // Reserve 3 chars for "..." + if maxLen <= 3 { + return s[:maxLen] + } + return s[:maxLen-3] + "..." +} + +// GetStartupInfo returns information about loaded tools and skills for logging. +func (al *AgentLoop) GetStartupInfo() map[string]interface{} { + info := make(map[string]interface{}) + + // Tools info + tools := al.tools.List() + info["tools"] = map[string]interface{}{ + "count": len(tools), + "names": tools, + } + + // Skills info + info["skills"] = al.contextBuilder.GetSkillsInfo() + + return info +} + func (al *AgentLoop) summarizeSession(sessionKey string) { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() @@ -363,4 +563,3 @@ func (al *AgentLoop) estimateTokens(messages []providers.Message) int { } return total } - diff --git a/pkg/agent/memory.go b/pkg/agent/memory.go new file mode 100644 index 0000000..4668685 --- /dev/null +++ b/pkg/agent/memory.go @@ -0,0 +1,150 @@ +// PicoClaw - Ultra-lightweight personal AI agent +// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot +// License: MIT +// +// Copyright (c) 2026 PicoClaw contributors + +package agent + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +// MemoryStore manages persistent memory for the agent. +// Supports daily notes (memory/YYYY-MM-DD.md) and long-term memory (MEMORY.md). +type MemoryStore struct { + workspace string + memoryDir string + memoryFile string +} + +// NewMemoryStore creates a new MemoryStore with the given workspace path. +// It ensures the memory directory exists. +func NewMemoryStore(workspace string) *MemoryStore { + memoryDir := filepath.Join(workspace, "memory") + memoryFile := filepath.Join(memoryDir, "MEMORY.md") + + // Ensure memory directory exists + os.MkdirAll(memoryDir, 0755) + + return &MemoryStore{ + workspace: workspace, + memoryDir: memoryDir, + memoryFile: memoryFile, + } +} + +// getMemoryDir returns the memory directory path. +func (ms *MemoryStore) getMemoryDir() string { + return ms.memoryDir +} + +// getMemoryFile returns the long-term memory file path. +func (ms *MemoryStore) getMemoryFile() string { + return ms.memoryFile +} + +// getTodayFile returns the path to today's memory file (YYYY-MM-DD.md). +func (ms *MemoryStore) getTodayFile() string { + today := time.Now().Format("2006-01-02") + return filepath.Join(ms.memoryDir, today+".md") +} + +// ReadToday reads today's memory notes. +// Returns empty string if the file doesn't exist. +func (ms *MemoryStore) ReadToday() string { + todayFile := ms.getTodayFile() + if data, err := os.ReadFile(todayFile); err == nil { + return string(data) + } + return "" +} + +// AppendToday appends content to today's memory notes. +// If the file doesn't exist, it creates a new file with a date header. +func (ms *MemoryStore) AppendToday(content string) error { + todayFile := ms.getTodayFile() + + var existingContent string + if data, err := os.ReadFile(todayFile); err == nil { + existingContent = string(data) + } + + var newContent string + if existingContent == "" { + // Add header for new day + header := fmt.Sprintf("# %s\n\n", time.Now().Format("2006-01-02")) + newContent = header + content + } else { + // Append to existing content + newContent = existingContent + "\n" + content + } + + return os.WriteFile(todayFile, []byte(newContent), 0644) +} + +// ReadLongTerm reads the long-term memory (MEMORY.md). +// Returns empty string if the file doesn't exist. +func (ms *MemoryStore) ReadLongTerm() string { + if data, err := os.ReadFile(ms.memoryFile); err == nil { + return string(data) + } + return "" +} + +// WriteLongTerm writes content to the long-term memory file (MEMORY.md). +func (ms *MemoryStore) WriteLongTerm(content string) error { + return os.WriteFile(ms.memoryFile, []byte(content), 0644) +} + +// GetRecentMemories returns memories from the last N days. +// It reads and combines the contents of memory files from the past days. +// Contents are joined with "---" separator. +func (ms *MemoryStore) GetRecentMemories(days int) string { + var memories []string + + for i := 0; i < days; i++ { + date := time.Now().AddDate(0, 0, -i) + dateStr := date.Format("2006-01-02") + filePath := filepath.Join(ms.memoryDir, dateStr+".md") + + if data, err := os.ReadFile(filePath); err == nil { + memories = append(memories, string(data)) + } + } + + if len(memories) == 0 { + return "" + } + + return strings.Join(memories, "\n\n---\n\n") +} + +// GetMemoryContext returns formatted memory context for the agent prompt. +// It includes long-term memory and today's notes sections if they exist. +// Returns empty string if no memory exists. +func (ms *MemoryStore) GetMemoryContext() string { + var parts []string + + // Long-term memory + longTerm := ms.ReadLongTerm() + if longTerm != "" { + parts = append(parts, "## Long-term Memory\n\n"+longTerm) + } + + // Today's notes + today := ms.ReadToday() + if today != "" { + parts = append(parts, "## Today's Notes\n\n"+today) + } + + if len(parts) == 0 { + return "" + } + + return strings.Join(parts, "\n\n") +} diff --git a/pkg/tools/edit.go b/pkg/tools/edit.go index f7aec17..339148e 100644 --- a/pkg/tools/edit.go +++ b/pkg/tools/edit.go @@ -8,10 +8,17 @@ import ( "strings" ) -type EditFileTool struct{} +// EditFileTool edits a file by replacing old_text with new_text. +// The old_text must exist exactly in the file. +type EditFileTool struct { + allowedDir string // Optional directory restriction for security +} -func NewEditFileTool() *EditFileTool { - return &EditFileTool{} +// NewEditFileTool creates a new EditFileTool with optional directory restriction. +func NewEditFileTool(allowedDir string) *EditFileTool { + return &EditFileTool{ + allowedDir: allowedDir, + } } func (t *EditFileTool) Name() string { @@ -59,13 +66,34 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{}) return "", fmt.Errorf("new_text is required") } - filePath := filepath.Clean(path) + // Resolve path and enforce directory restriction if configured + resolvedPath := path + if filepath.IsAbs(path) { + resolvedPath = filepath.Clean(path) + } else { + abs, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to resolve path: %w", err) + } + resolvedPath = abs + } - if _, err := os.Stat(filePath); os.IsNotExist(err) { + // Check directory restriction + if t.allowedDir != "" { + allowedAbs, err := filepath.Abs(t.allowedDir) + if err != nil { + return "", fmt.Errorf("failed to resolve allowed directory: %w", err) + } + if !strings.HasPrefix(resolvedPath, allowedAbs) { + return "", fmt.Errorf("path %s is outside allowed directory %s", path, t.allowedDir) + } + } + + if _, err := os.Stat(resolvedPath); os.IsNotExist(err) { return "", fmt.Errorf("file not found: %s", path) } - content, err := os.ReadFile(filePath) + content, err := os.ReadFile(resolvedPath) if err != nil { return "", fmt.Errorf("failed to read file: %w", err) } @@ -83,7 +111,7 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{}) newContent := strings.Replace(contentStr, oldText, newText, 1) - if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil { + if err := os.WriteFile(resolvedPath, []byte(newContent), 0644); err != nil { return "", fmt.Errorf("failed to write file: %w", err) } diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index 576d70a..04b6cf7 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -82,3 +82,22 @@ func (r *ToolRegistry) GetDefinitions() []map[string]interface{} { } return definitions } + +// List returns a list of all registered tool names. +func (r *ToolRegistry) List() []string { + r.mu.RLock() + defer r.mu.RUnlock() + + names := make([]string, 0, len(r.tools)) + for name := range r.tools { + names = append(names, name) + } + return names +} + +// Count returns the number of registered tools. +func (r *ToolRegistry) Count() int { + r.mu.RLock() + defer r.mu.RUnlock() + return len(r.tools) +} diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index ddec9ff..0c05097 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -5,6 +5,9 @@ import ( "fmt" "sync" "time" + + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/providers" ) type SubagentTask struct { @@ -21,15 +24,17 @@ type SubagentTask struct { type SubagentManager struct { tasks map[string]*SubagentTask mu sync.RWMutex - provider LLMProvider + provider providers.LLMProvider + bus *bus.MessageBus workspace string nextID int } -func NewSubagentManager(provider LLMProvider, workspace string) *SubagentManager { +func NewSubagentManager(provider providers.LLMProvider, workspace string, bus *bus.MessageBus) *SubagentManager { return &SubagentManager{ tasks: make(map[string]*SubagentTask), provider: provider, + bus: bus, workspace: workspace, nextID: 1, } @@ -65,7 +70,7 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { task.Status = "running" task.Created = time.Now().UnixMilli() - messages := []Message{ + messages := []providers.Message{ { Role: "system", Content: "You are a subagent. Complete the given task independently and report the result.", @@ -90,6 +95,18 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { task.Status = "completed" task.Result = response.Content } + + // Send announce message back to main agent + if sm.bus != nil { + announceContent := fmt.Sprintf("Task '%s' completed.\n\nResult:\n%s", task.Label, task.Result) + sm.bus.PublishInbound(bus.InboundMessage{ + Channel: "system", + SenderID: fmt.Sprintf("subagent:%s", task.ID), + // Format: "original_channel:original_chat_id" for routing back + ChatID: fmt.Sprintf("%s:%s", task.OriginChannel, task.OriginChatID), + Content: announceContent, + }) + } } func (sm *SubagentManager) GetTask(taskID string) (*SubagentTask, bool) { From 21d60f63fc744205e08eca9a8d2fe6ac0397d5a6 Mon Sep 17 00:00:00 2001 From: yinwm Date: Tue, 10 Feb 2026 23:33:28 +0800 Subject: [PATCH 3/4] Add memory system, dynamic tool loading, and fix logging issues - Add MemoryStore for persistent long-term and daily notes - Add dynamic tool summary generation in system prompt - Fix YAML frontmatter parsing for nanobot skill format - Add GetSummaries() method to ToolRegistry - Fix DebugCF logging to use structured metadata - Improve web_search and shell tool descriptions --- cmd/picoclaw/main.go | 87 +++-------------- pkg/agent/context.go | 122 ++++++++++++----------- pkg/agent/loop.go | 185 +++++++++++++++++------------------ pkg/agent/memory.go | 105 +++++++++++--------- pkg/skills/loader.go | 218 +++++++++++++++++++----------------------- pkg/tools/registry.go | 13 +++ pkg/tools/shell.go | 4 +- pkg/tools/web.go | 2 +- 8 files changed, 341 insertions(+), 395 deletions(-) diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index f5b5135..751cdda 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -102,7 +102,11 @@ func main() { workspace := cfg.WorkspacePath() installer := skills.NewSkillInstaller(workspace) - skillsLoader := skills.NewSkillsLoader(workspace, "") + // 获取全局配置目录和内置 skills 目录 + globalDir := filepath.Dir(getConfigPath()) + globalSkillsDir := filepath.Join(globalDir, "skills") + builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills") + skillsLoader := skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir) switch subcommand { case "list": @@ -242,70 +246,6 @@ Information about user goes here. - What the user wants to learn from AI - Preferred interaction style - Areas of interest -`, - "TOOLS.md": `# Available Tools - -This document describes the tools available to picoclaw. - -## File Operations - -### Read Files -- Read file contents -- Supports text, markdown, code files - -### Write Files -- Create new files -- Overwrite existing files -- Supports various formats - -### List Directories -- List directory contents -- Recursive listing support - -### Edit Files -- Make specific edits to files -- Line-by-line editing -- String replacement - -## Web Tools - -### Web Search -- Search the internet using search API -- Returns titles, URLs, snippets -- Optional: Requires API key for best results - -### Web Fetch -- Fetch specific URLs -- Extract readable content -- Supports HTML, JSON, plain text -- Automatic content extraction - -## Command Execution - -### Shell Commands -- Execute any shell command -- Run in workspace directory -- Full shell access with timeout protection - -## Messaging - -### Send Messages -- Send messages to chat channels -- Supports Telegram, WhatsApp, Feishu -- Used for notifications and responses - -## AI Capabilities - -### Context Building -- Load system instructions from files -- Load skills dynamically -- Build conversation history -- Include timezone and other context - -### Memory Management -- Long-term memory via MEMORY.md -- Daily notes via dated files -- Persistent across sessions `, "IDENTITY.md": `# Identity @@ -977,7 +917,11 @@ func skillsCmd() { workspace := cfg.WorkspacePath() installer := skills.NewSkillInstaller(workspace) - skillsLoader := skills.NewSkillsLoader(workspace, "") + // 获取全局配置目录和内置 skills 目录 + globalDir := filepath.Dir(getConfigPath()) + globalSkillsDir := filepath.Join(globalDir, "skills") + builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills") + skillsLoader := skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir) switch subcommand { case "list": @@ -1023,7 +967,7 @@ func skillsHelp() { } func skillsListCmd(loader *skills.SkillsLoader) { - allSkills := loader.ListSkills(false) + allSkills := loader.ListSkills() if len(allSkills) == 0 { fmt.Println("No skills installed.") @@ -1033,17 +977,10 @@ func skillsListCmd(loader *skills.SkillsLoader) { fmt.Println("\nInstalled Skills:") fmt.Println("------------------") for _, skill := range allSkills { - status := "✓" - if !skill.Available { - status = "✗" - } - fmt.Printf(" %s %s (%s)\n", status, skill.Name, skill.Source) + fmt.Printf(" ✓ %s (%s)\n", skill.Name, skill.Source) if skill.Description != "" { fmt.Printf(" %s\n", skill.Description) } - if !skill.Available { - fmt.Printf(" Missing: %s\n", skill.Missing) - } } } diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 0870a23..506e5dc 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -17,14 +17,29 @@ type ContextBuilder struct { workspace string skillsLoader *skills.SkillsLoader memory *MemoryStore + toolsSummary func() []string // Function to get tool summaries dynamically } -func NewContextBuilder(workspace string) *ContextBuilder { - builtinSkillsDir := filepath.Join(filepath.Dir(workspace), "picoclaw", "skills") +func getGlobalConfigDir() string { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".picoclaw") +} + +func NewContextBuilder(workspace string, toolsSummaryFunc func() []string) *ContextBuilder { + // builtin skills: 当前项目的 skills 目录 + // 使用当前工作目录下的 skills/ 目录 + wd, _ := os.Getwd() + builtinSkillsDir := filepath.Join(wd, "skills") + globalSkillsDir := filepath.Join(getGlobalConfigDir(), "skills") + return &ContextBuilder{ workspace: workspace, - skillsLoader: skills.NewSkillsLoader(workspace, builtinSkillsDir), + skillsLoader: skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir), memory: NewMemoryStore(workspace), + toolsSummary: toolsSummaryFunc, } } @@ -33,14 +48,12 @@ func (cb *ContextBuilder) getIdentity() string { workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace)) runtime := fmt.Sprintf("%s %s, Go %s", runtime.GOOS, runtime.GOARCH, runtime.Version()) + // Build tools section dynamically + toolsSection := cb.buildToolsSection() + return fmt.Sprintf(`# picoclaw 🦞 -You are picoclaw, a helpful AI assistant. You have access to tools that allow you to: -- Read, write, and edit files -- Execute shell commands -- Search the web and fetch web pages -- Send messages to users on chat channels -- Spawn subagents for complex background tasks +You are picoclaw, a helpful AI assistant. ## Current Time %s @@ -50,26 +63,36 @@ You are picoclaw, a helpful AI assistant. You have access to tools that allow yo ## Workspace Your workspace is at: %s -- Memory files: %s/memory/MEMORY.md -- Daily notes: %s/memory/2006-01-02.md -- Custom skills: %s/skills/{skill-name}/SKILL.md +- Memory: %s/memory/MEMORY.md +- Daily Notes: %s/memory/YYYYMM/YYYYMMDD.md +- Skills: %s/skills/{skill-name}/SKILL.md -## Weather Information -When users ask about weather, use the web_fetch tool with wttr.in URLs: -- Current weather: https://wttr.in/{city}?format=j1 -- Beijing: https://wttr.in/Beijing?format=j1 -- Shanghai: https://wttr.in/Shanghai?format=j1 -- New York: https://wttr.in/New_York?format=j1 -- London: https://wttr.in/London?format=j1 -- Tokyo: https://wttr.in/Tokyo?format=j1 - -IMPORTANT: When responding to direct questions or conversations, reply directly with your text response. -Only use the 'message' tool when you need to send a message to a specific chat channel (like WhatsApp). -For normal conversation, just respond with text - do not call the message tool. +%s Always be helpful, accurate, and concise. When using tools, explain what you're doing. When remembering something, write to %s/memory/MEMORY.md`, - now, runtime, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath) + now, runtime, workspacePath, workspacePath, workspacePath, workspacePath, toolsSection, workspacePath) +} + +func (cb *ContextBuilder) buildToolsSection() string { + if cb.toolsSummary == nil { + return "" + } + + summaries := cb.toolsSummary() + if len(summaries) == 0 { + return "" + } + + var sb strings.Builder + sb.WriteString("## Available Tools\n\n") + sb.WriteString("You have access to the following tools:\n\n") + for _, s := range summaries { + sb.WriteString(s) + sb.WriteString("\n") + } + + return sb.String() } func (cb *ContextBuilder) BuildSystemPrompt() string { @@ -84,22 +107,12 @@ func (cb *ContextBuilder) BuildSystemPrompt() string { parts = append(parts, bootstrapContent) } - // Skills - progressive loading - // 1. Always skills: load full content - alwaysSkills := cb.skillsLoader.GetAlwaysSkills() - if len(alwaysSkills) > 0 { - alwaysContent := cb.skillsLoader.LoadSkillsForContext(alwaysSkills) - if alwaysContent != "" { - parts = append(parts, "# Active Skills\n\n"+alwaysContent) - } - } - - // 2. Available skills: only show summary + // Skills - show summary, AI can read full content with read_file tool skillsSummary := cb.skillsLoader.BuildSkillsSummary() if skillsSummary != "" { parts = append(parts, fmt.Sprintf(`# Skills -The following skills extend your capabilities. To use a skill, read its SKILL.md file. +The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool. %s`, skillsSummary)) } @@ -119,9 +132,7 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string { "AGENTS.md", "SOUL.md", "USER.md", - "TOOLS.md", "IDENTITY.md", - "MEMORY.md", } var result string @@ -149,14 +160,23 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str systemPrompt += fmt.Sprintf("\n\n## Current Session\nChannel: %s\nChat ID: %s", channel, chatID) } - // Log system prompt for debugging - logger.InfoCF("agent", "System prompt built", + // Log system prompt summary for debugging (debug mode only) + logger.DebugCF("agent", "System prompt built", map[string]interface{}{ "total_chars": len(systemPrompt), "total_lines": strings.Count(systemPrompt, "\n") + 1, "section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1, }) - logger.DebugCF("agent", "Full system prompt:\n"+systemPrompt, nil) + + // Log preview of system prompt (avoid logging huge content) + preview := systemPrompt + if len(preview) > 500 { + preview = preview[:500] + "... (truncated)" + } + logger.DebugCF("agent", "System prompt preview", + map[string]interface{}{ + "preview": preview, + }) if summary != "" { systemPrompt += "\n\n## Summary of Previous Conversation\n\n" + summary @@ -191,14 +211,13 @@ func (cb *ContextBuilder) AddAssistantMessage(messages []providers.Message, cont Role: "assistant", Content: content, } - if len(toolCalls) > 0 { - messages = append(messages, msg) - } + // Always add assistant message, whether or not it has tool calls + messages = append(messages, msg) return messages } func (cb *ContextBuilder) loadSkills() string { - allSkills := cb.skillsLoader.ListSkills(true) + allSkills := cb.skillsLoader.ListSkills() if len(allSkills) == 0 { return "" } @@ -218,18 +237,13 @@ func (cb *ContextBuilder) loadSkills() string { // GetSkillsInfo returns information about loaded skills. func (cb *ContextBuilder) GetSkillsInfo() map[string]interface{} { - allSkills := cb.skillsLoader.ListSkills(true) + allSkills := cb.skillsLoader.ListSkills() skillNames := make([]string, 0, len(allSkills)) - availableCount := 0 for _, s := range allSkills { skillNames = append(skillNames, s.Name) - if s.Available { - availableCount++ - } } return map[string]interface{}{ - "total": len(allSkills), - "available": availableCount, - "names": skillNames, + "total": len(allSkills), + "names": skillNames, } } diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 79b3cb0..5737396 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -78,7 +78,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers model: cfg.Agents.Defaults.Model, maxIterations: cfg.Agents.Defaults.MaxToolIterations, sessions: sessionsManager, - contextBuilder: NewContextBuilder(workspace), + contextBuilder: NewContextBuilder(workspace, func() []string { return toolsRegistry.GetSummaries() }), tools: toolsRegistry, running: false, } @@ -159,12 +159,8 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } } - history := al.sessions.GetHistory(msg.SessionKey) - summary := al.sessions.GetSummary(msg.SessionKey) - messages := al.contextBuilder.BuildMessages( - history, - summary, + al.sessions.GetHistory(msg.SessionKey), msg.Content, nil, msg.Channel, @@ -196,6 +192,26 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) }) } + // Log LLM request details + logger.DebugCF("agent", "LLM request", + map[string]interface{}{ + "iteration": iteration, + "model": al.model, + "messages_count": len(messages), + "tools_count": len(providerToolDefs), + "max_tokens": 8192, + "temperature": 0.7, + "system_prompt_len": len(messages[0].Content), + }) + + // Log full messages (detailed) + logger.DebugCF("agent", "Full LLM request", + map[string]interface{}{ + "iteration": iteration, + "messages_json": formatMessagesForLog(messages), + "tools_json": formatToolsForLog(providerToolDefs), + }) + response, err := al.provider.Chat(ctx, messages, providerToolDefs, al.model, map[string]interface{}{ "max_tokens": 8192, "temperature": 0.7, @@ -331,11 +347,8 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe } // Build messages with the announce content - history := al.sessions.GetHistory(sessionKey) - summary := al.sessions.GetSummary(sessionKey) messages := al.contextBuilder.BuildMessages( - history, - summary, + al.sessions.GetHistory(sessionKey), msg.Content, nil, originChannel, @@ -361,6 +374,26 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe }) } + // Log LLM request details + logger.DebugCF("agent", "LLM request", + map[string]interface{}{ + "iteration": iteration, + "model": al.model, + "messages_count": len(messages), + "tools_count": len(providerToolDefs), + "max_tokens": 8192, + "temperature": 0.7, + "system_prompt_len": len(messages[0].Content), + }) + + // Log full messages (detailed) + logger.DebugCF("agent", "Full LLM request", + map[string]interface{}{ + "iteration": iteration, + "messages_json": formatMessagesForLog(messages), + "tools_json": formatToolsForLog(providerToolDefs), + }) + response, err := al.provider.Chat(ctx, messages, providerToolDefs, al.model, map[string]interface{}{ "max_tokens": 8192, "temperature": 0.7, @@ -462,104 +495,64 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} { return info } -func (al *AgentLoop) summarizeSession(sessionKey string) { - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - - history := al.sessions.GetHistory(sessionKey) - summary := al.sessions.GetSummary(sessionKey) - - // Keep last 4 messages for continuity - if len(history) <= 4 { - return +// formatMessagesForLog formats messages for logging +func formatMessagesForLog(messages []providers.Message) string { + if len(messages) == 0 { + return "[]" } - toSummarize := history[:len(history)-4] - - // Oversized Message Guard (Dynamic) - // Skip messages larger than 50% of context window to prevent summarizer overflow. - maxMessageTokens := al.contextWindow / 2 - validMessages := make([]providers.Message, 0) - omitted := false - - for _, m := range toSummarize { - if m.Role != "user" && m.Role != "assistant" { - continue + var result string + result += "[\n" + for i, msg := range messages { + result += fmt.Sprintf(" [%d] Role: %s\n", i, msg.Role) + if msg.ToolCalls != nil && len(msg.ToolCalls) > 0 { + result += " ToolCalls:\n" + for _, tc := range msg.ToolCalls { + result += fmt.Sprintf(" - ID: %s, Type: %s, Name: %s\n", tc.ID, tc.Type, tc.Name) + if tc.Function != nil { + result += fmt.Sprintf(" Arguments: %s\n", truncateString(tc.Function.Arguments, 200)) + } + } } - // Estimate tokens for this message - msgTokens := len(m.Content) / 4 - if msgTokens > maxMessageTokens { - omitted = true - continue + if msg.Content != "" { + content := truncateString(msg.Content, 200) + result += fmt.Sprintf(" Content: %s\n", content) } - validMessages = append(validMessages, m) - } - - if len(validMessages) == 0 { - return - } - - // Multi-Part Summarization - // Split into two parts if history is significant - var finalSummary string - if len(validMessages) > 10 { - mid := len(validMessages) / 2 - part1 := validMessages[:mid] - part2 := validMessages[mid:] - - s1, _ := al.summarizeBatch(ctx, part1, "") - s2, _ := al.summarizeBatch(ctx, part2, "") - - // Merge them - mergePrompt := fmt.Sprintf("Merge these two conversation summaries into one cohesive summary:\n\n1: %s\n\n2: %s", s1, s2) - resp, err := al.provider.Chat(ctx, []providers.Message{{Role: "user", Content: mergePrompt}}, nil, al.model, map[string]interface{}{ - "max_tokens": 1024, - "temperature": 0.3, - }) - if err == nil { - finalSummary = resp.Content - } else { - finalSummary = s1 + " " + s2 + if msg.ToolCallID != "" { + result += fmt.Sprintf(" ToolCallID: %s\n", msg.ToolCallID) } - } else { - finalSummary, _ = al.summarizeBatch(ctx, validMessages, summary) - } - - if omitted && finalSummary != "" { - finalSummary += "\n[Note: Some oversized messages were omitted from this summary for efficiency.]" - } - - if finalSummary != "" { - al.sessions.SetSummary(sessionKey, finalSummary) - al.sessions.TruncateHistory(sessionKey, 4) - al.sessions.Save(al.sessions.GetOrCreate(sessionKey)) + result += "\n" } + result += "]" + return result } -func (al *AgentLoop) summarizeBatch(ctx context.Context, batch []providers.Message, existingSummary string) (string, error) { - prompt := "Provide a concise summary of this conversation segment, preserving core context and key points.\n" - if existingSummary != "" { - prompt += "Existing context: " + existingSummary + "\n" - } - prompt += "\nCONVERSATION:\n" - for _, m := range batch { - prompt += fmt.Sprintf("%s: %s\n", m.Role, m.Content) +// formatToolsForLog formats tool definitions for logging +func formatToolsForLog(tools []providers.ToolDefinition) string { + if len(tools) == 0 { + return "[]" } - response, err := al.provider.Chat(ctx, []providers.Message{{Role: "user", Content: prompt}}, nil, al.model, map[string]interface{}{ - "max_tokens": 1024, - "temperature": 0.3, - }) - if err != nil { - return "", err + var result string + result += "[\n" + for i, tool := range tools { + result += fmt.Sprintf(" [%d] Type: %s, Name: %s\n", i, tool.Type, tool.Function.Name) + result += fmt.Sprintf(" Description: %s\n", tool.Function.Description) + if len(tool.Function.Parameters) > 0 { + result += fmt.Sprintf(" Parameters: %s\n", truncateString(fmt.Sprintf("%v", tool.Function.Parameters), 200)) + } } - return response.Content, nil + result += "]" + return result } -func (al *AgentLoop) estimateTokens(messages []providers.Message) int { - total := 0 - for _, m := range messages { - total += len(m.Content) / 4 // Simple heuristic: 4 chars per token +// truncateString truncates a string to max length +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s } - return total + if maxLen <= 3 { + return s[:maxLen] + } + return s[:maxLen-3] + "..." } diff --git a/pkg/agent/memory.go b/pkg/agent/memory.go index 4668685..f27882d 100644 --- a/pkg/agent/memory.go +++ b/pkg/agent/memory.go @@ -10,12 +10,12 @@ import ( "fmt" "os" "path/filepath" - "strings" "time" ) // MemoryStore manages persistent memory for the agent. -// Supports daily notes (memory/YYYY-MM-DD.md) and long-term memory (MEMORY.md). +// - Long-term memory: memory/MEMORY.md +// - Daily notes: memory/YYYYMM/YYYYMMDD.md type MemoryStore struct { workspace string memoryDir string @@ -38,23 +38,29 @@ func NewMemoryStore(workspace string) *MemoryStore { } } -// getMemoryDir returns the memory directory path. -func (ms *MemoryStore) getMemoryDir() string { - return ms.memoryDir -} - -// getMemoryFile returns the long-term memory file path. -func (ms *MemoryStore) getMemoryFile() string { - return ms.memoryFile -} - -// getTodayFile returns the path to today's memory file (YYYY-MM-DD.md). +// getTodayFile returns the path to today's daily note file (memory/YYYYMM/YYYYMMDD.md). func (ms *MemoryStore) getTodayFile() string { - today := time.Now().Format("2006-01-02") - return filepath.Join(ms.memoryDir, today+".md") + today := time.Now().Format("20060102") // YYYYMMDD + monthDir := today[:6] // YYYYMM + filePath := filepath.Join(ms.memoryDir, monthDir, today+".md") + return filePath } -// ReadToday reads today's memory notes. +// ReadLongTerm reads the long-term memory (MEMORY.md). +// Returns empty string if the file doesn't exist. +func (ms *MemoryStore) ReadLongTerm() string { + if data, err := os.ReadFile(ms.memoryFile); err == nil { + return string(data) + } + return "" +} + +// WriteLongTerm writes content to the long-term memory file (MEMORY.md). +func (ms *MemoryStore) WriteLongTerm(content string) error { + return os.WriteFile(ms.memoryFile, []byte(content), 0644) +} + +// ReadToday reads today's daily note. // Returns empty string if the file doesn't exist. func (ms *MemoryStore) ReadToday() string { todayFile := ms.getTodayFile() @@ -64,11 +70,15 @@ func (ms *MemoryStore) ReadToday() string { return "" } -// AppendToday appends content to today's memory notes. +// AppendToday appends content to today's daily note. // If the file doesn't exist, it creates a new file with a date header. func (ms *MemoryStore) AppendToday(content string) error { todayFile := ms.getTodayFile() + // Ensure month directory exists + monthDir := filepath.Dir(todayFile) + os.MkdirAll(monthDir, 0755) + var existingContent string if data, err := os.ReadFile(todayFile); err == nil { existingContent = string(data) @@ -87,46 +97,39 @@ func (ms *MemoryStore) AppendToday(content string) error { return os.WriteFile(todayFile, []byte(newContent), 0644) } -// ReadLongTerm reads the long-term memory (MEMORY.md). -// Returns empty string if the file doesn't exist. -func (ms *MemoryStore) ReadLongTerm() string { - if data, err := os.ReadFile(ms.memoryFile); err == nil { - return string(data) - } - return "" -} - -// WriteLongTerm writes content to the long-term memory file (MEMORY.md). -func (ms *MemoryStore) WriteLongTerm(content string) error { - return os.WriteFile(ms.memoryFile, []byte(content), 0644) -} - -// GetRecentMemories returns memories from the last N days. -// It reads and combines the contents of memory files from the past days. +// GetRecentDailyNotes returns daily notes from the last N days. // Contents are joined with "---" separator. -func (ms *MemoryStore) GetRecentMemories(days int) string { - var memories []string +func (ms *MemoryStore) GetRecentDailyNotes(days int) string { + var notes []string for i := 0; i < days; i++ { date := time.Now().AddDate(0, 0, -i) - dateStr := date.Format("2006-01-02") - filePath := filepath.Join(ms.memoryDir, dateStr+".md") + dateStr := date.Format("20060102") // YYYYMMDD + monthDir := dateStr[:6] // YYYYMM + filePath := filepath.Join(ms.memoryDir, monthDir, dateStr+".md") if data, err := os.ReadFile(filePath); err == nil { - memories = append(memories, string(data)) + notes = append(notes, string(data)) } } - if len(memories) == 0 { + if len(notes) == 0 { return "" } - return strings.Join(memories, "\n\n---\n\n") + // Join with separator + var result string + for i, note := range notes { + if i > 0 { + result += "\n\n---\n\n" + } + result += note + } + return result } // GetMemoryContext returns formatted memory context for the agent prompt. -// It includes long-term memory and today's notes sections if they exist. -// Returns empty string if no memory exists. +// Includes long-term memory and recent daily notes. func (ms *MemoryStore) GetMemoryContext() string { var parts []string @@ -136,15 +139,23 @@ func (ms *MemoryStore) GetMemoryContext() string { parts = append(parts, "## Long-term Memory\n\n"+longTerm) } - // Today's notes - today := ms.ReadToday() - if today != "" { - parts = append(parts, "## Today's Notes\n\n"+today) + // Recent daily notes (last 3 days) + recentNotes := ms.GetRecentDailyNotes(3) + if recentNotes != "" { + parts = append(parts, "## Recent Daily Notes\n\n"+recentNotes) } if len(parts) == 0 { return "" } - return strings.Join(parts, "\n\n") + // Join parts with separator + var result string + for i, part := range parts { + if i > 0 { + result += "\n\n---\n\n" + } + result += part + } + return fmt.Sprintf("# Memory\n\n%s", result) } diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index d0c2195..1f952c1 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -4,22 +4,14 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "path/filepath" "regexp" "strings" ) type SkillMetadata struct { - Name string `json:"name"` - Description string `json:"description"` - Always bool `json:"always"` - Requires *SkillRequirements `json:"requires,omitempty"` -} - -type SkillRequirements struct { - Bins []string `json:"bins"` - Env []string `json:"env"` + Name string `json:"name"` + Description string `json:"description"` } type SkillInfo struct { @@ -27,25 +19,25 @@ type SkillInfo struct { Path string `json:"path"` Source string `json:"source"` Description string `json:"description"` - Available bool `json:"available"` - Missing string `json:"missing,omitempty"` } type SkillsLoader struct { workspace string - workspaceSkills string - builtinSkills string + workspaceSkills string // workspace skills (项目级别) + globalSkills string // 全局 skills (~/.picoclaw/skills) + builtinSkills string // 内置 skills } -func NewSkillsLoader(workspace string, builtinSkills string) *SkillsLoader { +func NewSkillsLoader(workspace string, globalSkills string, builtinSkills string) *SkillsLoader { return &SkillsLoader{ workspace: workspace, workspaceSkills: filepath.Join(workspace, "skills"), + globalSkills: globalSkills, // ~/.picoclaw/skills builtinSkills: builtinSkills, } } -func (sl *SkillsLoader) ListSkills(filterUnavailable bool) []SkillInfo { +func (sl *SkillsLoader) ListSkills() []SkillInfo { skills := make([]SkillInfo, 0) if sl.workspaceSkills != "" { @@ -62,12 +54,41 @@ func (sl *SkillsLoader) ListSkills(filterUnavailable bool) []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description - info.Available = sl.checkRequirements(metadata.Requires) - if !info.Available { - info.Missing = sl.getMissingRequirements(metadata.Requires) + } + skills = append(skills, info) + } + } + } + } + } + + // 全局 skills (~/.picoclaw/skills) - 被 workspace skills 覆盖 + if sl.globalSkills != "" { + if dirs, err := os.ReadDir(sl.globalSkills); err == nil { + for _, dir := range dirs { + if dir.IsDir() { + skillFile := filepath.Join(sl.globalSkills, dir.Name(), "SKILL.md") + if _, err := os.Stat(skillFile); err == nil { + // 检查是否已被 workspace skills 覆盖 + exists := false + for _, s := range skills { + if s.Name == dir.Name() && s.Source == "workspace" { + exists = true + break } - } else { - info.Available = true + } + if exists { + continue + } + + info := SkillInfo{ + Name: dir.Name(), + Path: skillFile, + Source: "global", + } + metadata := sl.getSkillMetadata(skillFile) + if metadata != nil { + info.Description = metadata.Description } skills = append(skills, info) } @@ -82,9 +103,10 @@ func (sl *SkillsLoader) ListSkills(filterUnavailable bool) []SkillInfo { if dir.IsDir() { skillFile := filepath.Join(sl.builtinSkills, dir.Name(), "SKILL.md") if _, err := os.Stat(skillFile); err == nil { + // 检查是否已被 workspace 或 global skills 覆盖 exists := false for _, s := range skills { - if s.Name == dir.Name() && s.Source == "workspace" { + if s.Name == dir.Name() && (s.Source == "workspace" || s.Source == "global") { exists = true break } @@ -101,12 +123,6 @@ func (sl *SkillsLoader) ListSkills(filterUnavailable bool) []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description - info.Available = sl.checkRequirements(metadata.Requires) - if !info.Available { - info.Missing = sl.getMissingRequirements(metadata.Requires) - } - } else { - info.Available = true } skills = append(skills, info) } @@ -115,20 +131,11 @@ func (sl *SkillsLoader) ListSkills(filterUnavailable bool) []SkillInfo { } } - if filterUnavailable { - filtered := make([]SkillInfo, 0) - for _, s := range skills { - if s.Available { - filtered = append(filtered, s) - } - } - return filtered - } - return skills } func (sl *SkillsLoader) LoadSkill(name string) (string, bool) { + // 1. 优先从 workspace skills 加载(项目级别) if sl.workspaceSkills != "" { skillFile := filepath.Join(sl.workspaceSkills, name, "SKILL.md") if content, err := os.ReadFile(skillFile); err == nil { @@ -136,6 +143,15 @@ func (sl *SkillsLoader) LoadSkill(name string) (string, bool) { } } + // 2. 其次从全局 skills 加载 (~/.picoclaw/skills) + if sl.globalSkills != "" { + skillFile := filepath.Join(sl.globalSkills, name, "SKILL.md") + if content, err := os.ReadFile(skillFile); err == nil { + return sl.stripFrontmatter(string(content)), true + } + } + + // 3. 最后从内置 skills 加载 if sl.builtinSkills != "" { skillFile := filepath.Join(sl.builtinSkills, name, "SKILL.md") if content, err := os.ReadFile(skillFile); err == nil { @@ -163,7 +179,7 @@ func (sl *SkillsLoader) LoadSkillsForContext(skillNames []string) string { } func (sl *SkillsLoader) BuildSkillsSummary() string { - allSkills := sl.ListSkills(false) + allSkills := sl.ListSkills() if len(allSkills) == 0 { return "" } @@ -175,21 +191,11 @@ func (sl *SkillsLoader) BuildSkillsSummary() string { escapedDesc := escapeXML(s.Description) escapedPath := escapeXML(s.Path) - available := "true" - if !s.Available { - available = "false" - } - - lines = append(lines, fmt.Sprintf(" ", available)) + lines = append(lines, fmt.Sprintf(" ")) lines = append(lines, fmt.Sprintf(" %s", escapedName)) lines = append(lines, fmt.Sprintf(" %s", escapedDesc)) lines = append(lines, fmt.Sprintf(" %s", escapedPath)) - - if !s.Available && s.Missing != "" { - escapedMissing := escapeXML(s.Missing) - lines = append(lines, fmt.Sprintf(" %s", escapedMissing)) - } - + lines = append(lines, fmt.Sprintf(" %s", s.Source)) lines = append(lines, " ") } lines = append(lines, "") @@ -197,18 +203,6 @@ func (sl *SkillsLoader) BuildSkillsSummary() string { return strings.Join(lines, "\n") } -func (sl *SkillsLoader) GetAlwaysSkills() []string { - skills := sl.ListSkills(true) - var always []string - for _, s := range skills { - metadata := sl.getSkillMetadata(s.Path) - if metadata != nil && metadata.Always { - always = append(always, s.Name) - } - } - return always -} - func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata { content, err := os.ReadFile(skillPath) if err != nil { @@ -222,27 +216,54 @@ func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata { } } - var metadata struct { - Name string `json:"name"` - Description string `json:"description"` - Always bool `json:"always"` - Requires *SkillRequirements `json:"requires"` - } - - if err := json.Unmarshal([]byte(frontmatter), &metadata); err != nil { - return nil + // Try JSON first (for backward compatibility) + var jsonMeta struct { + Name string `json:"name"` + Description string `json:"description"` + } + if err := json.Unmarshal([]byte(frontmatter), &jsonMeta); err == nil { + return &SkillMetadata{ + Name: jsonMeta.Name, + Description: jsonMeta.Description, + } } + // Fall back to simple YAML parsing + yamlMeta := sl.parseSimpleYAML(frontmatter) return &SkillMetadata{ - Name: metadata.Name, - Description: metadata.Description, - Always: metadata.Always, - Requires: metadata.Requires, + Name: yamlMeta["name"], + Description: yamlMeta["description"], } } +// parseSimpleYAML parses simple key: value YAML format +// Example: name: github\n description: "..." +func (sl *SkillsLoader) parseSimpleYAML(content string) map[string]string { + result := make(map[string]string) + + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + // Remove quotes if present + value = strings.Trim(value, "\"'") + result[key] = value + } + } + + return result +} + func (sl *SkillsLoader) extractFrontmatter(content string) string { - re := regexp.MustCompile(`^---\n(.*?)\n---`) + // (?s) enables DOTALL mode so . matches newlines + // Match first ---, capture everything until next --- on its own line + re := regexp.MustCompile(`(?s)^---\n(.*)\n---`) match := re.FindStringSubmatch(content) if len(match) > 1 { return match[1] @@ -255,49 +276,6 @@ func (sl *SkillsLoader) stripFrontmatter(content string) string { return re.ReplaceAllString(content, "") } -func (sl *SkillsLoader) checkRequirements(requires *SkillRequirements) bool { - if requires == nil { - return true - } - - for _, bin := range requires.Bins { - if _, err := exec.LookPath(bin); err != nil { - continue - } else { - return true - } - } - - for _, env := range requires.Env { - if os.Getenv(env) == "" { - return false - } - } - - return true -} - -func (sl *SkillsLoader) getMissingRequirements(requires *SkillRequirements) string { - if requires == nil { - return "" - } - - var missing []string - for _, bin := range requires.Bins { - if _, err := exec.LookPath(bin); err != nil { - missing = append(missing, fmt.Sprintf("CLI: %s", bin)) - } - } - - for _, env := range requires.Env { - if os.Getenv(env) == "" { - missing = append(missing, fmt.Sprintf("ENV: %s", env)) - } - } - - return strings.Join(missing, ", ") -} - func escapeXML(s string) string { s = strings.ReplaceAll(s, "&", "&") s = strings.ReplaceAll(s, "<", "<") diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index 04b6cf7..d181944 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -101,3 +101,16 @@ func (r *ToolRegistry) Count() int { defer r.mu.RUnlock() return len(r.tools) } + +// GetSummaries returns human-readable summaries of all registered tools. +// Returns a slice of "name - description" strings. +func (r *ToolRegistry) GetSummaries() []string { + r.mu.RLock() + defer r.mu.RUnlock() + + summaries := make([]string, 0, len(r.tools)) + for _, tool := range r.tools { + summaries = append(summaries, fmt.Sprintf("- `%s` - %s", tool.Name(), tool.Description())) + } + return summaries +} diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 87f67a0..d8aea40 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -25,9 +25,9 @@ func NewExecTool(workingDir string) *ExecTool { regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`), regexp.MustCompile(`\bdel\s+/[fq]\b`), regexp.MustCompile(`\brmdir\s+/s\b`), - regexp.MustCompile(`\b(format|mkfs|diskpart)\b`), + regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args) regexp.MustCompile(`\bdd\s+if=`), - regexp.MustCompile(`>\s*/dev/sd`), + regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null) regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`), regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), } diff --git a/pkg/tools/web.go b/pkg/tools/web.go index 119c88f..3a35968 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -36,7 +36,7 @@ func (t *WebSearchTool) Name() string { } func (t *WebSearchTool) Description() string { - return "Search the web. Returns titles, URLs, and snippets." + return "Search the web for current information. Returns titles, URLs, and snippets from search results." } func (t *WebSearchTool) Parameters() map[string]interface{} { From c3f53985387bd366c69e4beda6d75900652fb19b Mon Sep 17 00:00:00 2001 From: yinwm Date: Wed, 11 Feb 2026 00:30:38 +0800 Subject: [PATCH 4/4] Fix BuildMessages calls, remove conflict markers, and add skills_available field --- pkg/agent/context.go | 9 +++------ pkg/agent/loop.go | 11 +++++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 506e5dc..7e8612e 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -146,11 +146,7 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string { return result } -<<<<<<< HEAD -func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary string, currentMessage string, media []string) []providers.Message { -======= func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary string, currentMessage string, media []string, channel, chatID string) []providers.Message { ->>>>>>> fd1dd87 (Add memory system, debug mode, and tools) messages := []providers.Message{} systemPrompt := cb.BuildSystemPrompt() @@ -243,7 +239,8 @@ func (cb *ContextBuilder) GetSkillsInfo() map[string]interface{} { skillNames = append(skillNames, s.Name) } return map[string]interface{}{ - "total": len(allSkills), - "names": skillNames, + "total": len(allSkills), + "available": len(allSkills), + "names": skillNames, } } diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 5737396..d38848b 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -159,8 +159,12 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } } + history := al.sessions.GetHistory(msg.SessionKey) + summary := al.sessions.GetSummary(msg.SessionKey) + messages := al.contextBuilder.BuildMessages( - al.sessions.GetHistory(msg.SessionKey), + history, + summary, msg.Content, nil, msg.Channel, @@ -347,8 +351,11 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe } // Build messages with the announce content + history := al.sessions.GetHistory(sessionKey) + summary := al.sessions.GetSummary(sessionKey) messages := al.contextBuilder.BuildMessages( - al.sessions.GetHistory(sessionKey), + history, + summary, msg.Content, nil, originChannel,