/ internal / agent / resultshape.go
resultshape.go
  1  package agent
  2  
  3  import (
  4  	"crypto/sha256"
  5  	"encoding/hex"
  6  	"fmt"
  7  	"regexp"
  8  	"strings"
  9  )
 10  
 11  type ResultProfile string
 12  
 13  const (
 14  	ResultProfileDefault ResultProfile = "default"
 15  	ResultProfileTree    ResultProfile = "tree"
 16  )
 17  
 18  type ShapedResult struct {
 19  	Text      string
 20  	Signature string
 21  }
 22  
 23  var treeRefPattern = regexp.MustCompile(`\bref=e\d+\b`)
 24  var treeWhitespacePattern = regexp.MustCompile(`[ \t]+`)
 25  
 26  func resultProfileForTool(toolName string) ResultProfile {
 27  	switch toolName {
 28  	case "browser_snapshot":
 29  		return ResultProfileTree
 30  	default:
 31  		return ResultProfileDefault
 32  	}
 33  }
 34  
 35  func shapeContextResult(toolName, content string, previous *ShapedResult) ShapedResult {
 36  	switch resultProfileForTool(toolName) {
 37  	case ResultProfileTree:
 38  		return shapeTreeResult(content, previous)
 39  	default:
 40  		return ShapedResult{Text: content}
 41  	}
 42  }
 43  
 44  func shapeContextKey(toolName string, traits CallStateTraits, tracker *stateVersionTracker) string {
 45  	if tracker == nil || len(traits.Reads) == 0 {
 46  		return toolName
 47  	}
 48  	if fingerprint := tracker.fingerprint(traits.Reads); fingerprint != "" {
 49  		return toolName + "\x00" + fingerprint
 50  	}
 51  	return toolName
 52  }
 53  
 54  func shapeTreeResult(content string, previous *ShapedResult) ShapedResult {
 55  	normalizedLines := normalizeTreeLines(content)
 56  	if len(normalizedLines) == 0 {
 57  		return ShapedResult{Text: content}
 58  	}
 59  
 60  	signature := shortStableHash(strings.Join(normalizedLines, "\n"))
 61  	if len([]rune(content)) < 2000 && len(normalizedLines) < 30 {
 62  		return ShapedResult{Text: content, Signature: signature}
 63  	}
 64  
 65  	refCount := 0
 66  	for _, line := range normalizedLines {
 67  		if strings.Contains(line, "ref=*") {
 68  			refCount++
 69  		}
 70  	}
 71  
 72  	if previous != nil && previous.Signature == signature {
 73  		return ShapedResult{
 74  			Text:      fmt.Sprintf("[tree snapshot unchanged since last read; signature %s; %d lines; %d refs]", signature, len(normalizedLines), refCount),
 75  			Signature: signature,
 76  		}
 77  	}
 78  
 79  	header := fmt.Sprintf("[tree snapshot summary; signature %s; %d lines; %d refs]", signature, len(normalizedLines), refCount)
 80  	if previous != nil && previous.Signature != "" {
 81  		header = fmt.Sprintf("[tree snapshot changed since last read; signature %s; previous %s; %d lines; %d refs]", signature, previous.Signature, len(normalizedLines), refCount)
 82  	}
 83  
 84  	excerpt := buildTreeExcerpt(normalizedLines, 18, 1600)
 85  	if excerpt == "" {
 86  		return ShapedResult{Text: header, Signature: signature}
 87  	}
 88  	return ShapedResult{
 89  		Text:      header + "\n" + excerpt,
 90  		Signature: signature,
 91  	}
 92  }
 93  
 94  func normalizeTreeLines(content string) []string {
 95  	lines := strings.Split(content, "\n")
 96  	normalized := make([]string, 0, len(lines))
 97  	for _, line := range lines {
 98  		line = treeRefPattern.ReplaceAllString(line, "ref=*")
 99  		line = treeWhitespacePattern.ReplaceAllString(line, " ")
100  		line = strings.TrimSpace(line)
101  		if line == "" {
102  			continue
103  		}
104  		normalized = append(normalized, line)
105  	}
106  	return normalized
107  }
108  
109  func buildTreeExcerpt(lines []string, maxLines, maxChars int) string {
110  	if len(lines) == 0 || maxLines <= 0 || maxChars <= 0 {
111  		return ""
112  	}
113  
114  	var excerpt []string
115  	chars := 0
116  	for _, line := range lines {
117  		if len(excerpt) >= maxLines {
118  			break
119  		}
120  		if len([]rune(line)) > 140 {
121  			runes := []rune(line)
122  			line = string(runes[:140]) + "..."
123  		}
124  		nextChars := chars + len([]rune(line))
125  		if len(excerpt) > 0 {
126  			nextChars++
127  		}
128  		if nextChars > maxChars {
129  			break
130  		}
131  		excerpt = append(excerpt, line)
132  		chars = nextChars
133  	}
134  	return strings.Join(excerpt, "\n")
135  }
136  
137  func shortStableHash(content string) string {
138  	sum := sha256.Sum256([]byte(content))
139  	return hex.EncodeToString(sum[:])[:12]
140  }