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.
- package/RELEASE_NOTES_v0.4.0.md +140 -0
- package/dist/agent/agent.d.ts +17 -2
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +279 -49
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/types.d.ts +15 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/checkpointing/checkpoint-manager.d.ts +24 -0
- package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -1
- package/dist/checkpointing/checkpoint-manager.js +28 -0
- package/dist/checkpointing/checkpoint-manager.js.map +1 -1
- package/dist/cli/components/App.d.ts +8 -0
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +478 -36
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +2 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Header.d.ts +6 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/Header.js +3 -3
- package/dist/cli/components/Header.js.map +1 -1
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +7 -9
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/index.js +3 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/types.d.ts +20 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/input/history-manager.d.ts +78 -0
- package/dist/input/history-manager.d.ts.map +1 -0
- package/dist/input/history-manager.js +224 -0
- package/dist/input/history-manager.js.map +1 -0
- package/dist/input/index.d.ts +6 -0
- package/dist/input/index.d.ts.map +1 -0
- package/dist/input/index.js +5 -0
- package/dist/input/index.js.map +1 -0
- package/dist/prompts/index.js +3 -3
- package/dist/prompts/index.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +33 -2
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/google.d.ts +22 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +297 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +4 -4
- package/dist/providers/index.js +11 -11
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +6 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/registry.js +3 -3
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +30 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/session/compression/engine.d.ts +109 -0
- package/dist/session/compression/engine.d.ts.map +1 -0
- package/dist/session/compression/engine.js +311 -0
- package/dist/session/compression/engine.js.map +1 -0
- package/dist/session/compression/index.d.ts +12 -0
- package/dist/session/compression/index.d.ts.map +1 -0
- package/dist/session/compression/index.js +11 -0
- package/dist/session/compression/index.js.map +1 -0
- package/dist/session/compression/types.d.ts +90 -0
- package/dist/session/compression/types.d.ts.map +1 -0
- package/dist/session/compression/types.js +17 -0
- package/dist/session/compression/types.js.map +1 -0
- package/dist/session/manager.d.ts +64 -3
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +254 -2
- package/dist/session/manager.js.map +1 -1
- package/dist/session/types.d.ts +16 -0
- package/dist/session/types.d.ts.map +1 -1
- package/dist/session/types.js.map +1 -1
- package/docs/README.md +1 -0
- package/docs/diagrams/compression-decision.mmd +30 -0
- package/docs/diagrams/compression-workflow.mmd +54 -0
- package/docs/diagrams/layer1-pruning.mmd +45 -0
- package/docs/diagrams/layer2-compaction.mmd +42 -0
- package/docs/proposals/0007-context-management.md +252 -2
- package/docs/proposals/README.md +4 -3
- package/docs/providers.md +3 -3
- package/docs/session-compression.md +695 -0
- package/examples/agent-demo.ts +23 -1
- package/examples/basic.ts +3 -3
- package/package.json +3 -4
- package/src/agent/agent.ts +314 -52
- package/src/agent/types.ts +19 -1
- package/src/checkpointing/checkpoint-manager.ts +48 -0
- package/src/cli/components/App.tsx +553 -34
- package/src/cli/components/CommandSuggestions.tsx +2 -0
- package/src/cli/components/Header.tsx +16 -1
- package/src/cli/components/Messages.tsx +20 -14
- package/src/cli/index.tsx +3 -2
- package/src/config/types.ts +26 -1
- package/src/index.ts +3 -3
- package/src/input/history-manager.ts +289 -0
- package/src/input/index.ts +6 -0
- package/src/prompts/index.test.ts +2 -1
- package/src/prompts/index.ts +3 -3
- package/src/providers/{gemini.ts → google.ts} +69 -18
- package/src/providers/index.ts +14 -14
- package/src/providers/openai.ts +7 -0
- package/src/providers/registry.ts +3 -3
- package/src/providers/types.ts +33 -3
- package/src/session/compression/engine.ts +406 -0
- package/src/session/compression/index.ts +18 -0
- package/src/session/compression/types.ts +102 -0
- package/src/session/manager.ts +326 -3
- package/src/session/types.ts +21 -0
- package/tests/input-history-manager.test.ts +335 -0
- package/tests/session-checkpoint-persistence.test.ts +198 -0
package/src/providers/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
32
|
+
google: GoogleConfig;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export interface CreateProviderOptions {
|
|
36
36
|
provider: Provider;
|
|
37
37
|
authMethod?: AuthMethod;
|
|
38
|
-
config?: OpenAIConfig | AnthropicConfig |
|
|
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 === '
|
|
65
|
+
if (provider === 'google') {
|
|
66
66
|
if (authMethod === 'api_key') {
|
|
67
|
-
return new
|
|
67
|
+
return new GoogleProvider(config as GoogleConfig);
|
|
68
68
|
}
|
|
69
|
-
throw new Error(`Unsupported auth method for
|
|
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
|
-
//
|
|
100
|
+
// Google models (Gemini)
|
|
101
101
|
if (modelLower.includes('gemini') || modelLower.includes('palm')) {
|
|
102
|
-
return '
|
|
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: '
|
|
148
|
-
'gemini-1.5-pro': { provider: '
|
|
149
|
-
'gemini-1.5-flash': { provider: '
|
|
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' },
|
package/src/providers/openai.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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: '
|
|
44
|
+
{ id: 'google', name: 'Google', popularity: 3 },
|
|
45
45
|
];
|
|
46
46
|
|
|
47
47
|
// ============================================================================
|
package/src/providers/types.ts
CHANGED
|
@@ -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' | '
|
|
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
|
|
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 |
|
|
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';
|