/ internal / agent / microcompact.go
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  }