Skip to content

Commit fb6d116

Browse files
committed
fix: cap query tool results to prevent token limit errors
A single tool result could be 647KB (20 rows with full log body examples). This gets stored in conversation history and sent with every API call, eventually hitting the 200K token limit. - Truncate any string field > 4KB with '…(truncated)' - Drop trailing rows if total serialized JSON > 100KB - Include rows_dropped count in result so the AI knows data was omitted
1 parent 957ef0c commit fb6d116

3 files changed

Lines changed: 56 additions & 8 deletions

File tree

‎internal/app/chat/messagelist/round/turn/assistant/blocks/tools/query/query.go‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ type Model struct {
4141
resultTemplate string
4242

4343
// Results
44-
rows []map[string]any
45-
err error
46-
duration time.Duration
44+
rows []map[string]any
45+
rowsDropped int
46+
err error
47+
duration time.Duration
4748
}
4849

4950
// New creates a new query tool model.
@@ -215,16 +216,17 @@ func (m *Model) execute() tea.Cmd {
215216
}
216217

217218
m.rows = result.Rows
219+
m.rowsDropped = result.RowsDropped
218220
m.state = tools.StateComplete
219-
m.scope.Info("query completed", "row_count", len(m.rows), "duration", m.duration)
221+
m.scope.Info("query completed", "row_count", len(m.rows), "rows_dropped", m.rowsDropped, "duration", m.duration)
220222
return m.fireCompleted()
221223
}
222224

223225
func (m *Model) fireCompleted() tea.Cmd {
224226
return func() tea.Msg {
225227
return msgs.QueryCompleted{
226228
ToolUseID: m.toolID,
227-
Result: domaintools.QueryResult{Rows: m.rows},
229+
Result: domaintools.QueryResult{Rows: m.rows, RowsDropped: m.rowsDropped},
228230
Error: m.err,
229231
}
230232
}

‎internal/chat/tools/query.go‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,46 @@ func (t *QueryTool) Execute(input json.RawMessage) (tools.QueryResult, error) {
109109
if err := rows.Err(); err != nil {
110110
return tools.QueryResult{}, err
111111
}
112-
return tools.QueryResult{Rows: results}, nil
112+
113+
capped, dropped := capResults(results)
114+
return tools.QueryResult{Rows: capped, RowsDropped: dropped}, nil
115+
}
116+
117+
const (
118+
maxFieldBytes = 4096 // truncate any string value longer than this
119+
maxResultBytes = 102400 // drop trailing rows if total JSON exceeds this
120+
)
121+
122+
// capResults truncates large string values and drops trailing rows to keep
123+
// the serialized result within size limits. Returns the capped rows and the
124+
// number of rows that were dropped.
125+
func capResults(rows []map[string]any) ([]map[string]any, int) {
126+
for _, row := range rows {
127+
for k, v := range row {
128+
s, ok := v.(string)
129+
if ok && len(s) > maxFieldBytes {
130+
row[k] = s[:maxFieldBytes] + "…(truncated)"
131+
}
132+
}
133+
}
134+
135+
b, err := json.Marshal(rows)
136+
if err != nil || len(b) <= maxResultBytes {
137+
return rows, 0
138+
}
139+
140+
// Binary search for the max number of rows that fit.
141+
total := len(rows)
142+
lo, hi := 0, total
143+
for lo < hi {
144+
mid := (lo + hi + 1) / 2
145+
b, _ = json.Marshal(rows[:mid])
146+
if len(b) <= maxResultBytes {
147+
lo = mid
148+
} else {
149+
hi = mid - 1
150+
}
151+
}
152+
153+
return rows[:lo], total - lo
113154
}

‎internal/domain/tools/query.go‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ type QueryInput struct {
99

1010
// QueryResult is the typed output of a query tool execution.
1111
type QueryResult struct {
12-
Rows []map[string]any `json:"rows"`
12+
Rows []map[string]any `json:"rows"`
13+
RowsDropped int `json:"rows_dropped,omitempty"`
1314
}
1415

1516
// ToMap serializes the result for the GraphQL API.
1617
func (r QueryResult) ToMap() map[string]any {
17-
return map[string]any{"rows": r.Rows}
18+
m := map[string]any{"rows": r.Rows}
19+
if r.RowsDropped > 0 {
20+
m["rows_dropped"] = r.RowsDropped
21+
}
22+
return m
1823
}

0 commit comments

Comments
 (0)