From fa1524f247eafe8000c1df7a5b33c82f4b752cd9 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Mar 2026 19:30:18 +0100 Subject: [PATCH] feat: Gemini architecture improvements (Singleflight, Tools Filtering) --- pkg/agent/loop.go | 11 +++--- pkg/agent/tools_filter.go | 75 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 pkg/agent/tools_filter.go diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index ef6d58775..cce27ba8e 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -19,6 +19,8 @@ import ( "time" "unicode/utf8" + "golang.org/x/sync/singleflight" + "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/commands" @@ -49,6 +51,7 @@ type AgentLoop struct { transcriber voice.Transcriber cmdRegistry *commands.Registry version string + sf singleflight.Group wg sync.WaitGroup } @@ -980,7 +983,7 @@ func (al *AgentLoop) runLLMIteration( }) // Build tool definitions - providerToolDefs := agent.Tools.ToProviderDefs() + providerToolDefs := al.selectRelevantTools(agent, opts.UserMessage) // Log LLM request details logger.DebugCF("agent", "LLM request", @@ -1410,15 +1413,15 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c if len(newHistory) > agent.SummarizeMessageThreshold || tokenEstimate > threshold { summarizeKey := agent.ID + ":" + sessionKey - if _, loading := al.summarizing.LoadOrStore(summarizeKey, true); !loading { + al.sf.Do(summarizeKey, func() (interface{}, error) { al.wg.Add(1) go func() { defer al.wg.Done() - defer al.summarizing.Delete(summarizeKey) logger.Debug("Memory threshold reached. Optimizing conversation history...") al.summarizeSession(agent, sessionKey) }() - } + return nil, nil + }) } } diff --git a/pkg/agent/tools_filter.go b/pkg/agent/tools_filter.go new file mode 100644 index 000000000..1b62d794f --- /dev/null +++ b/pkg/agent/tools_filter.go @@ -0,0 +1,75 @@ +package agent + +import ( + "strings" + + "github.com/sipeed/picoclaw/pkg/providers" +) + +// selectRelevantTools filters available tools based on user intent to reduce context. +func (al *AgentLoop) selectRelevantTools(agent *AgentInstance, userMsg string) []providers.ToolDefinition { + allTools := agent.Tools.ToProviderDefs() + if len(allTools) <= 5 { + return allTools + } + + lowerMsg := strings.ToLower(userMsg) + + // Essential tools are always included. + essentialTools := map[string]bool{ + "read_file": true, + "list_dir": true, + "google": true, // Web search is often needed for verification + } + + // Keyword mappings for contextual tools. + toolKeywords := map[string][]string{ + "write_file": {"write", "save", "create", "file", "code"}, + "edit_file": {"edit", "modify", "change", "file", "code", "replace", "fix"}, + "append_file": {"append", "add", "log", "file"}, + "exec": {"run", "execute", "shell", "command", "terminal", "install", "build", "cat", "ls", "git"}, + "vps": {"vps", "server", "remote", "ssh", "cloud"}, + "voice_call": {"call", "voice", "phone", "speak", "tell", "say"}, + "spawn": {"spawn", "acp", "harness", "agent", "protocol"}, + } + + relevant := make([]providers.ToolDefinition, 0) + for _, tool := range allTools { + name := tool.Function.Name + + // 1. Always include essential tools. + if essentialTools[name] { + relevant = append(relevant, tool) + continue + } + + // 2. Include based on keywords. + if kws, ok := toolKeywords[name]; ok { + matched := false + for _, kw := range kws { + if strings.Contains(lowerMsg, kw) { + matched = true + break + } + } + if matched { + relevant = append(relevant, tool) + continue + } + } + + // 3. Fallback: if tool has no keywords defined, include it by default? + // To be safe and avoid context bloat, we only include tools with defined keywords if they match. + // If a tool is NOT in our map, we'll include it for now to avoid breaking unknown tools. + if _, ok := toolKeywords[name]; !ok { + relevant = append(relevant, tool) + } + } + + // Safety: ensure we don't return an empty tool set if any were available. + if len(relevant) == 0 && len(allTools) > 0 { + return allTools[:min(len(allTools), 5)] + } + + return relevant +}