From c05c66c8aff6ad03ecffd18f6135428825c2dd31 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 1 Mar 2026 04:52:31 +0900 Subject: [PATCH] fix: strip think tags from heartbeat messages and log web search provider - Strip blocks from force response path (max iterations) in runLLMIteration, which was missing StripThinkBlocks - Add StripThinkBlocks safety net in heartbeat sendResponse to prevent LLM reasoning artifacts from leaking to users - Add ProviderName() to WebSearchTool and log selected search provider (brave/perplexity/tavily/duckduckgo) at startup for diagnostics - Show web search provider in gateway startup console output Co-Authored-By: Claude Opus 4.6 --- cmd/picoclaw/internal/gateway/helpers.go | 5 +++++ pkg/agent/loop.go | 19 +++++++++++++++++-- pkg/heartbeat/service.go | 6 +++++- pkg/tools/web.go | 20 ++++++++++++++++---- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/cmd/picoclaw/internal/gateway/helpers.go b/cmd/picoclaw/internal/gateway/helpers.go index 8001c1a23..0f064d4ee 100644 --- a/cmd/picoclaw/internal/gateway/helpers.go +++ b/cmd/picoclaw/internal/gateway/helpers.go @@ -80,6 +80,11 @@ func gatewayCmd(debug bool, orchestration bool, enableStats bool) error { toolsInfo := startupInfo["tools"].(map[string]any) skillsInfo := startupInfo["skills"].(map[string]any) fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"]) + if wsProvider, ok := toolsInfo["web_search_provider"].(string); ok { + fmt.Printf(" • Web search: %s\n", wsProvider) + } else { + fmt.Println(" • Web search: disabled") + } fmt.Printf(" • Skills: %d/%d available\n", skillsInfo["available"], skillsInfo["total"]) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 015e1a149..066a53747 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -240,6 +240,14 @@ func registerSharedTools( Proxy: cfg.Tools.Web.Proxy, }); searchTool != nil { agent.Tools.Register(searchTool) + logger.InfoCF("agent", "Web search provider registered", map[string]any{ + "agent_id": agentID, + "provider": searchTool.ProviderName(), + }) + } else { + logger.WarnCF("agent", "No web search provider configured", map[string]any{ + "agent_id": agentID, + }) } agent.Tools.Register(tools.NewWebFetchToolWithProxy(50000, cfg.Tools.Web.Proxy)) @@ -2775,7 +2783,7 @@ func (al *AgentLoop) runLLMIteration( "prompt_cache_key": agent.ID, }) if forceErr == nil && forceResp.Content != "" { - finalContent = forceResp.Content + finalContent = utils.StripThinkBlocks(forceResp.Content) if forceResp.Usage != nil && al.stats != nil { al.stats.RecordUsage( forceResp.Usage.PromptTokens, @@ -2896,10 +2904,17 @@ func (al *AgentLoop) GetStartupInfo() map[string]any { // Tools info toolsList := agent.Tools.List() - info["tools"] = map[string]any{ + 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() diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go index 60e5f3214..e19c758b8 100644 --- a/pkg/heartbeat/service.go +++ b/pkg/heartbeat/service.go @@ -21,6 +21,7 @@ import ( "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/state" "github.com/sipeed/picoclaw/pkg/tools" + "github.com/sipeed/picoclaw/pkg/utils" ) const ( @@ -308,8 +309,11 @@ Add your heartbeat tasks below this line: } } -// sendResponse sends the heartbeat response to the last channel +// sendResponse sends the heartbeat response to the last channel. +// Think blocks are stripped as a safety net to prevent LLM reasoning +// artifacts from leaking into user-facing messages. func (hs *HeartbeatService) sendResponse(response string) { + response = utils.StripThinkBlocks(response) hs.mu.RLock() msgBus := hs.bus hs.mu.RUnlock() diff --git a/pkg/tools/web.go b/pkg/tools/web.go index 38a42b5eb..50380571f 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -387,8 +387,14 @@ func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, cou } type WebSearchTool struct { - provider SearchProvider - maxResults int + provider SearchProvider + providerName string + maxResults int +} + +// ProviderName returns the name of the active search provider (e.g. "brave", "perplexity"). +func (t *WebSearchTool) ProviderName() string { + return t.providerName } type WebSearchToolOptions struct { @@ -409,16 +415,19 @@ type WebSearchToolOptions struct { func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool { var provider SearchProvider + var providerName string maxResults := 5 // Priority: Perplexity > Brave > Tavily > DuckDuckGo if opts.PerplexityEnabled && opts.PerplexityAPIKey != "" { provider = &PerplexitySearchProvider{apiKey: opts.PerplexityAPIKey, proxy: opts.Proxy} + providerName = "perplexity" if opts.PerplexityMaxResults > 0 { maxResults = opts.PerplexityMaxResults } } else if opts.BraveEnabled && opts.BraveAPIKey != "" { provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey, proxy: opts.Proxy} + providerName = "brave" if opts.BraveMaxResults > 0 { maxResults = opts.BraveMaxResults } @@ -428,11 +437,13 @@ func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool { baseURL: opts.TavilyBaseURL, proxy: opts.Proxy, } + providerName = "tavily" if opts.TavilyMaxResults > 0 { maxResults = opts.TavilyMaxResults } } else if opts.DuckDuckGoEnabled { provider = &DuckDuckGoSearchProvider{proxy: opts.Proxy} + providerName = "duckduckgo" if opts.DuckDuckGoMaxResults > 0 { maxResults = opts.DuckDuckGoMaxResults } @@ -441,8 +452,9 @@ func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool { } return &WebSearchTool{ - provider: provider, - maxResults: maxResults, + provider: provider, + providerName: providerName, + maxResults: maxResults, } }