on main help sign in
Merge pull request #394 from charmbracelet/truncate_tool_results
e494646Kujtim Hoxha
internal/llm/tools/tools.go+74−1
@@ -3,6 +3,8 @@ package tools
3import (
3import (
4 "context"
4 "context"
5 "encoding/json"
5 "encoding/json"
6+ "fmt"
7+ "strings"
6)
8)
7
9
8type ToolInfo struct {
10type ToolInfo struct {
@@ -25,6 +27,10 @@ const (
25
27
26 SessionIDContextKey sessionIDContextKey = "session_id"
28 SessionIDContextKey sessionIDContextKey = "session_id"
27 MessageIDContextKey messageIDContextKey = "message_id"
29 MessageIDContextKey messageIDContextKey = "message_id"
30+
31+ maxResponseWidth = 3000
32+ maxResponseHeight = 5000
33+ maxResponseChars = 50000
28)
34)
29
35
30type ToolResponse struct {
36type ToolResponse struct {
@@ -37,10 +43,77 @@ type ToolResponse struct {
37func NewTextResponse(content string) ToolResponse {
43func NewTextResponse(content string) ToolResponse {
38 return ToolResponse{
44 return ToolResponse{
39 Type: ToolResponseTypeText,
45 Type: ToolResponseTypeText,
40− Content: content,
46+ Content: truncateContent(content),
41 }
47 }
42}
48}
43
49
50+func truncateContent(content string) string {
51+ if len(content) <= maxResponseChars {
52+ return truncateWidthAndHeight(content)
53+ }
54+
55+ truncated := content[:maxResponseChars]
56+
57+ if lastNewline := strings.LastIndex(truncated, "\n"); lastNewline > maxResponseChars/2 {
58+ truncated = truncated[:lastNewline]
59+ }
60+
61+ truncated += "\n\n... [Content truncated due to length] ..."
62+
63+ return truncateWidthAndHeight(truncated)
64+}
65+
66+func truncateWidthAndHeight(content string) string {
67+ lines := strings.Split(content, "\n")
68+
69+ heightTruncated := false
70+ if len(lines) > maxResponseHeight {
71+ keepLines := maxResponseHeight - 3
72+ firstHalf := keepLines / 2
73+ secondHalf := keepLines - firstHalf
74+
75+ truncatedLines := make([]string, 0, maxResponseHeight)
76+ truncatedLines = append(truncatedLines, lines[:firstHalf]...)
77+ truncatedLines = append(truncatedLines, "")
78+ truncatedLines = append(truncatedLines, fmt.Sprintf("... [%d lines truncated] ...", len(lines)-keepLines))
79+ truncatedLines = append(truncatedLines, "")
80+ truncatedLines = append(truncatedLines, lines[len(lines)-secondHalf:]...)
81+
82+ lines = truncatedLines
83+ heightTruncated = true
84+ }
85+
86+ widthTruncated := false
87+ for i, line := range lines {
88+ if len(line) > maxResponseWidth {
89+ if maxResponseWidth > 20 {
90+ keepChars := maxResponseWidth - 10
91+ firstHalf := keepChars / 2
92+ secondHalf := keepChars - firstHalf
93+ lines[i] = line[:firstHalf] + " ... " + line[len(line)-secondHalf:]
94+ } else {
95+ lines[i] = line[:maxResponseWidth]
96+ }
97+ widthTruncated = true
98+ }
99+ }
100+
101+ result := strings.Join(lines, "\n")
102+
103+ if heightTruncated || widthTruncated {
104+ notices := make([]string, 0, 2)
105+ if heightTruncated {
106+ notices = append(notices, "height")
107+ }
108+ if widthTruncated {
109+ notices = append(notices, "width")
110+ }
111+ result += fmt.Sprintf("\n\n[Note: Content truncated by %s to fit response limits]", strings.Join(notices, " and "))
112+ }
113+
114+ return result
115+}
116+
44func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse {
117func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse {
45 if metadata != nil {
118 if metadata != nil {
46 metadataBytes, err := json.Marshal(metadata)
119 metadataBytes, err := json.Marshal(metadata)