@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
@@ -493,7 +493,8 @@ export class CognitiveSecurityHooks {
493
493
  if (this.config.logEvents) {
494
494
  const prefix = action === "deny" ? "\x1b[31m[Security]\x1b[0m" : "\x1b[90m[Security]\x1b[0m";
495
495
  const toolStr = tool ? ` ${tool}:` : "";
496
- console.log(`${prefix}${toolStr} ${reason}`);
496
+ // Use console.error to avoid interfering with TUI (stdout is used by renderer)
497
+ console.error(`${prefix}${toolStr} ${reason}`);
497
498
  }
498
499
  }
499
500
 
@@ -0,0 +1,7 @@
1
+ mv
2
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/claude-md.ts
3
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/config-loader.ts
4
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/models.ts
5
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/permissions.ts
6
+
7
+ here aqi
@@ -0,0 +1,334 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { compactMessages, needsCompaction, getCompactionStats } from "../compaction.js";
3
+ import { LoopState } from "../../agent-loop/loop-state.js";
4
+ import type { Message, ContentBlock } from "../../../types/index.js";
5
+
6
+ /**
7
+ * Integration tests for context compaction module
8
+ * Tests compaction, token estimation, and LoopState integration
9
+ */
10
+ describe("Context Compaction Integration", () => {
11
+ // Helper to create text content blocks
12
+ const textBlock = (text: string): ContentBlock => ({ type: "text", text });
13
+
14
+ // Helper to create messages
15
+ const userMessage = (content: string | ContentBlock[]): Message => ({
16
+ role: "user",
17
+ content: Array.isArray(content) ? content : [textBlock(content)]
18
+ });
19
+
20
+ const assistantMessage = (content: string | ContentBlock[]): Message => ({
21
+ role: "assistant",
22
+ content: Array.isArray(content) ? content : [textBlock(content)]
23
+ });
24
+
25
+ describe("needsCompaction", () => {
26
+ it("returns false for empty messages", () => {
27
+ const messages: Message[] = [];
28
+ const result = needsCompaction(messages, 1000);
29
+ expect(result).toBe(false);
30
+ });
31
+
32
+ it("returns false for small messages", () => {
33
+ const messages = [
34
+ userMessage("Hello"),
35
+ assistantMessage("Hi there!")
36
+ ];
37
+
38
+ const result = needsCompaction(messages, 100000);
39
+ expect(result).toBe(false);
40
+ });
41
+
42
+ it("returns true when exceeding threshold", () => {
43
+ // Add many large messages to exceed threshold
44
+ const messages: Message[] = [];
45
+ for (let i = 0; i < 100; i++) {
46
+ messages.push(userMessage("This is a test message ".repeat(100)));
47
+ messages.push(assistantMessage("This is a response message ".repeat(100)));
48
+ }
49
+
50
+ const result = needsCompaction(messages, 1000);
51
+ expect(result).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe("compactMessages", () => {
56
+ it("compacts messages while preserving first and last", async () => {
57
+ const messages: Message[] = [];
58
+
59
+ // First messages (to preserve)
60
+ messages.push(userMessage("Initial question"));
61
+ messages.push(assistantMessage("Initial response"));
62
+
63
+ // Middle messages (to summarize)
64
+ for (let i = 0; i < 20; i++) {
65
+ messages.push(userMessage(`User question ${i}: ` + "test ".repeat(50)));
66
+ messages.push(assistantMessage(`Assistant answer ${i}: ` + "response ".repeat(50)));
67
+ }
68
+
69
+ // Last messages (to preserve)
70
+ messages.push(userMessage("Final question"));
71
+ messages.push(assistantMessage("Final response"));
72
+
73
+ // Use lower maxTokens to ensure compaction triggers
74
+ const result = await compactMessages(messages, 500, {
75
+ keepFirst: 2,
76
+ keepLast: 2,
77
+ useLLMSummarization: false
78
+ });
79
+
80
+ // Verify compaction occurred
81
+ expect(result.didCompact).toBe(true);
82
+ expect(result.messages.length).toBeLessThan(messages.length);
83
+ expect(result.messages.length).toBeGreaterThan(0);
84
+ expect(result.tokensAfter).toBeLessThan(result.tokensBefore);
85
+
86
+ // Verify first messages preserved
87
+ expect(result.messages[0]).toEqual(messages[0]);
88
+ expect(result.messages[1]).toEqual(messages[1]);
89
+
90
+ // Verify last messages preserved
91
+ const lastIdx = result.messages.length - 1;
92
+ expect(result.messages[lastIdx - 1]).toEqual(messages[messages.length - 2]);
93
+ expect(result.messages[lastIdx]).toEqual(messages[messages.length - 1]);
94
+
95
+ // Verify summary inserted
96
+ const hasSummary = result.messages.some(m =>
97
+ m.content.some(block =>
98
+ block.type === "text" &&
99
+ (block as any).text?.includes("compacted")
100
+ )
101
+ );
102
+ expect(hasSummary).toBe(true);
103
+ });
104
+
105
+ it("handles empty messages gracefully", async () => {
106
+ const messages: Message[] = [];
107
+
108
+ const result = await compactMessages(messages, 1000);
109
+
110
+ expect(result.messages).toEqual([]);
111
+ expect(result.didCompact).toBe(false);
112
+ });
113
+
114
+ it("returns unchanged if under token limit", async () => {
115
+ const messages = [
116
+ userMessage("Hello"),
117
+ assistantMessage("Hi!")
118
+ ];
119
+
120
+ const result = await compactMessages(messages, 100000);
121
+
122
+ expect(result.didCompact).toBe(false);
123
+ expect(result.messages).toEqual(messages);
124
+ });
125
+
126
+ it("preserves tool use and tool result pairs", async () => {
127
+ const messages: Message[] = [
128
+ userMessage("Read the file"),
129
+ {
130
+ role: "assistant",
131
+ content: [
132
+ textBlock("I'll read the file."),
133
+ { type: "tool_use", id: "tool-1", name: "Read", input: { file_path: "/test.txt" } }
134
+ ]
135
+ },
136
+ {
137
+ role: "user",
138
+ content: [
139
+ { type: "tool_result", tool_use_id: "tool-1", content: "File contents here" }
140
+ ]
141
+ },
142
+ assistantMessage("The file contains test data.")
143
+ ];
144
+
145
+ const result = await compactMessages(messages, 500, {
146
+ keepFirst: 1,
147
+ keepLast: 1,
148
+ preserveToolPairs: true,
149
+ useLLMSummarization: false
150
+ });
151
+
152
+ // Should have compacted with tool pairs preserved
153
+ expect(result.messages.length).toBeGreaterThan(0);
154
+ });
155
+ });
156
+
157
+ describe("getCompactionStats", () => {
158
+ it("returns zero stats for no compaction", () => {
159
+ const result = {
160
+ messages: [],
161
+ messagesRemoved: 0,
162
+ tokensBefore: 100,
163
+ tokensAfter: 100,
164
+ didCompact: false
165
+ };
166
+
167
+ const stats = getCompactionStats(result);
168
+
169
+ expect(stats.reductionPercent).toBe(0);
170
+ expect(stats.tokensSaved).toBe(0);
171
+ });
172
+
173
+ it("calculates correct stats after compaction", () => {
174
+ const result = {
175
+ messages: [],
176
+ messagesRemoved: 50,
177
+ tokensBefore: 1000,
178
+ tokensAfter: 300,
179
+ didCompact: true
180
+ };
181
+
182
+ const stats = getCompactionStats(result);
183
+
184
+ expect(stats.tokensSaved).toBe(700);
185
+ expect(stats.reductionPercent).toBeCloseTo(70, 0);
186
+ });
187
+ });
188
+
189
+ describe("LoopState integration", () => {
190
+ it("LoopState.applyCompaction works with compactMessages result", async () => {
191
+ const initialMessages: Message[] = [];
192
+ for (let i = 0; i < 30; i++) {
193
+ initialMessages.push(userMessage(`Message ${i}: ` + "x".repeat(100)));
194
+ initialMessages.push(assistantMessage(`Response ${i}: ` + "y".repeat(100)));
195
+ }
196
+
197
+ const loopState = new LoopState(initialMessages);
198
+
199
+ // Compact
200
+ const compactionResult = await compactMessages(loopState.messages, 5000, {
201
+ keepFirst: 2,
202
+ keepLast: 4,
203
+ useLLMSummarization: false
204
+ });
205
+
206
+ // Apply to state
207
+ const applied = loopState.applyCompaction(compactionResult, getCompactionStats);
208
+
209
+ if (compactionResult.didCompact) {
210
+ expect(applied).toBe(true);
211
+ expect(loopState.messages.length).toBeLessThan(initialMessages.length);
212
+ expect(loopState.compactionCount).toBe(1);
213
+ expect(loopState.totalTokensCompacted).toBeGreaterThan(0);
214
+ }
215
+ });
216
+ });
217
+
218
+ describe("extraction and summarization integration", () => {
219
+ it("extracts text correctly from mixed content blocks", async () => {
220
+ const messages: Message[] = [
221
+ {
222
+ role: "user",
223
+ content: [
224
+ textBlock("Here's my question:"),
225
+ { type: "image", source: { type: "base64", media_type: "image/png", data: "abc123" } } as ContentBlock,
226
+ textBlock("Please help with this.")
227
+ ]
228
+ },
229
+ assistantMessage("I'll help you with that.")
230
+ ];
231
+
232
+ const result = await compactMessages(messages, 500, {
233
+ useLLMSummarization: false
234
+ });
235
+
236
+ // Text should be handled, image preserved or summarized
237
+ expect(result.messages.length).toBeGreaterThan(0);
238
+ });
239
+
240
+ it("summarizes long conversations effectively", async () => {
241
+ const messages: Message[] = [
242
+ userMessage("I need to build a REST API"),
243
+ assistantMessage("I'll help you build a REST API. What framework?"),
244
+ userMessage("Express.js"),
245
+ assistantMessage("Great choice. Let's set up routes.")
246
+ ];
247
+
248
+ // Add more conversation to ensure we exceeds token limit
249
+ for (let i = 0; i < 30; i++) {
250
+ messages.push(userMessage(`Question about route ${i}: ` + "test ".repeat(100)));
251
+ messages.push(assistantMessage(`Answer about route ${i}: ` + "response ".repeat(100)));
252
+ }
253
+
254
+ // Use a very low token limit to force compaction
255
+ const result = await compactMessages(messages, 100, {
256
+ keepFirst: 2,
257
+ keepLast: 4,
258
+ useLLMSummarization: false
259
+ });
260
+
261
+ // Should have summary of earlier conversation
262
+ expect(result.didCompact).toBe(true);
263
+ expect(result.messages.length).toBeLessThan(messages.length);
264
+ expect(result.messagesRemoved).toBeGreaterThan(0);
265
+ });
266
+ });
267
+
268
+ describe("performance with large contexts", () => {
269
+ it("handles 100+ messages efficiently", async () => {
270
+ const messages: Message[] = [];
271
+
272
+ // Add 100 messages
273
+ for (let i = 0; i < 100; i++) {
274
+ messages.push(userMessage(`User ${i}: ` + "test ".repeat(20)));
275
+ messages.push(assistantMessage(`Assistant ${i}: ` + "response ".repeat(20)));
276
+ }
277
+
278
+ const startTime = Date.now();
279
+
280
+ const result = await compactMessages(messages, 5000, {
281
+ keepFirst: 2,
282
+ keepLast: 4,
283
+ useLLMSummarization: false
284
+ });
285
+
286
+ const duration = Date.now() - startTime;
287
+
288
+ // Should complete in reasonable time (< 5s for integration test)
289
+ expect(duration).toBeLessThan(5000);
290
+ expect(result.didCompact).toBe(true);
291
+ expect(result.messages.length).toBeLessThan(200);
292
+ });
293
+ });
294
+
295
+ describe("error handling and edge cases", () => {
296
+ it("handles messages with empty content", async () => {
297
+ const messages: Message[] = [
298
+ { role: "user", content: [] },
299
+ { role: "assistant", content: [] }
300
+ ];
301
+
302
+ const result = await compactMessages(messages, 1000);
303
+
304
+ expect(result.messages).toBeDefined();
305
+ expect(result.didCompact).toBe(false);
306
+ });
307
+
308
+ it("handles malformed content blocks gracefully", async () => {
309
+ const messages: Message[] = [
310
+ {
311
+ role: "user",
312
+ content: [
313
+ { type: "unknown" } as unknown as ContentBlock,
314
+ textBlock("Valid text")
315
+ ]
316
+ }
317
+ ];
318
+
319
+ const result = await compactMessages(messages, 1000);
320
+
321
+ expect(result.messages.length).toBeGreaterThan(0);
322
+ });
323
+
324
+ it("handles very large single messages", async () => {
325
+ const hugeText = "x".repeat(100000);
326
+ const messages = [userMessage(hugeText)];
327
+
328
+ const result = await compactMessages(messages, 1000);
329
+
330
+ // Should return as-is (not enough messages to compact)
331
+ expect(result.messages.length).toBe(1);
332
+ });
333
+ });
334
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Compaction - Reduce message context size while preserving important information
3
+ *
4
+ * Strategy:
5
+ * 1. Always keep the first N messages (original query)
6
+ * 2. Always keep the last M messages (recent context)
7
+ * 3. Summarize middle messages into a single "context summary" user message
8
+ * 4. Preserve tool_use/tool_result pairs when possible
9
+ */
10
+
11
+ import type { Message, ContentBlock } from "../../types/index.js";
12
+ import type { CompactionOptions, CompactionResult, CompactionStats } from "./types.js";
13
+ import {
14
+ DEFAULT_KEEP_FIRST,
15
+ DEFAULT_KEEP_LAST,
16
+ DEFAULT_COMPACTION_THRESHOLD,
17
+ MIN_MESSAGES_FOR_COMPACTION,
18
+ } from "./constants.js";
19
+ import { estimateMessagesTokens } from "./token-estimation.js";
20
+ import { extractToolPairs } from "./extraction.js";
21
+ import { summarizeMessages, summarizeWithLLM } from "./summarization.js";
22
+
23
+ /**
24
+ * Compact messages to fit within a token limit.
25
+ */
26
+ export async function compactMessages(
27
+ messages: Message[],
28
+ maxTokens: number,
29
+ options: CompactionOptions = {}
30
+ ): Promise<CompactionResult> {
31
+ const {
32
+ keepFirst = DEFAULT_KEEP_FIRST,
33
+ keepLast = DEFAULT_KEEP_LAST,
34
+ preserveToolPairs = true,
35
+ useLLMSummarization = true,
36
+ apiKey,
37
+ baseUrl,
38
+ } = options;
39
+
40
+ const tokensBefore = estimateMessagesTokens(messages);
41
+
42
+ // If already under limit, no compaction needed
43
+ if (tokensBefore <= maxTokens) {
44
+ return {
45
+ messages,
46
+ messagesRemoved: 0,
47
+ tokensBefore,
48
+ tokensAfter: tokensBefore,
49
+ didCompact: false,
50
+ };
51
+ }
52
+
53
+ // Not enough messages to compact - silent return
54
+ if (messages.length <= keepFirst + keepLast) {
55
+ return {
56
+ messages,
57
+ messagesRemoved: 0,
58
+ tokensBefore,
59
+ tokensAfter: tokensBefore,
60
+ didCompact: false,
61
+ };
62
+ }
63
+
64
+ // Extract segments
65
+ const firstMessages = messages.slice(0, keepFirst);
66
+ const middleMessages = messages.slice(keepFirst, -keepLast);
67
+ const lastMessages = messages.slice(-keepLast);
68
+
69
+ // Create summary of middle messages (use LLM if available, fallback to simple)
70
+ const summary = useLLMSummarization
71
+ ? await summarizeWithLLM(middleMessages, { apiKey, baseUrl })
72
+ : await summarizeMessages(middleMessages);
73
+
74
+ // Build summary message
75
+ const summaryMessage: Message = {
76
+ role: "user",
77
+ content: [{
78
+ type: "text",
79
+ text: `[Previous context has been compacted for continuity]\n\n${summary}`,
80
+ }],
81
+ };
82
+
83
+ // Optionally preserve important tool pairs
84
+ let preservedBlocks: ContentBlock[] = [];
85
+ if (preserveToolPairs && middleMessages.length > 0) {
86
+ const toolPairs = extractToolPairs(middleMessages);
87
+
88
+ // Keep the most recent tool use/result pairs (up to 3)
89
+ const recentPairs = Array.from(toolPairs.values())
90
+ .slice(-3)
91
+ .filter(pair => pair.result && !pair.result.is_error);
92
+
93
+ for (const pair of recentPairs) {
94
+ preservedBlocks.push(pair.use as ContentBlock);
95
+ if (pair.result) {
96
+ preservedBlocks.push(pair.result as ContentBlock);
97
+ }
98
+ }
99
+ }
100
+
101
+ // Build compacted message list
102
+ const compacted: Message[] = [
103
+ ...firstMessages,
104
+ summaryMessage,
105
+ ];
106
+
107
+ // Add preserved tool results if any
108
+ if (preservedBlocks.length > 0) {
109
+ compacted.push({
110
+ role: "assistant",
111
+ content: preservedBlocks.filter(b => b.type === "tool_use"),
112
+ });
113
+ compacted.push({
114
+ role: "user",
115
+ content: preservedBlocks.filter(b => b.type === "tool_result"),
116
+ });
117
+ }
118
+
119
+ // Add recent messages
120
+ compacted.push(...lastMessages);
121
+
122
+ const tokensAfter = estimateMessagesTokens(compacted);
123
+ const messagesRemoved = messages.length - compacted.length;
124
+
125
+ console.log(`Context compaction: ${messages.length} -> ${compacted.length} messages, ${tokensBefore} -> ${tokensAfter} tokens`);
126
+
127
+ return {
128
+ messages: compacted,
129
+ messagesRemoved,
130
+ tokensBefore,
131
+ tokensAfter,
132
+ didCompact: true,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Check if compaction is needed proactively.
138
+ * Returns true if current token usage exceeds the threshold AND there are enough messages to compact.
139
+ */
140
+ export function needsCompaction(
141
+ messages: Message[],
142
+ maxTokens: number,
143
+ threshold: number = DEFAULT_COMPACTION_THRESHOLD
144
+ ): boolean {
145
+ // Not enough messages to meaningfully compact
146
+ if (messages.length < MIN_MESSAGES_FOR_COMPACTION) {
147
+ return false;
148
+ }
149
+
150
+ const currentTokens = estimateMessagesTokens(messages);
151
+ const thresholdTokens = Math.floor(maxTokens * threshold);
152
+ return currentTokens >= thresholdTokens;
153
+ }
154
+
155
+ /**
156
+ * Get compaction statistics for logging/metrics
157
+ */
158
+ export function getCompactionStats(result: CompactionResult): CompactionStats {
159
+ if (!result.didCompact) {
160
+ return { reductionPercent: 0, tokensSaved: 0 };
161
+ }
162
+
163
+ const tokensSaved = result.tokensBefore - result.tokensAfter;
164
+ const reductionPercent = (tokensSaved / result.tokensBefore) * 100;
165
+
166
+ return {
167
+ reductionPercent: Math.round(reductionPercent * 100) / 100,
168
+ tokensSaved,
169
+ };
170
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Context Constants - Configuration values for context compaction
3
+ */
4
+
5
+ /** Approximate characters per token (rough estimate for Claude models) */
6
+ export const CHARS_PER_TOKEN = 4;
7
+
8
+ /** Default number of recent messages to keep during compaction */
9
+ export const DEFAULT_KEEP_LAST = 5;
10
+
11
+ /** Default number of initial messages to keep (usually just the first user query) */
12
+ export const DEFAULT_KEEP_FIRST = 1;
13
+
14
+ /** Minimum messages required before compaction is possible */
15
+ export const MIN_MESSAGES_FOR_COMPACTION = 8;
16
+
17
+ /** Default threshold for proactive compaction (90% of max tokens) */
18
+ export const DEFAULT_COMPACTION_THRESHOLD = 0.9;
19
+
20
+ /** Maximum length for summary text before truncation */
21
+ export const MAX_SUMMARY_LENGTH = 8000;
22
+
23
+ /** Maximum tokens for summary output */
24
+ export const SUMMARY_MAX_TOKENS = 2000;
25
+
26
+ /** System prompt for summarization */
27
+ export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarizer. Your job is to create concise, information-dense summaries of conversation history.
28
+
29
+ Guidelines:
30
+ - Preserve all important decisions, file changes, and key information
31
+ - Keep track of what tools were used and their outcomes
32
+ - Note any errors encountered and how they were resolved
33
+ - Maintain chronological flow
34
+ - Be extremely concise - use bullet points and short sentences
35
+ - Focus on information that would be needed to continue the conversation
36
+ - Do not include pleasantries or filler text
37
+
38
+ Format your summary as:
39
+ ## Summary
40
+ [Brief overview of what was discussed]
41
+
42
+ ## Key Actions
43
+ - [Action 1]
44
+ - [Action 2]
45
+
46
+ ## Files Modified
47
+ - [file]: [what changed]
48
+
49
+ ## Important Context
50
+ [Any critical information needed going forward]`;
51
+
52
+ /** User prompt template for summarization */
53
+ export const SUMMARIZATION_PROMPT = `Summarize the following conversation messages for context compaction. Preserve all important information in a concise format.
54
+
55
+ Messages to summarize:
56
+ {{MESSAGES}}
57
+
58
+ Provide a dense, information-rich summary that captures everything needed to continue this conversation.`;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Content Extraction - Extract and organize message content
3
+ */
4
+
5
+ import type { Message, ContentBlock } from "../../types/index.js";
6
+ import type { ToolPair } from "./types.js";
7
+
8
+ /**
9
+ * Extract text content from a message for summarization
10
+ */
11
+ export function extractTextFromMessage(message: Message): string {
12
+ const parts: string[] = [];
13
+
14
+ for (const block of message.content) {
15
+ switch (block.type) {
16
+ case "text":
17
+ parts.push(block.text);
18
+ break;
19
+ case "tool_use":
20
+ parts.push(`[Tool: ${block.name}(${JSON.stringify(block.input)})]`);
21
+ break;
22
+ case "tool_result":
23
+ const content = typeof block.content === "string"
24
+ ? block.content
25
+ : block.content.map(b => b.type === "text" ? b.text : "[content]").join("");
26
+ parts.push(`[Result: ${content.slice(0, 500)}${content.length > 500 ? "..." : ""}]`);
27
+ break;
28
+ case "thinking":
29
+ parts.push(`[Thinking: ${block.thinking.slice(0, 200)}...]`);
30
+ break;
31
+ }
32
+ }
33
+
34
+ return parts.join("\n");
35
+ }
36
+
37
+ /**
38
+ * Extract tool use/result pairs from messages for preservation
39
+ */
40
+ export function extractToolPairs(messages: Message[]): Map<string, ToolPair> {
41
+ const toolPairs = new Map<string, ToolPair>();
42
+
43
+ // First pass: collect all tool uses
44
+ for (const message of messages) {
45
+ for (const block of message.content) {
46
+ if (block.type === "tool_use") {
47
+ toolPairs.set(block.id, {
48
+ use: { type: "tool_use", id: block.id, name: block.name, input: block.input }
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ // Second pass: match results to uses
55
+ for (const message of messages) {
56
+ for (const block of message.content) {
57
+ if (block.type === "tool_result") {
58
+ const pair = toolPairs.get(block.tool_use_id);
59
+ if (pair) {
60
+ pair.result = {
61
+ type: "tool_result",
62
+ tool_use_id: block.tool_use_id,
63
+ content: block.content,
64
+ is_error: block.is_error
65
+ };
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ return toolPairs;
72
+ }
73
+
74
+ /**
75
+ * Extract tool names from a message
76
+ */
77
+ export function extractToolNames(message: Message): string[] {
78
+ const names: string[] = [];
79
+ for (const block of message.content) {
80
+ if (block.type === "tool_use") {
81
+ names.push(block.name);
82
+ }
83
+ }
84
+ return names;
85
+ }