microcompact.go
1 package agent 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 8 "github.com/Kocoro-lab/ShanClaw/internal/client" 9 ctxwin "github.com/Kocoro-lab/ShanClaw/internal/context" 10 ) 11 12 const ( 13 // microCompactMarker is prefixed to LLM-summarized tool results to prevent 14 // re-summarization on subsequent compaction passes. 15 microCompactMarker = "[micro-compact] " 16 17 // microCompactMinChars is the minimum content length for a Tier 2 result 18 // to be eligible for LLM summarization. Below this, head+tail is fine. 19 microCompactMinChars = 2000 20 21 // microCompactMaxPerPass caps LLM call *attempts* per compressOldToolResults 22 // invocation to prevent latency spikes. Counts both successes and failures. 23 microCompactMaxPerPass = 2 24 ) 25 26 // isMicroCompactSkipTool reports whether a tool's result should never be 27 // micro-compacted (LLM-summarized). 28 // 29 // - think: internal reasoning, not factual — summarization destroys the purpose. 30 // - cloud_delegate: deliverables for the user, not agent working memory. 31 // - file_read, grep, glob, directory_list: code/search/repo-inspection results 32 // where the model needs actual content (paths, signatures, line numbers), 33 // not summaries. 34 // - browser_*: DOM snapshots and page state ARE the model's eyes for web tasks — 35 // summarizing them into "the browser navigated to X" blinds the model 36 // mid-task. Prefix-matched so newly added playwright tools are covered 37 // automatically (browser_drag, browser_take_screenshot, …). 38 // 39 // These always get mechanical head+tail truncation in Tier 2. 40 func isMicroCompactSkipTool(name string) bool { 41 switch name { 42 case "think", "cloud_delegate", "file_read", "grep", "glob", "directory_list": 43 return true 44 } 45 return strings.HasPrefix(name, "browser_") 46 } 47 48 const microCompactPrompt = `Summarize this tool result in 1-2 sentences. Preserve exact error strings, file paths, URLs, IDs, and numbers when present. Focus on the final outcome or conclusion. 49 50 Tool: %s 51 Result: 52 %s` 53 54 // microCompactResult uses the small LLM tier to produce a 1-2 sentence semantic 55 // summary of a tool result. Returns ("", false) if summarization fails or is 56 // skipped, signaling the caller to fall back to mechanical truncation. 57 func microCompactResult(ctx context.Context, c ctxwin.Completer, toolName, content string) (string, bool, client.Usage) { 58 if c == nil { 59 return "", false, client.Usage{} 60 } 61 62 prompt := fmt.Sprintf(microCompactPrompt, toolName, content) 63 64 resp, err := c.Complete(ctx, client.CompletionRequest{ 65 Messages: []client.Message{ 66 {Role: "user", Content: client.NewTextContent(prompt)}, 67 }, 68 ModelTier: "small", 69 Temperature: 0.0, 70 MaxTokens: 200, 71 }) 72 if err != nil || resp.OutputText == "" { 73 return "", false, client.Usage{} 74 } 75 summary := strings.TrimSpace(resp.OutputText) 76 if summary == "" { 77 return "", false, client.Usage{} 78 } 79 80 return microCompactMarker + summary, true, resp.Usage 81 } 82 83 // isMicroCompacted returns true if the content was already summarized by micro-compact. 84 func isMicroCompacted(content string) bool { 85 return strings.HasPrefix(content, microCompactMarker) 86 }