@ebowwa/coder 0.7.64 → 0.7.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/index.js +36233 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +377 -176
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
@@ -1,578 +0,0 @@
1
- /**
2
- * Context Compaction - Reduces context size while preserving important information
3
- *
4
- * When the context window fills up, we need to compact messages to continue.
5
- * This module provides token estimation, summarization, and compaction utilities.
6
- */
7
-
8
- import type { Message, ContentBlock, ToolUseBlock, ToolResultBlock } from "../types/index.js";
9
- import { SUMMARIZATION_MODEL } from "./models.js";
10
-
11
- // ============================================
12
- // CONSTANTS
13
- // ============================================
14
-
15
- /** Approximate characters per token (rough estimate for Claude models) */
16
- const CHARS_PER_TOKEN = 4;
17
-
18
- /** Default number of recent messages to keep during compaction */
19
- const DEFAULT_KEEP_LAST = 5;
20
-
21
- /** Default number of initial messages to keep (usually just the first user query) */
22
- const DEFAULT_KEEP_FIRST = 1;
23
-
24
- /** Minimum messages required before compaction is possible */
25
- const MIN_MESSAGES_FOR_COMPACTION = 8;
26
-
27
- /** Default threshold for proactive compaction (90% of max tokens) */
28
- const DEFAULT_COMPACTION_THRESHOLD = 0.9;
29
-
30
- /** Maximum length for summary text before truncation */
31
- const MAX_SUMMARY_LENGTH = 8000;
32
-
33
- /** Maximum tokens for summary output */
34
- const SUMMARY_MAX_TOKENS = 2000;
35
-
36
- /** System prompt for summarization */
37
- const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarizer. Your job is to create concise, information-dense summaries of conversation history.
38
-
39
- Guidelines:
40
- - Preserve all important decisions, file changes, and key information
41
- - Keep track of what tools were used and their outcomes
42
- - Note any errors encountered and how they were resolved
43
- - Maintain chronological flow
44
- - Be extremely concise - use bullet points and short sentences
45
- - Focus on information that would be needed to continue the conversation
46
- - Do not include pleasantries or filler text
47
-
48
- Format your summary as:
49
- ## Summary
50
- [Brief overview of what was discussed]
51
-
52
- ## Key Actions
53
- - [Action 1]
54
- - [Action 2]
55
-
56
- ## Files Modified
57
- - [file]: [what changed]
58
-
59
- ## Important Context
60
- [Any critical information needed going forward]`;
61
-
62
- /** User prompt template for summarization */
63
- const SUMMARIZATION_PROMPT = `Summarize the following conversation messages for context compaction. Preserve all important information in a concise format.
64
-
65
- Messages to summarize:
66
- {{MESSAGES}}
67
-
68
- Provide a dense, information-rich summary that captures everything needed to continue this conversation.`;
69
-
70
- // ============================================
71
- // TOKEN ESTIMATION
72
- // ============================================
73
-
74
- /**
75
- * Estimate the number of tokens in a text string.
76
- * Uses a simple heuristic of ~4 characters per token.
77
- * This is a rough estimate; actual tokenization varies by model.
78
- */
79
- export function estimateTokens(text: string): number {
80
- if (!text || text.length === 0) return 0;
81
- return Math.ceil(text.length / CHARS_PER_TOKEN);
82
- }
83
-
84
- /**
85
- * Estimate tokens for a single content block
86
- */
87
- function estimateBlockTokens(block: ContentBlock): number {
88
- switch (block.type) {
89
- case "text":
90
- return estimateTokens(block.text);
91
- case "image":
92
- // Images are roughly 85-110 tokens for standard sizes
93
- // Use 100 as an average estimate
94
- return 100;
95
- case "tool_use":
96
- // Tool use: name + JSON input
97
- const toolInput = JSON.stringify(block.input);
98
- return estimateTokens(block.name) + estimateTokens(toolInput) + 10; // overhead
99
- case "tool_result":
100
- // Tool result: content + metadata
101
- if (typeof block.content === "string") {
102
- return estimateTokens(block.content) + 10;
103
- }
104
- return block.content.reduce((sum, b) => sum + estimateBlockTokens(b), 0) + 10;
105
- case "thinking":
106
- return estimateTokens(block.thinking);
107
- default:
108
- return 0;
109
- }
110
- }
111
-
112
- /**
113
- * Estimate the total number of tokens in a message
114
- */
115
- function estimateMessageTokens(message: Message): number {
116
- // Role overhead (~4 tokens)
117
- const roleOverhead = 4;
118
-
119
- // Sum all content blocks
120
- const contentTokens = message.content.reduce(
121
- (sum, block) => sum + estimateBlockTokens(block),
122
- 0
123
- );
124
-
125
- return roleOverhead + contentTokens;
126
- }
127
-
128
- /**
129
- * Get total estimated tokens across all messages
130
- */
131
- export function estimateMessagesTokens(messages: Message[]): number {
132
- if (!messages || messages.length === 0) return 0;
133
- return messages.reduce((sum, msg) => sum + estimateMessageTokens(msg), 0);
134
- }
135
-
136
- // ============================================
137
- // CONTENT EXTRACTION
138
- // ============================================
139
-
140
- /**
141
- * Extract text content from a message for summarization
142
- */
143
- function extractTextFromMessage(message: Message): string {
144
- const parts: string[] = [];
145
-
146
- for (const block of message.content) {
147
- switch (block.type) {
148
- case "text":
149
- parts.push(block.text);
150
- break;
151
- case "tool_use":
152
- parts.push(`[Tool: ${block.name}(${JSON.stringify(block.input)})]`);
153
- break;
154
- case "tool_result":
155
- const content = typeof block.content === "string"
156
- ? block.content
157
- : block.content.map(b => b.type === "text" ? b.text : "[content]").join("");
158
- parts.push(`[Result: ${content.slice(0, 500)}${content.length > 500 ? "..." : ""}]`);
159
- break;
160
- case "thinking":
161
- parts.push(`[Thinking: ${block.thinking.slice(0, 200)}...]`);
162
- break;
163
- }
164
- }
165
-
166
- return parts.join("\n");
167
- }
168
-
169
- /**
170
- * Extract tool use/result pairs from messages for preservation
171
- */
172
- function extractToolPairs(messages: Message[]): Map<string, { use: ToolUseBlock; result?: ToolResultBlock }> {
173
- const toolPairs = new Map<string, { use: ToolUseBlock; result?: ToolResultBlock }>();
174
-
175
- // First pass: collect all tool uses
176
- for (const message of messages) {
177
- for (const block of message.content) {
178
- if (block.type === "tool_use") {
179
- toolPairs.set(block.id, { use: block });
180
- }
181
- }
182
- }
183
-
184
- // Second pass: match results to uses
185
- for (const message of messages) {
186
- for (const block of message.content) {
187
- if (block.type === "tool_result") {
188
- const pair = toolPairs.get(block.tool_use_id);
189
- if (pair) {
190
- pair.result = block;
191
- }
192
- }
193
- }
194
- }
195
-
196
- return toolPairs;
197
- }
198
-
199
- // ============================================
200
- // SUMMARIZATION
201
- // ============================================
202
-
203
- /**
204
- * Summarize a range of messages into a single text.
205
- * This is a simple implementation that concatenates and truncates.
206
- * Can be enhanced later with LLM-based summarization.
207
- */
208
- export async function summarizeMessages(messages: Message[]): Promise<string> {
209
- if (!messages || messages.length === 0) {
210
- return "";
211
- }
212
-
213
- const summaryParts: string[] = [];
214
- summaryParts.push(`[Context Summary: ${messages.length} messages compacted]\n`);
215
-
216
- // Track tool operations for a cleaner summary
217
- const toolOperations: string[] = [];
218
-
219
- for (let i = 0; i < messages.length; i++) {
220
- const message = messages[i];
221
- if (!message) continue;
222
-
223
- const role = message.role.toUpperCase();
224
- const text = extractTextFromMessage(message);
225
-
226
- // Track tool operations
227
- for (const block of message.content) {
228
- if (block.type === "tool_use") {
229
- toolOperations.push(`${block.name}`);
230
- }
231
- }
232
-
233
- // Add truncated message content
234
- const truncated = text.length > 300 ? `${text.slice(0, 300)}...` : text;
235
- summaryParts.push(`${role}: ${truncated}\n`);
236
- }
237
-
238
- // Add tool summary
239
- if (toolOperations.length > 0) {
240
- const toolCounts = toolOperations.reduce((acc, tool) => {
241
- acc[tool] = (acc[tool] || 0) + 1;
242
- return acc;
243
- }, {} as Record<string, number>);
244
-
245
- const toolSummary = Object.entries(toolCounts)
246
- .map(([name, count]) => `${name}(${count})`)
247
- .join(", ");
248
- summaryParts.push(`\nTools used: ${toolSummary}\n`);
249
- }
250
-
251
- let summary = summaryParts.join("");
252
-
253
- // Truncate if too long
254
- if (summary.length > MAX_SUMMARY_LENGTH) {
255
- summary = summary.slice(0, MAX_SUMMARY_LENGTH) + "\n...[truncated]";
256
- }
257
-
258
- return summary;
259
- }
260
-
261
- // ============================================
262
- // LLM-BASED SUMMARIZATION
263
- // ============================================
264
-
265
- export interface LLMSummarizationOptions {
266
- /** API key for the LLM */
267
- apiKey?: string;
268
- /** Model to use for summarization (default: haiku) */
269
- model?: string;
270
- /** Base URL for API (for non-Anthropic providers) */
271
- baseUrl?: string;
272
- /** Timeout in ms (default: 30000) */
273
- timeout?: number;
274
- }
275
-
276
- /**
277
- * Summarize messages using an LLM for better context preservation.
278
- * Falls back to simple truncation if LLM fails or no API key provided.
279
- */
280
- export async function summarizeWithLLM(
281
- messages: Message[],
282
- options: LLMSummarizationOptions = {}
283
- ): Promise<string> {
284
- const {
285
- apiKey = process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY,
286
- model = SUMMARIZATION_MODEL,
287
- baseUrl = process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
288
- timeout = 30000,
289
- } = options;
290
-
291
- // No API key - fall back to simple summarization
292
- if (!apiKey) {
293
- return summarizeMessages(messages);
294
- }
295
-
296
- try {
297
- // Build the conversation text for summarization
298
- const conversationText = messages.map((msg) => {
299
- const role = msg.role.toUpperCase();
300
- const text = extractTextFromMessage(msg);
301
-
302
- // Extract tool info
303
- const tools: string[] = [];
304
- for (const block of msg.content) {
305
- if (block.type === "tool_use") {
306
- tools.push(`[TOOL_USE: ${block.name}]`);
307
- } else if (block.type === "tool_result") {
308
- const resultBlock = block as ToolResultBlock;
309
- const preview = typeof resultBlock.content === "string"
310
- ? resultBlock.content.slice(0, 200)
311
- : "[complex result]";
312
- tools.push(`[TOOL_RESULT: ${resultBlock.is_error ? "ERROR" : "OK"}] ${preview}`);
313
- }
314
- }
315
-
316
- const toolsStr = tools.length > 0 ? `\n${tools.join("\n")}` : "";
317
- return `${role}:\n${text.slice(0, 2000)}${toolsStr}`;
318
- }).join("\n\n---\n\n");
319
-
320
- // Build request
321
- const requestBody = {
322
- model,
323
- max_tokens: SUMMARY_MAX_TOKENS,
324
- system: SUMMARIZATION_SYSTEM_PROMPT,
325
- messages: [{
326
- role: "user" as const,
327
- content: SUMMARIZATION_PROMPT.replace("{{MESSAGES}}", conversationText),
328
- }],
329
- };
330
-
331
- // Make API call with timeout
332
- const controller = new AbortController();
333
- const timeoutId = setTimeout(() => controller.abort(), timeout);
334
-
335
- try {
336
- const response = await fetch(`${baseUrl}/v1/messages`, {
337
- method: "POST",
338
- headers: {
339
- "Content-Type": "application/json",
340
- "x-api-key": apiKey,
341
- "anthropic-version": "2023-06-01",
342
- },
343
- body: JSON.stringify(requestBody),
344
- signal: controller.signal,
345
- });
346
-
347
- clearTimeout(timeoutId);
348
-
349
- if (!response.ok) {
350
- const errorText = await response.text();
351
- console.error(`\x1b[33m[Compaction] LLM summarization failed: ${response.status} - ${errorText}\x1b[0m`);
352
- return summarizeMessages(messages);
353
- }
354
-
355
- const data = await response.json() as {
356
- content?: Array<{ type: string; text?: string }>;
357
- usage?: { input_tokens: number; output_tokens: number };
358
- };
359
-
360
- // Extract text from response
361
- const summaryText = data.content
362
- ?.filter((block) => block.type === "text")
363
- .map((block) => block.text || "")
364
- .join("\n") || "";
365
-
366
- if (!summaryText) {
367
- console.error("\x1b[33m[Compaction] LLM returned empty summary\x1b[0m");
368
- return summarizeMessages(messages);
369
- }
370
-
371
- // Log usage for debugging
372
- if (data.usage) {
373
- console.log(`\x1b[90m[Compaction] LLM summary: ${data.usage.input_tokens} in, ${data.usage.output_tokens} out\x1b[0m`);
374
- }
375
-
376
- return `[LLM Summary of ${messages.length} messages]\n\n${summaryText}`;
377
-
378
- } finally {
379
- clearTimeout(timeoutId);
380
- }
381
-
382
- } catch (error) {
383
- const errorMsg = error instanceof Error ? error.message : String(error);
384
- console.error(`\x1b[33m[Compaction] LLM summarization error: ${errorMsg}\x1b[0m`);
385
- // Fall back to simple summarization
386
- return summarizeMessages(messages);
387
- }
388
- }
389
-
390
- // ============================================
391
- // COMPACTION
392
- // ============================================
393
-
394
- export interface CompactionOptions {
395
- /** Number of initial messages to keep unchanged */
396
- keepFirst?: number;
397
- /** Number of recent messages to keep unchanged */
398
- keepLast?: number;
399
- /** Whether to preserve tool_use/tool_result pairs */
400
- preserveToolPairs?: boolean;
401
- /** Use LLM for summarization (default: true if API key available) */
402
- useLLMSummarization?: boolean;
403
- /** API key for LLM summarization (falls back to env) */
404
- apiKey?: string;
405
- /** Base URL for API (for non-Anthropic providers like Z.AI) */
406
- baseUrl?: string;
407
- }
408
-
409
- export interface CompactionResult {
410
- /** The compacted messages */
411
- messages: Message[];
412
- /** Number of messages removed */
413
- messagesRemoved: number;
414
- /** Estimated tokens before compaction */
415
- tokensBefore: number;
416
- /** Estimated tokens after compaction */
417
- tokensAfter: number;
418
- /** Whether compaction actually occurred */
419
- didCompact: boolean;
420
- }
421
-
422
- /**
423
- * Compact messages to fit within a token limit.
424
- *
425
- * Strategy:
426
- * 1. Always keep the first N messages (original query)
427
- * 2. Always keep the last M messages (recent context)
428
- * 3. Summarize middle messages into a single "context summary" user message
429
- * 4. Preserve tool_use/tool_result pairs when possible
430
- */
431
- export async function compactMessages(
432
- messages: Message[],
433
- maxTokens: number,
434
- options: CompactionOptions = {}
435
- ): Promise<CompactionResult> {
436
- const {
437
- keepFirst = DEFAULT_KEEP_FIRST,
438
- keepLast = DEFAULT_KEEP_LAST,
439
- preserveToolPairs = true,
440
- useLLMSummarization = true, // Default to LLM summarization
441
- apiKey,
442
- baseUrl,
443
- } = options;
444
-
445
- const tokensBefore = estimateMessagesTokens(messages);
446
-
447
- // If already under limit, no compaction needed
448
- if (tokensBefore <= maxTokens) {
449
- return {
450
- messages,
451
- messagesRemoved: 0,
452
- tokensBefore,
453
- tokensAfter: tokensBefore,
454
- didCompact: false,
455
- };
456
- }
457
-
458
- // Not enough messages to compact - silent return
459
- if (messages.length <= keepFirst + keepLast) {
460
- return {
461
- messages,
462
- messagesRemoved: 0,
463
- tokensBefore,
464
- tokensAfter: tokensBefore,
465
- didCompact: false,
466
- };
467
- }
468
-
469
- // Extract segments
470
- const firstMessages = messages.slice(0, keepFirst);
471
- const middleMessages = messages.slice(keepFirst, -keepLast);
472
- const lastMessages = messages.slice(-keepLast);
473
-
474
- // Create summary of middle messages (use LLM if available, fallback to simple)
475
- const summary = useLLMSummarization
476
- ? await summarizeWithLLM(middleMessages, { apiKey, baseUrl })
477
- : await summarizeMessages(middleMessages);
478
-
479
- // Build summary message
480
- const summaryMessage: Message = {
481
- role: "user",
482
- content: [{
483
- type: "text",
484
- text: `[Previous context has been compacted for continuity]\n\n${summary}`,
485
- }],
486
- };
487
-
488
- // Optionally preserve important tool pairs
489
- let preservedBlocks: ContentBlock[] = [];
490
- if (preserveToolPairs && middleMessages.length > 0) {
491
- const toolPairs = extractToolPairs(middleMessages);
492
-
493
- // Keep the most recent tool use/result pairs (up to 3)
494
- const recentPairs = Array.from(toolPairs.values())
495
- .slice(-3)
496
- .filter(pair => pair.result && !pair.result.is_error);
497
-
498
- for (const pair of recentPairs) {
499
- preservedBlocks.push(pair.use as ContentBlock);
500
- if (pair.result) {
501
- preservedBlocks.push(pair.result as ContentBlock);
502
- }
503
- }
504
- }
505
-
506
- // Build compacted message list
507
- const compacted: Message[] = [
508
- ...firstMessages,
509
- summaryMessage,
510
- ];
511
-
512
- // Add preserved tool results if any
513
- if (preservedBlocks.length > 0) {
514
- compacted.push({
515
- role: "assistant",
516
- content: preservedBlocks.filter(b => b.type === "tool_use"),
517
- });
518
- compacted.push({
519
- role: "user",
520
- content: preservedBlocks.filter(b => b.type === "tool_result"),
521
- });
522
- }
523
-
524
- // Add recent messages
525
- compacted.push(...lastMessages);
526
-
527
- const tokensAfter = estimateMessagesTokens(compacted);
528
- const messagesRemoved = messages.length - compacted.length;
529
-
530
- console.log(`Context compaction: ${messages.length} -> ${compacted.length} messages, ${tokensBefore} -> ${tokensAfter} tokens`);
531
-
532
- return {
533
- messages: compacted,
534
- messagesRemoved,
535
- tokensBefore,
536
- tokensAfter,
537
- didCompact: true,
538
- };
539
- }
540
-
541
- /**
542
- * Check if compaction is needed proactively.
543
- * Returns true if current token usage exceeds the threshold AND there are enough messages to compact.
544
- */
545
- export function needsCompaction(
546
- messages: Message[],
547
- maxTokens: number,
548
- threshold: number = DEFAULT_COMPACTION_THRESHOLD
549
- ): boolean {
550
- // Not enough messages to meaningfully compact
551
- if (messages.length < MIN_MESSAGES_FOR_COMPACTION) {
552
- return false;
553
- }
554
-
555
- const currentTokens = estimateMessagesTokens(messages);
556
- const thresholdTokens = Math.floor(maxTokens * threshold);
557
- return currentTokens >= thresholdTokens;
558
- }
559
-
560
- /**
561
- * Get compaction statistics for logging/metrics
562
- */
563
- export function getCompactionStats(result: CompactionResult): {
564
- reductionPercent: number;
565
- tokensSaved: number;
566
- } {
567
- if (!result.didCompact) {
568
- return { reductionPercent: 0, tokensSaved: 0 };
569
- }
570
-
571
- const tokensSaved = result.tokensBefore - result.tokensAfter;
572
- const reductionPercent = (tokensSaved / result.tokensBefore) * 100;
573
-
574
- return {
575
- reductionPercent: Math.round(reductionPercent * 100) / 100,
576
- tokensSaved,
577
- };
578
- }