forked from sanbuphy/learn-coding-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontextSuggestions.ts
More file actions
235 lines (209 loc) · 7.09 KB
/
contextSuggestions.ts
File metadata and controls
235 lines (209 loc) · 7.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js'
import { FILE_READ_TOOL_NAME } from '../tools/FileReadTool/prompt.js'
import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js'
import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js'
import type { ContextData } from './analyzeContext.js'
import { getDisplayPath } from './file.js'
import { formatTokens } from './format.js'
// --
export type SuggestionSeverity = 'info' | 'warning'
export type ContextSuggestion = {
severity: SuggestionSeverity
title: string
detail: string
/** Estimated tokens that could be saved */
savingsTokens?: number
}
// Thresholds for triggering suggestions
const LARGE_TOOL_RESULT_PERCENT = 15 // tool results > 15% of context
const LARGE_TOOL_RESULT_TOKENS = 10_000
const READ_BLOAT_PERCENT = 5 // Read results > 5% of context
const NEAR_CAPACITY_PERCENT = 80
const MEMORY_HIGH_PERCENT = 5
const MEMORY_HIGH_TOKENS = 5_000
// --
export function generateContextSuggestions(
data: ContextData,
): ContextSuggestion[] {
const suggestions: ContextSuggestion[] = []
checkNearCapacity(data, suggestions)
checkLargeToolResults(data, suggestions)
checkReadResultBloat(data, suggestions)
checkMemoryBloat(data, suggestions)
checkAutoCompactDisabled(data, suggestions)
// Sort: warnings first, then by savings descending
suggestions.sort((a, b) => {
if (a.severity !== b.severity) {
return a.severity === 'warning' ? -1 : 1
}
return (b.savingsTokens ?? 0) - (a.savingsTokens ?? 0)
})
return suggestions
}
// --
function checkNearCapacity(
data: ContextData,
suggestions: ContextSuggestion[],
): void {
if (data.percentage >= NEAR_CAPACITY_PERCENT) {
suggestions.push({
severity: 'warning',
title: `Context is ${data.percentage}% full`,
detail: data.isAutoCompactEnabled
? 'Autocompact will trigger soon, which discards older messages. Use /compact now to control what gets kept.'
: 'Autocompact is disabled. Use /compact to free space, or enable autocompact in /config.',
})
}
}
function checkLargeToolResults(
data: ContextData,
suggestions: ContextSuggestion[],
): void {
if (!data.messageBreakdown) return
for (const tool of data.messageBreakdown.toolCallsByType) {
const totalToolTokens = tool.callTokens + tool.resultTokens
const percent = (totalToolTokens / data.rawMaxTokens) * 100
if (
percent < LARGE_TOOL_RESULT_PERCENT ||
totalToolTokens < LARGE_TOOL_RESULT_TOKENS
) {
continue
}
const suggestion = getLargeToolSuggestion(
tool.name,
totalToolTokens,
percent,
)
if (suggestion) {
suggestions.push(suggestion)
}
}
}
function getLargeToolSuggestion(
toolName: string,
tokens: number,
percent: number,
): ContextSuggestion | null {
const tokenStr = formatTokens(tokens)
switch (toolName) {
case BASH_TOOL_NAME:
return {
severity: 'warning',
title: `Bash results using ${tokenStr} tokens (${percent.toFixed(0)}%)`,
detail:
'Pipe output through head, tail, or grep to reduce result size. Avoid cat on large files \u2014 use Read with offset/limit instead.',
savingsTokens: Math.floor(tokens * 0.5),
}
case FILE_READ_TOOL_NAME:
return {
severity: 'info',
title: `Read results using ${tokenStr} tokens (${percent.toFixed(0)}%)`,
detail:
'Use offset and limit parameters to read only the sections you need. Avoid re-reading entire files when you only need a few lines.',
savingsTokens: Math.floor(tokens * 0.3),
}
case GREP_TOOL_NAME:
return {
severity: 'info',
title: `Grep results using ${tokenStr} tokens (${percent.toFixed(0)}%)`,
detail:
'Add more specific patterns or use the glob or type parameter to narrow file types. Consider Glob for file discovery instead of Grep.',
savingsTokens: Math.floor(tokens * 0.3),
}
case WEB_FETCH_TOOL_NAME:
return {
severity: 'info',
title: `WebFetch results using ${tokenStr} tokens (${percent.toFixed(0)}%)`,
detail:
'Web page content can be very large. Consider extracting only the specific information needed.',
savingsTokens: Math.floor(tokens * 0.4),
}
default:
if (percent >= 20) {
return {
severity: 'info',
title: `${toolName} using ${tokenStr} tokens (${percent.toFixed(0)}%)`,
detail: `This tool is consuming a significant portion of context.`,
savingsTokens: Math.floor(tokens * 0.2),
}
}
return null
}
}
function checkReadResultBloat(
data: ContextData,
suggestions: ContextSuggestion[],
): void {
if (!data.messageBreakdown) return
const callsByType = data.messageBreakdown.toolCallsByType
const readTool = callsByType.find(t => t.name === FILE_READ_TOOL_NAME)
if (!readTool) return
const totalReadTokens = readTool.callTokens + readTool.resultTokens
const totalReadPercent = (totalReadTokens / data.rawMaxTokens) * 100
const readPercent = (readTool.resultTokens / data.rawMaxTokens) * 100
// Skip if already covered by checkLargeToolResults (>= 15% band)
if (
totalReadPercent >= LARGE_TOOL_RESULT_PERCENT &&
totalReadTokens >= LARGE_TOOL_RESULT_TOKENS
) {
return
}
if (
readPercent >= READ_BLOAT_PERCENT &&
readTool.resultTokens >= LARGE_TOOL_RESULT_TOKENS
) {
suggestions.push({
severity: 'info',
title: `File reads using ${formatTokens(readTool.resultTokens)} tokens (${readPercent.toFixed(0)}%)`,
detail:
'If you are re-reading files, consider referencing earlier reads. Use offset/limit for large files.',
savingsTokens: Math.floor(readTool.resultTokens * 0.3),
})
}
}
function checkMemoryBloat(
data: ContextData,
suggestions: ContextSuggestion[],
): void {
const totalMemoryTokens = data.memoryFiles.reduce(
(sum, f) => sum + f.tokens,
0,
)
const memoryPercent = (totalMemoryTokens / data.rawMaxTokens) * 100
if (
memoryPercent >= MEMORY_HIGH_PERCENT &&
totalMemoryTokens >= MEMORY_HIGH_TOKENS
) {
const largestFiles = [...data.memoryFiles]
.sort((a, b) => b.tokens - a.tokens)
.slice(0, 3)
.map(f => {
const name = getDisplayPath(f.path)
return `${name} (${formatTokens(f.tokens)})`
})
.join(', ')
suggestions.push({
severity: 'info',
title: `Memory files using ${formatTokens(totalMemoryTokens)} tokens (${memoryPercent.toFixed(0)}%)`,
detail: `Largest: ${largestFiles}. Use /memory to review and prune stale entries.`,
savingsTokens: Math.floor(totalMemoryTokens * 0.3),
})
}
}
function checkAutoCompactDisabled(
data: ContextData,
suggestions: ContextSuggestion[],
): void {
if (
!data.isAutoCompactEnabled &&
data.percentage >= 50 &&
data.percentage < NEAR_CAPACITY_PERCENT
) {
suggestions.push({
severity: 'info',
title: 'Autocompact is disabled',
detail:
'Without autocompact, you will hit context limits and lose the conversation. Enable it in /config or use /compact manually.',
})
}
}