* Data-driven recommendations. Each one fires only when its threshold is hit, so the * advice is calibrated to THIS session, not a generic checklist. * * Thresholds intentionally on the strict side — we don't want noise.
(args: {
totals: TokenReport['totals'];
breakdowns: TurnBreakdown[];
toolHotspots: TokenReport['toolHotspots'];
fileHotspots: TokenReport['fileHotspots'];
})
| 233 | * Thresholds intentionally on the strict side — we don't want noise. |
| 234 | */ |
| 235 | function buildRecommendations(args: { |
| 236 | totals: TokenReport['totals']; |
| 237 | breakdowns: TurnBreakdown[]; |
| 238 | toolHotspots: TokenReport['toolHotspots']; |
| 239 | fileHotspots: TokenReport['fileHotspots']; |
| 240 | }): string[] { |
| 241 | const recs: string[] = []; |
| 242 | const { totals, breakdowns, toolHotspots, fileHotspots } = args; |
| 243 | const turnCount = breakdowns.length; |
| 244 | if (turnCount === 0) return ['Session has no turns yet — run a task first.']; |
| 245 | |
| 246 | // 1. Tool schemas dominate? Each turn reships them, so total = perTurn * turnCount |
| 247 | const schemasShare = totals.grandTotal > 0 ? totals.toolSchemas / totals.grandTotal : 0; |
| 248 | if (schemasShare > 0.4) { |
| 249 | recs.push( |
| 250 | `Tool schemas account for ${Math.round(schemasShare * 100)}% of all tokens ` + |
| 251 | `(${totals.toolSchemas.toLocaleString()} across ${turnCount} turns). ` + |
| 252 | `Highest-leverage fix: enable prompt caching (cloud) or KV-cache reuse (local Ollama/MLX), ` + |
| 253 | `OR slim tool descriptions. This is Lever 1 + Lever 2 in the optimization plan.`, |
| 254 | ); |
| 255 | } |
| 256 | |
| 257 | // 2. System prompt similar |
| 258 | const systemShare = totals.grandTotal > 0 ? totals.system / totals.grandTotal : 0; |
| 259 | if (systemShare > 0.2 && turnCount > 3) { |
| 260 | recs.push( |
| 261 | `System prompt is reshipped every turn (${totals.system.toLocaleString()} total = ` + |
| 262 | `${Math.round(systemShare * 100)}% of session). ` + |
| 263 | `Same caching strategy applies — system + tools should be a single cached prefix.`, |
| 264 | ); |
| 265 | } |
| 266 | |
| 267 | // 3. Repeated reads of the same file |
| 268 | const repeatedFiles = fileHotspots.filter(f => f.reads >= 3); |
| 269 | if (repeatedFiles.length > 0) { |
| 270 | const top = repeatedFiles.slice(0, 3).map(f => `${f.path} (${f.reads}×)`).join(', '); |
| 271 | recs.push( |
| 272 | `Repeated reads detected: ${top}. ` + |
| 273 | `Dedup with content hashing (Lever 3) would skip the redundant content. ` + |
| 274 | `Each repeat costs the full file body in tokens.`, |
| 275 | ); |
| 276 | } |
| 277 | |
| 278 | // 4. One tool dominates output |
| 279 | if (toolHotspots.length > 0) { |
| 280 | const top = toolHotspots[0]!; |
| 281 | const topShare = totals.toolResults > 0 ? top.outputTokens / totals.toolResults : 0; |
| 282 | if (topShare > 0.5 && top.outputTokens > 2000) { |
| 283 | recs.push( |
| 284 | `'${top.tool}' produced ${top.outputTokens.toLocaleString()} tokens of output ` + |
| 285 | `(${Math.round(topShare * 100)}% of all tool results), ` + |
| 286 | `avg ${top.avgPerCall.toLocaleString()} per call. ` + |
| 287 | `Consider truncation at the tool level (e.g. max bytes) or summarisation of older results in history.`, |
| 288 | ); |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | // 5. Total trajectory — is the session about to blow past a typical 32K context? |