gencode-ai 0.3.0 → 0.4.0

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 (116) hide show
  1. package/RELEASE_NOTES_v0.4.0.md +140 -0
  2. package/dist/agent/agent.d.ts +17 -2
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +279 -49
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +15 -1
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/checkpointing/checkpoint-manager.d.ts +24 -0
  9. package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -1
  10. package/dist/checkpointing/checkpoint-manager.js +28 -0
  11. package/dist/checkpointing/checkpoint-manager.js.map +1 -1
  12. package/dist/cli/components/App.d.ts +8 -0
  13. package/dist/cli/components/App.d.ts.map +1 -1
  14. package/dist/cli/components/App.js +478 -36
  15. package/dist/cli/components/App.js.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  17. package/dist/cli/components/CommandSuggestions.js +2 -0
  18. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  19. package/dist/cli/components/Header.d.ts +6 -1
  20. package/dist/cli/components/Header.d.ts.map +1 -1
  21. package/dist/cli/components/Header.js +3 -3
  22. package/dist/cli/components/Header.js.map +1 -1
  23. package/dist/cli/components/Messages.d.ts.map +1 -1
  24. package/dist/cli/components/Messages.js +7 -9
  25. package/dist/cli/components/Messages.js.map +1 -1
  26. package/dist/cli/index.js +3 -2
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/config/types.d.ts +20 -1
  29. package/dist/config/types.d.ts.map +1 -1
  30. package/dist/config/types.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +2 -2
  33. package/dist/input/history-manager.d.ts +78 -0
  34. package/dist/input/history-manager.d.ts.map +1 -0
  35. package/dist/input/history-manager.js +224 -0
  36. package/dist/input/history-manager.js.map +1 -0
  37. package/dist/input/index.d.ts +6 -0
  38. package/dist/input/index.d.ts.map +1 -0
  39. package/dist/input/index.js +5 -0
  40. package/dist/input/index.js.map +1 -0
  41. package/dist/prompts/index.js +3 -3
  42. package/dist/prompts/index.js.map +1 -1
  43. package/dist/providers/gemini.d.ts.map +1 -1
  44. package/dist/providers/gemini.js +33 -2
  45. package/dist/providers/gemini.js.map +1 -1
  46. package/dist/providers/google.d.ts +22 -0
  47. package/dist/providers/google.d.ts.map +1 -0
  48. package/dist/providers/google.js +297 -0
  49. package/dist/providers/google.js.map +1 -0
  50. package/dist/providers/index.d.ts +4 -4
  51. package/dist/providers/index.js +11 -11
  52. package/dist/providers/index.js.map +1 -1
  53. package/dist/providers/openai.d.ts.map +1 -1
  54. package/dist/providers/openai.js +6 -0
  55. package/dist/providers/openai.js.map +1 -1
  56. package/dist/providers/registry.js +3 -3
  57. package/dist/providers/registry.js.map +1 -1
  58. package/dist/providers/types.d.ts +30 -4
  59. package/dist/providers/types.d.ts.map +1 -1
  60. package/dist/session/compression/engine.d.ts +109 -0
  61. package/dist/session/compression/engine.d.ts.map +1 -0
  62. package/dist/session/compression/engine.js +311 -0
  63. package/dist/session/compression/engine.js.map +1 -0
  64. package/dist/session/compression/index.d.ts +12 -0
  65. package/dist/session/compression/index.d.ts.map +1 -0
  66. package/dist/session/compression/index.js +11 -0
  67. package/dist/session/compression/index.js.map +1 -0
  68. package/dist/session/compression/types.d.ts +90 -0
  69. package/dist/session/compression/types.d.ts.map +1 -0
  70. package/dist/session/compression/types.js +17 -0
  71. package/dist/session/compression/types.js.map +1 -0
  72. package/dist/session/manager.d.ts +64 -3
  73. package/dist/session/manager.d.ts.map +1 -1
  74. package/dist/session/manager.js +254 -2
  75. package/dist/session/manager.js.map +1 -1
  76. package/dist/session/types.d.ts +16 -0
  77. package/dist/session/types.d.ts.map +1 -1
  78. package/dist/session/types.js.map +1 -1
  79. package/docs/README.md +1 -0
  80. package/docs/diagrams/compression-decision.mmd +30 -0
  81. package/docs/diagrams/compression-workflow.mmd +54 -0
  82. package/docs/diagrams/layer1-pruning.mmd +45 -0
  83. package/docs/diagrams/layer2-compaction.mmd +42 -0
  84. package/docs/proposals/0007-context-management.md +252 -2
  85. package/docs/proposals/README.md +4 -3
  86. package/docs/providers.md +3 -3
  87. package/docs/session-compression.md +695 -0
  88. package/examples/agent-demo.ts +23 -1
  89. package/examples/basic.ts +3 -3
  90. package/package.json +3 -4
  91. package/src/agent/agent.ts +314 -52
  92. package/src/agent/types.ts +19 -1
  93. package/src/checkpointing/checkpoint-manager.ts +48 -0
  94. package/src/cli/components/App.tsx +553 -34
  95. package/src/cli/components/CommandSuggestions.tsx +2 -0
  96. package/src/cli/components/Header.tsx +16 -1
  97. package/src/cli/components/Messages.tsx +20 -14
  98. package/src/cli/index.tsx +3 -2
  99. package/src/config/types.ts +26 -1
  100. package/src/index.ts +3 -3
  101. package/src/input/history-manager.ts +289 -0
  102. package/src/input/index.ts +6 -0
  103. package/src/prompts/index.test.ts +2 -1
  104. package/src/prompts/index.ts +3 -3
  105. package/src/providers/{gemini.ts → google.ts} +69 -18
  106. package/src/providers/index.ts +14 -14
  107. package/src/providers/openai.ts +7 -0
  108. package/src/providers/registry.ts +3 -3
  109. package/src/providers/types.ts +33 -3
  110. package/src/session/compression/engine.ts +406 -0
  111. package/src/session/compression/index.ts +18 -0
  112. package/src/session/compression/types.ts +102 -0
  113. package/src/session/manager.ts +326 -3
  114. package/src/session/types.ts +21 -0
  115. package/tests/input-history-manager.test.ts +335 -0
  116. package/tests/session-checkpoint-persistence.test.ts +198 -0
@@ -5,7 +5,7 @@
5
5
  export * from './types.js';
6
6
  export { OpenAIProvider } from './openai.js';
7
7
  export { AnthropicProvider } from './anthropic.js';
8
- export { GeminiProvider } from './gemini.js';
8
+ export { GoogleProvider } from './google.js';
9
9
  export { AnthropicVertexProvider } from './vertex-ai.js';
10
10
 
11
11
  import type {
@@ -14,12 +14,12 @@ import type {
14
14
  AuthMethod,
15
15
  OpenAIConfig,
16
16
  AnthropicConfig,
17
- GeminiConfig,
17
+ GoogleConfig,
18
18
  VertexAIConfig,
19
19
  } from './types.js';
20
20
  import { OpenAIProvider } from './openai.js';
21
21
  import { AnthropicProvider } from './anthropic.js';
22
- import { GeminiProvider } from './gemini.js';
22
+ import { GoogleProvider } from './google.js';
23
23
  import { AnthropicVertexProvider } from './vertex-ai.js';
24
24
 
25
25
  // Legacy type alias for backward compatibility
@@ -29,13 +29,13 @@ export type ProviderName = Provider;
29
29
  export type ProviderConfigMap = {
30
30
  openai: OpenAIConfig;
31
31
  anthropic: AnthropicConfig;
32
- gemini: GeminiConfig;
32
+ google: GoogleConfig;
33
33
  };
34
34
 
35
35
  export interface CreateProviderOptions {
36
36
  provider: Provider;
37
37
  authMethod?: AuthMethod;
38
- config?: OpenAIConfig | AnthropicConfig | GeminiConfig | VertexAIConfig;
38
+ config?: OpenAIConfig | AnthropicConfig | GoogleConfig | VertexAIConfig;
39
39
  }
40
40
 
41
41
  /**
@@ -62,11 +62,11 @@ export function createProvider(options: CreateProviderOptions): LLMProvider {
62
62
  throw new Error(`Unsupported auth method for openai: ${authMethod}`);
63
63
  }
64
64
 
65
- if (provider === 'gemini') {
65
+ if (provider === 'google') {
66
66
  if (authMethod === 'api_key') {
67
- return new GeminiProvider(config as GeminiConfig);
67
+ return new GoogleProvider(config as GoogleConfig);
68
68
  }
69
- throw new Error(`Unsupported auth method for gemini: ${authMethod}`);
69
+ throw new Error(`Unsupported auth method for google: ${authMethod}`);
70
70
  }
71
71
 
72
72
  throw new Error(`Unknown provider: ${provider}`);
@@ -97,9 +97,9 @@ export function inferProvider(model: string): Provider {
97
97
  return 'anthropic';
98
98
  }
99
99
 
100
- // Gemini models
100
+ // Google models (Gemini)
101
101
  if (modelLower.includes('gemini') || modelLower.includes('palm')) {
102
- return 'gemini';
102
+ return 'google';
103
103
  }
104
104
 
105
105
  // Default to OpenAI (most common)
@@ -143,10 +143,10 @@ export const ModelAliases: Record<
143
143
  'claude-haiku': { provider: 'anthropic', model: 'claude-haiku-4-20250514' },
144
144
  'claude-3.5-sonnet': { provider: 'anthropic', model: 'claude-3-5-sonnet-20241022' },
145
145
 
146
- // Gemini
147
- 'gemini-2.0-flash': { provider: 'gemini', model: 'gemini-2.0-flash' },
148
- 'gemini-1.5-pro': { provider: 'gemini', model: 'gemini-1.5-pro' },
149
- 'gemini-1.5-flash': { provider: 'gemini', model: 'gemini-1.5-flash' },
146
+ // Google (Gemini models)
147
+ 'gemini-2.0-flash': { provider: 'google', model: 'gemini-2.0-flash' },
148
+ 'gemini-1.5-pro': { provider: 'google', model: 'gemini-1.5-pro' },
149
+ 'gemini-1.5-flash': { provider: 'google', model: 'gemini-1.5-flash' },
150
150
 
151
151
  // Anthropic via Vertex AI
152
152
  'vertex-sonnet': { provider: 'anthropic', authMethod: 'vertex', model: 'claude-sonnet-4-5@20250929' },
@@ -247,13 +247,20 @@ export class OpenAIProvider implements LLMProvider {
247
247
  }
248
248
 
249
249
  private convertStopReason(reason: string | null): StopReason {
250
+ if (!reason) {
251
+ console.warn('[OpenAI] Warning: Received null finish_reason, defaulting to end_turn');
252
+ return 'end_turn';
253
+ }
254
+
250
255
  switch (reason) {
251
256
  case 'tool_calls':
252
257
  return 'tool_use';
253
258
  case 'length':
254
259
  return 'max_tokens';
255
260
  case 'stop':
261
+ return 'end_turn';
256
262
  default:
263
+ console.warn(`[OpenAI] Unknown finish_reason: ${reason}, defaulting to end_turn`);
257
264
  return 'end_turn';
258
265
  }
259
266
  }
@@ -7,7 +7,7 @@ import type { SearchProviderName } from './search/types.js';
7
7
  import { AnthropicProvider } from './anthropic.js';
8
8
  import { AnthropicVertexProvider } from './vertex-ai.js';
9
9
  import { OpenAIProvider } from './openai.js';
10
- import { GeminiProvider } from './gemini.js';
10
+ import { GoogleProvider } from './google.js';
11
11
 
12
12
  // ============================================================================
13
13
  // LLM Provider Classes
@@ -26,7 +26,7 @@ export const PROVIDER_CLASSES: ProviderClass[] = [
26
26
  AnthropicProvider,
27
27
  AnthropicVertexProvider,
28
28
  OpenAIProvider,
29
- GeminiProvider,
29
+ GoogleProvider,
30
30
  ];
31
31
 
32
32
  /**
@@ -41,7 +41,7 @@ export interface ProviderMeta {
41
41
  export const PROVIDER_METADATA: ProviderMeta[] = [
42
42
  { id: 'anthropic', name: 'Anthropic', popularity: 1 },
43
43
  { id: 'openai', name: 'OpenAI', popularity: 2 },
44
- { id: 'gemini', name: 'Google Gemini', popularity: 3 },
44
+ { id: 'google', name: 'Google', popularity: 3 },
45
45
  ];
46
46
 
47
47
  // ============================================================================
@@ -12,7 +12,7 @@ import type { CostEstimate } from '../pricing/types.js';
12
12
  /**
13
13
  * Provider - Semantic layer (only 3 providers)
14
14
  */
15
- export type Provider = 'anthropic' | 'openai' | 'gemini';
15
+ export type Provider = 'anthropic' | 'openai' | 'google';
16
16
 
17
17
  /**
18
18
  * Authentication method for providers
@@ -113,6 +113,7 @@ export interface CompletionOptions {
113
113
  maxTokens?: number;
114
114
  temperature?: number;
115
115
  stream?: boolean;
116
+ signal?: AbortSignal; // For cancellation support
116
117
  }
117
118
 
118
119
  export type StopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence';
@@ -123,6 +124,15 @@ export interface CompletionResponse {
123
124
  usage?: {
124
125
  inputTokens: number;
125
126
  outputTokens: number;
127
+ /** Reasoning tokens (for thinking models like o1/o3) */
128
+ reasoningTokens?: number;
129
+ /** Cache statistics (Anthropic prompt caching) */
130
+ cache?: {
131
+ /** Cache hit tokens (~10% cost) */
132
+ readTokens: number;
133
+ /** Cache creation tokens */
134
+ writeTokens: number;
135
+ };
126
136
  };
127
137
  cost?: CostEstimate;
128
138
  }
@@ -150,6 +160,11 @@ export interface StreamChunkToolInput {
150
160
  input: string; // Partial JSON string
151
161
  }
152
162
 
163
+ export interface StreamChunkReasoning {
164
+ type: 'reasoning';
165
+ text: string; // Reasoning content from o1/o3/Gemini 3+ models
166
+ }
167
+
153
168
  export interface StreamChunkDone {
154
169
  type: 'done';
155
170
  response: CompletionResponse;
@@ -164,6 +179,7 @@ export type StreamChunk =
164
179
  | StreamChunkText
165
180
  | StreamChunkToolStart
166
181
  | StreamChunkToolInput
182
+ | StreamChunkReasoning
167
183
  | StreamChunkDone
168
184
  | StreamChunkError;
169
185
 
@@ -175,6 +191,10 @@ export interface ModelInfo {
175
191
  id: string;
176
192
  name: string;
177
193
  description?: string;
194
+ /** Model context window size (for compression decisions) */
195
+ contextWindow?: number;
196
+ /** Model output limit (for compression decisions) */
197
+ outputLimit?: number;
178
198
  }
179
199
 
180
200
  export interface LLMProvider {
@@ -194,6 +214,16 @@ export interface LLMProvider {
194
214
  * List available models from the provider
195
215
  */
196
216
  listModels(): Promise<ModelInfo[]>;
217
+
218
+ /**
219
+ * Get current model name (optional - for backward compatibility)
220
+ */
221
+ getModel?(): string;
222
+
223
+ /**
224
+ * Get model information (for compression decisions)
225
+ */
226
+ getModelInfo?(model: string): ModelInfo;
197
227
  }
198
228
 
199
229
  // ============================================================================
@@ -211,7 +241,7 @@ export interface AnthropicConfig {
211
241
  baseURL?: string;
212
242
  }
213
243
 
214
- export interface GeminiConfig {
244
+ export interface GoogleConfig {
215
245
  apiKey?: string;
216
246
  }
217
247
 
@@ -221,4 +251,4 @@ export interface VertexAIConfig {
221
251
  accessToken?: string;
222
252
  }
223
253
 
224
- export type ProviderConfig = OpenAIConfig | AnthropicConfig | GeminiConfig | VertexAIConfig;
254
+ export type ProviderConfig = OpenAIConfig | AnthropicConfig | GoogleConfig | VertexAIConfig;
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Three-layer compression engine (inspired by OpenCode)
3
+ * Layer 1: Tool output pruning (fast, no cost)
4
+ * Layer 2: Compaction summarization (LLM-based, medium cost)
5
+ * Layer 3: Message filtering (recovery optimization)
6
+ */
7
+
8
+ import type { Message, MessageContent } from '../../providers/types.js';
9
+ import type {
10
+ CompressionConfig,
11
+ ConversationSummary,
12
+ ToolUsageSummary,
13
+ TokenUsage,
14
+ ModelInfo,
15
+ } from './types.js';
16
+ import { DEFAULT_COMPRESSION_CONFIG } from './types.js';
17
+
18
+ // Provider interface (minimal subset needed for compression)
19
+ interface LLMProvider {
20
+ complete(options: {
21
+ model: string;
22
+ messages: Message[];
23
+ maxTokens?: number;
24
+ }): Promise<{ content: string | MessageContent[] }>;
25
+ getModel?(): string;
26
+ }
27
+
28
+ /**
29
+ * Compression engine implementing OpenCode's three-layer strategy
30
+ */
31
+ export class CompressionEngine {
32
+ private provider: LLMProvider;
33
+ private config: CompressionConfig;
34
+
35
+ // OpenCode constants
36
+ private readonly CHARS_PER_TOKEN = 4;
37
+
38
+ constructor(provider: LLMProvider, config?: Partial<CompressionConfig>) {
39
+ this.provider = provider;
40
+ this.config = { ...DEFAULT_COMPRESSION_CONFIG, ...config };
41
+ }
42
+
43
+ /**
44
+ * Estimate tokens using 4:1 character-to-token ratio (OpenCode approach)
45
+ */
46
+ estimateTokens(text: string): number {
47
+ return Math.max(0, Math.round((text || '').length / this.CHARS_PER_TOKEN));
48
+ }
49
+
50
+ /**
51
+ * Calculate total tokens from messages
52
+ */
53
+ calculateTotalTokens(messages: Message[], tokenUsage?: TokenUsage): number {
54
+ // If we have actual token usage data, use it
55
+ if (tokenUsage) {
56
+ return (
57
+ tokenUsage.input +
58
+ (tokenUsage.cache?.read || 0) +
59
+ tokenUsage.output +
60
+ (tokenUsage.reasoning || 0)
61
+ );
62
+ }
63
+
64
+ // Otherwise estimate
65
+ let total = 0;
66
+ for (const msg of messages) {
67
+ const content =
68
+ typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
69
+ total += this.estimateTokens(content);
70
+ }
71
+ return total;
72
+ }
73
+
74
+ /**
75
+ * Calculate usable context space (OpenCode logic)
76
+ */
77
+ getUsableContext(model: ModelInfo): number {
78
+ const maxOutput = Math.min(
79
+ model.outputLimit || 4096,
80
+ this.config.reservedOutputTokens
81
+ );
82
+ return model.contextWindow - maxOutput;
83
+ }
84
+
85
+ /**
86
+ * Check if compression is needed (OpenCode isOverflow logic)
87
+ */
88
+ needsCompression(
89
+ messages: Message[],
90
+ model: ModelInfo,
91
+ tokenUsage?: TokenUsage
92
+ ): {
93
+ needed: boolean;
94
+ strategy: 'prune' | 'compact' | 'none';
95
+ usagePercent?: number;
96
+ shouldWarn?: boolean;
97
+ } {
98
+ if (!this.config.enabled) {
99
+ return { needed: false, strategy: 'none' };
100
+ }
101
+
102
+ const totalTokens = this.calculateTotalTokens(messages, tokenUsage);
103
+ const usable = this.getUsableContext(model);
104
+ const usagePercent = (totalTokens / usable) * 100;
105
+
106
+ // Warn at 80% usage
107
+ const shouldWarn = usagePercent >= 80;
108
+
109
+ // Auto-compress at 90% usage or if exceeding usable space
110
+ const needed = totalTokens > usable || usagePercent >= 90;
111
+
112
+ if (!needed) {
113
+ return { needed: false, strategy: 'none', usagePercent, shouldWarn };
114
+ }
115
+
116
+ // Determine compression strategy
117
+ let strategy: 'prune' | 'compact' = 'compact';
118
+ if (totalTokens > this.config.pruneMinimum && this.config.enablePruning) {
119
+ strategy = 'prune';
120
+ }
121
+
122
+ return { needed: true, strategy, usagePercent, shouldWarn };
123
+ }
124
+
125
+ /**
126
+ * Layer 1: Tool output pruning (OpenCode pruning logic)
127
+ * Fast and cost-free - removes old tool results
128
+ */
129
+ async pruneToolOutputs(messages: Message[]): Promise<{
130
+ pruned: boolean;
131
+ prunedCount: number;
132
+ savedTokens: number;
133
+ }> {
134
+ const totalTokens = this.calculateTotalTokens(messages);
135
+ if (totalTokens < this.config.pruneMinimum) {
136
+ return { pruned: false, prunedCount: 0, savedTokens: 0 };
137
+ }
138
+
139
+ // Collect recent tool outputs (protect last 40k tokens)
140
+ let protectedTokens = 0;
141
+ const protectedIndices = new Set<number>();
142
+
143
+ for (
144
+ let i = messages.length - 1;
145
+ i >= 0 && protectedTokens < this.config.pruneProtect;
146
+ i--
147
+ ) {
148
+ const msg = messages[i];
149
+ if (this.hasToolResults(msg)) {
150
+ const msgTokens = this.calculateTotalTokens([msg]);
151
+ protectedTokens += msgTokens;
152
+ protectedIndices.add(i);
153
+ }
154
+ }
155
+
156
+ // Prune older tool outputs
157
+ let prunedCount = 0;
158
+ let savedTokens = 0;
159
+
160
+ for (let i = 0; i < messages.length; i++) {
161
+ if (!protectedIndices.has(i) && this.hasToolResults(messages[i])) {
162
+ const before = this.calculateTotalTokens([messages[i]]);
163
+ this.clearToolResults(messages[i]);
164
+ const after = this.calculateTotalTokens([messages[i]]);
165
+ savedTokens += before - after;
166
+ prunedCount++;
167
+ }
168
+ }
169
+
170
+ return { pruned: prunedCount > 0, prunedCount, savedTokens };
171
+ }
172
+
173
+ /**
174
+ * Layer 2: Compaction summarization (OpenCode compaction logic)
175
+ * Generates continuation prompt focusing on future context needs
176
+ */
177
+ async compact(
178
+ messages: Message[],
179
+ range: [number, number]
180
+ ): Promise<ConversationSummary> {
181
+ const toSummarize = messages.slice(range[0], range[1] + 1);
182
+
183
+ // Extract structured information
184
+ const filesModified = this.extractFilesModified(toSummarize);
185
+ const toolsUsed = this.extractToolUsage(toSummarize);
186
+ const keyDecisions = await this.extractKeyDecisions(toSummarize);
187
+
188
+ // Generate continuation prompt (OpenCode style)
189
+ const continuationPrompt = await this.generateContinuationPrompt(toSummarize);
190
+
191
+ return {
192
+ id: this.generateId(),
193
+ type: 'compaction',
194
+ coveringMessages: range,
195
+ content: continuationPrompt,
196
+ keyDecisions,
197
+ filesModified,
198
+ toolsUsed,
199
+ generatedAt: new Date().toISOString(),
200
+ estimatedTokens: this.estimateTokens(continuationPrompt),
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Generate continuation prompt (OpenCode style)
206
+ * Focus on what's needed to continue, not just what was done
207
+ */
208
+ private async generateContinuationPrompt(messages: Message[]): Promise<string> {
209
+ const prompt = `Provide a detailed prompt for continuing our conversation above.
210
+
211
+ Focus on information that would be helpful for continuing the conversation:
212
+ 1. What we accomplished so far
213
+ 2. What we're currently working on
214
+ 3. Which files we modified and key changes made
215
+ 4. What we plan to do next
216
+ 5. Any important context or decisions that would be needed
217
+
218
+ Remember: The new session will NOT have access to our full conversation history,
219
+ so include all essential context needed to continue working effectively.
220
+
221
+ Be technical and specific. Use structured bullet points.
222
+
223
+ Conversation:
224
+ ${this.formatMessagesForSummary(messages)}
225
+
226
+ Continuation Prompt:`;
227
+
228
+ const response = await this.provider.complete({
229
+ model: this.config.model ?? (this.provider.getModel?.() || 'unknown'),
230
+ messages: [{ role: 'user', content: prompt }],
231
+ maxTokens: 1500, // Larger for continuation prompt
232
+ });
233
+
234
+ return this.extractTextContent(response.content);
235
+ }
236
+
237
+ /**
238
+ * Extract files modified from tool uses
239
+ */
240
+ private extractFilesModified(messages: Message[]): string[] {
241
+ const files = new Set<string>();
242
+
243
+ for (const msg of messages) {
244
+ if (typeof msg.content !== 'string') {
245
+ for (const block of msg.content) {
246
+ if (block.type === 'tool_use') {
247
+ if (['Write', 'Edit'].includes(block.name)) {
248
+ const filePath = (block.input as any).file_path;
249
+ if (filePath) files.add(filePath);
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ return Array.from(files);
257
+ }
258
+
259
+ /**
260
+ * Extract tool usage statistics
261
+ */
262
+ private extractToolUsage(messages: Message[]): ToolUsageSummary[] {
263
+ const toolStats = new Map<string, { count: number; uses: string[] }>();
264
+
265
+ for (const msg of messages) {
266
+ if (typeof msg.content !== 'string') {
267
+ for (const block of msg.content) {
268
+ if (block.type === 'tool_use') {
269
+ const stats = toolStats.get(block.name) || { count: 0, uses: [] };
270
+ stats.count++;
271
+
272
+ if (stats.uses.length < 3) {
273
+ stats.uses.push(this.summarizeToolUse(block));
274
+ }
275
+
276
+ toolStats.set(block.name, stats);
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ return Array.from(toolStats.entries()).map(([tool, stats]) => ({
283
+ tool,
284
+ count: stats.count,
285
+ notableUses: stats.uses,
286
+ }));
287
+ }
288
+
289
+ /**
290
+ * Extract key decisions from conversation
291
+ */
292
+ private async extractKeyDecisions(messages: Message[]): Promise<string[]> {
293
+ const decisions: string[] = [];
294
+ const decisionIndicators = ['decided to', 'chose to', 'will use', 'going with'];
295
+
296
+ for (const msg of messages) {
297
+ const content = typeof msg.content === 'string' ? msg.content : '';
298
+
299
+ // Look for decision indicators
300
+ const hasDecision = decisionIndicators.some((indicator) =>
301
+ content.includes(indicator)
302
+ );
303
+
304
+ if (hasDecision) {
305
+ const decision = this.extractDecisionContext(content);
306
+ if (decision) decisions.push(decision);
307
+ }
308
+ }
309
+
310
+ return decisions.slice(0, 5); // Keep top 5
311
+ }
312
+
313
+ // ===== Helper Methods =====
314
+
315
+ /**
316
+ * Check if message contains tool results
317
+ */
318
+ private hasToolResults(message: Message): boolean {
319
+ if (typeof message.content === 'string') return false;
320
+ return message.content.some((block) => block.type === 'tool_result');
321
+ }
322
+
323
+ /**
324
+ * Clear tool result content (mark as pruned)
325
+ */
326
+ private clearToolResults(message: Message): void {
327
+ if (typeof message.content !== 'string') {
328
+ for (const block of message.content) {
329
+ if (block.type === 'tool_result') {
330
+ // Mark as pruned (OpenCode style)
331
+ (block as any).content = '[Old tool result content cleared]';
332
+ (block as any).pruned = true;
333
+ (block as any).prunedAt = new Date().toISOString();
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Format messages for summary prompt
341
+ */
342
+ private formatMessagesForSummary(messages: Message[]): string {
343
+ return messages
344
+ .map((msg, idx) => {
345
+ const role = msg.role.toUpperCase();
346
+ const content =
347
+ typeof msg.content === 'string'
348
+ ? msg.content
349
+ : this.extractTextContent(msg.content);
350
+ return `[${idx + 1}] ${role}: ${content.slice(0, 500)}`;
351
+ })
352
+ .join('\n\n');
353
+ }
354
+
355
+ /**
356
+ * Extract text content from message content
357
+ */
358
+ private extractTextContent(content: string | MessageContent[]): string {
359
+ if (typeof content === 'string') return content;
360
+ if (Array.isArray(content)) {
361
+ return content
362
+ .filter((c) => c.type === 'text')
363
+ .map((c) => (c as any).text)
364
+ .join(' ');
365
+ }
366
+ return '';
367
+ }
368
+
369
+ /**
370
+ * Summarize a tool use
371
+ */
372
+ private summarizeToolUse(block: any): string {
373
+ switch (block.name) {
374
+ case 'Write':
375
+ case 'Edit':
376
+ return `Modified ${block.input.file_path}`;
377
+ case 'Bash':
378
+ return `Ran: ${block.input.command?.slice(0, 50)}`;
379
+ case 'Read':
380
+ return `Read ${block.input.file_path}`;
381
+ default:
382
+ return `Used ${block.name}`;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Extract decision context from content
388
+ */
389
+ private extractDecisionContext(content: string): string | null {
390
+ const sentences = content.split(/[.!?]/);
391
+ const decisionKeywords = ['decided', 'chose', 'will use', 'going with'];
392
+
393
+ const decisionSentence = sentences.find((s) =>
394
+ decisionKeywords.some((keyword) => s.includes(keyword))
395
+ );
396
+
397
+ return decisionSentence?.trim() || null;
398
+ }
399
+
400
+ /**
401
+ * Generate unique summary ID
402
+ */
403
+ private generateId(): string {
404
+ return `sum-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
405
+ }
406
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Session Compression Module
3
+ *
4
+ * Implements OpenCode-inspired three-layer compression strategy:
5
+ * - Layer 1: Tool output pruning (fast, no cost)
6
+ * - Layer 2: Compaction summarization (LLM-based, medium cost)
7
+ * - Layer 3: Message filtering (recovery optimization)
8
+ */
9
+
10
+ export { CompressionEngine } from './engine.js';
11
+ export type {
12
+ CompressionConfig,
13
+ ConversationSummary,
14
+ ToolUsageSummary,
15
+ TokenUsage,
16
+ ModelInfo,
17
+ } from './types.js';
18
+ export { DEFAULT_COMPRESSION_CONFIG } from './types.js';