nova-terminal-assistant 0.1.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.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ContextCompressor - Intelligent context compression with structured summarization
|
|
3
|
+
// Reference: Factory.ai research (98.6% compression, 4.04/5 accuracy)
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import type { Message, ContentBlock, SessionId } from '../types/session.js';
|
|
7
|
+
|
|
8
|
+
// --- Structured Summary Types ---
|
|
9
|
+
|
|
10
|
+
export interface FileChange {
|
|
11
|
+
path: string;
|
|
12
|
+
action: 'created' | 'modified' | 'deleted';
|
|
13
|
+
summary: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Decision {
|
|
18
|
+
id: string;
|
|
19
|
+
topic: string;
|
|
20
|
+
reasoning: string;
|
|
21
|
+
outcome: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StructuredSummary {
|
|
26
|
+
/** Core intent of the current session */
|
|
27
|
+
sessionIntent: string;
|
|
28
|
+
/** Tracked file modifications */
|
|
29
|
+
fileModifications: FileChange[];
|
|
30
|
+
/** Key decisions made during the session */
|
|
31
|
+
decisions: Decision[];
|
|
32
|
+
/** Pending tasks / next steps */
|
|
33
|
+
nextSteps: string[];
|
|
34
|
+
/** Summary of completed actions */
|
|
35
|
+
completedActions: string[];
|
|
36
|
+
/** Total token count of this summary */
|
|
37
|
+
tokenCount: number;
|
|
38
|
+
/** Compression ratio (original / summary) */
|
|
39
|
+
compressionRatio: number;
|
|
40
|
+
/** When this summary was last updated */
|
|
41
|
+
lastUpdated: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ContextState {
|
|
45
|
+
/** Current token usage estimate */
|
|
46
|
+
tokenUsage: number;
|
|
47
|
+
/** Maximum context window */
|
|
48
|
+
maxTokens: number;
|
|
49
|
+
/** Whether the conversation has precise history that shouldn't be lost */
|
|
50
|
+
hasPreciseHistory: boolean;
|
|
51
|
+
/** Number of messages in the conversation */
|
|
52
|
+
messageCount: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Compression Strategy Types ---
|
|
56
|
+
|
|
57
|
+
type CompressionAction = 'compress' | 'keep' | 'summarize' | 'retrieve';
|
|
58
|
+
|
|
59
|
+
export interface CompressionResult {
|
|
60
|
+
action: CompressionAction;
|
|
61
|
+
/** Reasoning for the chosen action */
|
|
62
|
+
reason: string;
|
|
63
|
+
/** The resulting message list (may be compressed or original) */
|
|
64
|
+
messages: Message[];
|
|
65
|
+
/** If compression happened, the generated summary */
|
|
66
|
+
summary?: StructuredSummary;
|
|
67
|
+
/** Token count after compression */
|
|
68
|
+
resultingTokens: number;
|
|
69
|
+
/** Token count before compression */
|
|
70
|
+
originalTokens: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CompressionOptions {
|
|
74
|
+
/** Maximum tokens for the context window (default: 128000) */
|
|
75
|
+
maxTokens?: number;
|
|
76
|
+
/** Threshold to start compressing (default: 0.6 = 60%) */
|
|
77
|
+
compressThreshold?: number;
|
|
78
|
+
/** Threshold for aggressive compression (default: 0.85 = 85%) */
|
|
79
|
+
aggressiveThreshold?: number;
|
|
80
|
+
/** Maximum number of recent messages to always keep (default: 5) */
|
|
81
|
+
keepRecentCount?: number;
|
|
82
|
+
/** Whether to extract structured info from removed messages */
|
|
83
|
+
extractStructuredInfo?: boolean;
|
|
84
|
+
/** Existing summary to merge into (for incremental compression) */
|
|
85
|
+
existingSummary?: StructuredSummary;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Constants ---
|
|
89
|
+
|
|
90
|
+
const DEFAULT_OPTIONS: Required<CompressionOptions> = {
|
|
91
|
+
maxTokens: 128000,
|
|
92
|
+
compressThreshold: 0.6,
|
|
93
|
+
aggressiveThreshold: 0.85,
|
|
94
|
+
keepRecentCount: 5,
|
|
95
|
+
extractStructuredInfo: true,
|
|
96
|
+
existingSummary: undefined,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// --- ContextCompressor ---
|
|
100
|
+
|
|
101
|
+
export interface ContextCompressorOptions {
|
|
102
|
+
sessionId?: SessionId;
|
|
103
|
+
maxTokens?: number;
|
|
104
|
+
summaryModel?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class ContextCompressor {
|
|
108
|
+
private currentSummary: StructuredSummary | null = null;
|
|
109
|
+
private sessionId: SessionId | null = null;
|
|
110
|
+
private maxTokens: number;
|
|
111
|
+
|
|
112
|
+
constructor(options?: ContextCompressorOptions | SessionId) {
|
|
113
|
+
if (typeof options === 'string') {
|
|
114
|
+
this.sessionId = options;
|
|
115
|
+
this.maxTokens = DEFAULT_OPTIONS.maxTokens;
|
|
116
|
+
} else {
|
|
117
|
+
this.sessionId = options?.sessionId || null;
|
|
118
|
+
this.maxTokens = options?.maxTokens || DEFAULT_OPTIONS.maxTokens;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Analyze the context state and determine the best compression action.
|
|
124
|
+
*/
|
|
125
|
+
shouldCompress(state: ContextState): CompressionAction {
|
|
126
|
+
const usage = state.tokenUsage / state.maxTokens;
|
|
127
|
+
|
|
128
|
+
if (usage < DEFAULT_OPTIONS.compressThreshold) {
|
|
129
|
+
return 'keep';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (usage >= DEFAULT_OPTIONS.aggressiveThreshold && state.hasPreciseHistory) {
|
|
133
|
+
return 'retrieve';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (usage >= DEFAULT_OPTIONS.aggressiveThreshold) {
|
|
137
|
+
return 'summarize';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Between 60-85%: light compression
|
|
141
|
+
return 'compress';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Compress messages intelligently.
|
|
146
|
+
* - Keeps system messages always
|
|
147
|
+
* - Keeps the N most recent messages
|
|
148
|
+
* - Extracts structured information from removed messages
|
|
149
|
+
* - Generates a structured summary for the LLM context
|
|
150
|
+
*/
|
|
151
|
+
compress(
|
|
152
|
+
messages: Message[],
|
|
153
|
+
options?: CompressionOptions
|
|
154
|
+
): CompressionResult {
|
|
155
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
156
|
+
const originalTokens = this.estimateMessagesTokens(messages);
|
|
157
|
+
|
|
158
|
+
const state: ContextState = {
|
|
159
|
+
tokenUsage: originalTokens,
|
|
160
|
+
maxTokens: opts.maxTokens,
|
|
161
|
+
hasPreciseHistory: this.hasPreciseHistory(messages),
|
|
162
|
+
messageCount: messages.length,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const action = this.shouldCompress(state);
|
|
166
|
+
|
|
167
|
+
if (action === 'keep') {
|
|
168
|
+
return {
|
|
169
|
+
action: 'keep',
|
|
170
|
+
reason: `Context usage at ${((state.tokenUsage / state.maxTokens) * 100).toFixed(0)}%, below threshold of ${opts.compressThreshold * 100}%`,
|
|
171
|
+
messages,
|
|
172
|
+
resultingTokens: originalTokens,
|
|
173
|
+
originalTokens,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Separate message types
|
|
178
|
+
const systemMessages = messages.filter((m) => m.role === 'system');
|
|
179
|
+
const nonSystemMessages = messages.filter((m) => m.role !== 'system');
|
|
180
|
+
|
|
181
|
+
if (nonSystemMessages.length <= opts.keepRecentCount) {
|
|
182
|
+
// Not enough messages to compress meaningfully
|
|
183
|
+
return {
|
|
184
|
+
action: 'keep',
|
|
185
|
+
reason: 'Too few non-system messages to compress',
|
|
186
|
+
messages,
|
|
187
|
+
resultingTokens: originalTokens,
|
|
188
|
+
originalTokens,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Split into "old" (to be summarized) and "recent" (to be kept)
|
|
193
|
+
const recentMessages = nonSystemMessages.slice(-opts.keepRecentCount);
|
|
194
|
+
const oldMessages = nonSystemMessages.slice(0, -opts.keepRecentCount);
|
|
195
|
+
|
|
196
|
+
// Extract structured information from old messages
|
|
197
|
+
let summary = opts.existingSummary || this.currentSummary;
|
|
198
|
+
if (opts.extractStructuredInfo && oldMessages.length > 0) {
|
|
199
|
+
summary = this.extractStructuredInfo(oldMessages, summary);
|
|
200
|
+
this.currentSummary = summary;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Build the compressed message list
|
|
204
|
+
const compressedMessages: Message[] = [
|
|
205
|
+
...systemMessages,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
// Inject summary as a system-level context message if we have one
|
|
209
|
+
if (summary) {
|
|
210
|
+
compressedMessages.push(this.createSummaryMessage(summary));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add the recent messages back
|
|
214
|
+
compressedMessages.push(...recentMessages);
|
|
215
|
+
|
|
216
|
+
const resultingTokens = this.estimateMessagesTokens(compressedMessages);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
action,
|
|
220
|
+
reason: this.getCompressionReason(action, oldMessages.length, originalTokens, resultingTokens),
|
|
221
|
+
messages: compressedMessages,
|
|
222
|
+
summary,
|
|
223
|
+
resultingTokens,
|
|
224
|
+
originalTokens,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Incremental compression: only process new messages since last compression,
|
|
230
|
+
* merge into existing summary. More efficient than full recompression.
|
|
231
|
+
*/
|
|
232
|
+
incrementalCompress(
|
|
233
|
+
newMessages: Message[],
|
|
234
|
+
existingSummary: StructuredSummary | null
|
|
235
|
+
): StructuredSummary {
|
|
236
|
+
if (!existingSummary) {
|
|
237
|
+
return this.extractStructuredInfo(newMessages, null);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const newInfo = this.extractNewInfo(newMessages);
|
|
241
|
+
return this.mergeSummary(existingSummary, newInfo);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Extract a structured summary from a set of messages.
|
|
246
|
+
* This analyzes tool calls, file operations, and conversation content
|
|
247
|
+
* to build a structured representation of what happened.
|
|
248
|
+
*/
|
|
249
|
+
extractStructuredInfo(
|
|
250
|
+
messages: Message[],
|
|
251
|
+
base: StructuredSummary | null
|
|
252
|
+
): StructuredSummary {
|
|
253
|
+
const fileChanges = base ? [...base.fileModifications] : [];
|
|
254
|
+
const decisions = base ? [...base.decisions] : [];
|
|
255
|
+
const nextSteps: string[] = base ? [...base.nextSteps] : [];
|
|
256
|
+
const completedActions: string[] = base ? [...base.completedActions] : [];
|
|
257
|
+
|
|
258
|
+
let sessionIntent = base?.sessionIntent || '';
|
|
259
|
+
|
|
260
|
+
for (const message of messages) {
|
|
261
|
+
for (const block of message.content) {
|
|
262
|
+
if (block.type === 'tool_use') {
|
|
263
|
+
this.processToolUse(block.name, block.input, fileChanges, completedActions, decisions);
|
|
264
|
+
} else if (block.type === 'text') {
|
|
265
|
+
// Try to extract session intent from user messages
|
|
266
|
+
if (message.role === 'user' && !sessionIntent && block.text.length > 5) {
|
|
267
|
+
sessionIntent = block.text.slice(0, 200);
|
|
268
|
+
}
|
|
269
|
+
// Extract explicit decisions or next steps patterns
|
|
270
|
+
this.extractFromText(block.text, decisions, nextSteps, completedActions);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Deduplicate
|
|
276
|
+
const uniqueDecisions = this.deduplicateDecisions(decisions);
|
|
277
|
+
const uniqueNextSteps = [...new Set(nextSteps.filter((s) => s.length > 0))];
|
|
278
|
+
|
|
279
|
+
const summaryText = this.renderSummary(sessionIntent, fileChanges, uniqueDecisions, uniqueNextSteps, completedActions);
|
|
280
|
+
const tokenCount = this.estimateTokensText(summaryText);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
sessionIntent: sessionIntent || 'User conversation',
|
|
284
|
+
fileModifications: fileChanges.slice(-20), // Keep last 20 file changes
|
|
285
|
+
decisions: uniqueDecisions.slice(-10), // Keep last 10 decisions
|
|
286
|
+
nextSteps: uniqueNextSteps.slice(-10),
|
|
287
|
+
completedActions: completedActions.slice(-20),
|
|
288
|
+
tokenCount,
|
|
289
|
+
compressionRatio: 0, // Will be calculated externally
|
|
290
|
+
lastUpdated: Date.now(),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get the current structured summary.
|
|
296
|
+
*/
|
|
297
|
+
getSummary(): StructuredSummary | null {
|
|
298
|
+
return this.currentSummary;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Set an existing summary (e.g., loaded from persistence).
|
|
303
|
+
*/
|
|
304
|
+
setSummary(summary: StructuredSummary): void {
|
|
305
|
+
this.currentSummary = summary;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Clear the current summary.
|
|
310
|
+
*/
|
|
311
|
+
clearSummary(): void {
|
|
312
|
+
this.currentSummary = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Render a structured summary into a text format suitable for LLM context.
|
|
317
|
+
*/
|
|
318
|
+
renderSummaryToText(summary: StructuredSummary): string {
|
|
319
|
+
return this.renderSummary(
|
|
320
|
+
summary.sessionIntent,
|
|
321
|
+
summary.fileModifications,
|
|
322
|
+
summary.decisions,
|
|
323
|
+
summary.nextSteps,
|
|
324
|
+
summary.completedActions
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// --- Private Methods ---
|
|
329
|
+
|
|
330
|
+
private createSummaryMessage(summary: StructuredSummary): Message {
|
|
331
|
+
const text = this.renderSummaryToText(summary);
|
|
332
|
+
return {
|
|
333
|
+
id: `summary-${Date.now()}` as any,
|
|
334
|
+
role: 'user' as const,
|
|
335
|
+
content: [{ type: 'text', text }],
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
createdAt: new Date(),
|
|
338
|
+
metadata: { isContextSummary: true },
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private renderSummary(
|
|
343
|
+
intent: string,
|
|
344
|
+
files: FileChange[],
|
|
345
|
+
decisions: Decision[],
|
|
346
|
+
nextSteps: string[],
|
|
347
|
+
completed: string[]
|
|
348
|
+
): string {
|
|
349
|
+
const parts: string[] = [];
|
|
350
|
+
const safeCompleted = Array.isArray(completed) ? completed : [];
|
|
351
|
+
const safeFiles = Array.isArray(files) ? files : [];
|
|
352
|
+
const safeDecisions = Array.isArray(decisions) ? decisions : [];
|
|
353
|
+
const safeNextSteps = Array.isArray(nextSteps) ? nextSteps : [];
|
|
354
|
+
|
|
355
|
+
parts.push('<context-summary>');
|
|
356
|
+
parts.push(`Session goal: ${intent}`);
|
|
357
|
+
|
|
358
|
+
if (safeCompleted.length > 0) {
|
|
359
|
+
parts.push(`Completed: ${safeCompleted.join('; ')}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (safeFiles.length > 0) {
|
|
363
|
+
const fileStr = safeFiles.map((f) => `${f.action} ${f.path} (${f.summary})`).join('; ');
|
|
364
|
+
parts.push(`Files changed: ${fileStr}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (safeDecisions.length > 0) {
|
|
368
|
+
const decisionStr = safeDecisions.map((d) => `${d.topic}: ${d.outcome}`).join('; ');
|
|
369
|
+
parts.push(`Decisions: ${decisionStr}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (safeNextSteps.length > 0) {
|
|
373
|
+
parts.push(`Next: ${safeNextSteps.join('; ')}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
parts.push('</context-summary>');
|
|
377
|
+
return parts.join('\n');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private extractNewInfo(messages: Message[]): Partial<StructuredSummary> {
|
|
381
|
+
// For incremental: just extract from the new messages
|
|
382
|
+
const tempBase: StructuredSummary = {
|
|
383
|
+
sessionIntent: '',
|
|
384
|
+
fileModifications: [],
|
|
385
|
+
decisions: [],
|
|
386
|
+
nextSteps: [],
|
|
387
|
+
completedActions: [],
|
|
388
|
+
tokenCount: 0,
|
|
389
|
+
compressionRatio: 0,
|
|
390
|
+
lastUpdated: Date.now(),
|
|
391
|
+
};
|
|
392
|
+
const extracted = this.extractStructuredInfo(messages, null);
|
|
393
|
+
return {
|
|
394
|
+
sessionIntent: extracted.sessionIntent,
|
|
395
|
+
fileModifications: extracted.fileModifications,
|
|
396
|
+
decisions: extracted.decisions,
|
|
397
|
+
nextSteps: extracted.nextSteps,
|
|
398
|
+
completedActions: extracted.completedActions,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private mergeSummary(
|
|
403
|
+
existing: StructuredSummary,
|
|
404
|
+
newInfo: Partial<StructuredSummary>
|
|
405
|
+
): StructuredSummary {
|
|
406
|
+
const mergedFiles = [...(Array.isArray(existing.fileModifications) ? existing.fileModifications : [])];
|
|
407
|
+
for (const fc of newInfo.fileModifications || []) {
|
|
408
|
+
// Update if same path, otherwise add
|
|
409
|
+
const existingIdx = mergedFiles.findIndex((f) => f.path === fc.path);
|
|
410
|
+
if (existingIdx >= 0) {
|
|
411
|
+
mergedFiles[existingIdx] = fc; // Replace with newer version
|
|
412
|
+
} else {
|
|
413
|
+
mergedFiles.push(fc);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const mergedDecisions = [...(Array.isArray(existing.decisions) ? existing.decisions : [])];
|
|
418
|
+
for (const d of newInfo.decisions || []) {
|
|
419
|
+
const existingIdx = mergedDecisions.findIndex((ed) => ed.topic === d.topic);
|
|
420
|
+
if (existingIdx >= 0) {
|
|
421
|
+
mergedDecisions[existingIdx] = d;
|
|
422
|
+
} else {
|
|
423
|
+
mergedDecisions.push(d);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Next steps: keep new ones, remove completed ones
|
|
428
|
+
const completedSet = new Set(newInfo.completedActions || []);
|
|
429
|
+
const mergedNextSteps = (newInfo.nextSteps || [])
|
|
430
|
+
.filter((s) => !completedSet.has(s))
|
|
431
|
+
.concat(
|
|
432
|
+
(existing.nextSteps || []).filter(
|
|
433
|
+
(s) => !completedSet.has(s) && !(newInfo.nextSteps || []).includes(s)
|
|
434
|
+
)
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const allCompleted = [
|
|
438
|
+
...(existing.completedActions || []),
|
|
439
|
+
...(newInfo.completedActions || []),
|
|
440
|
+
].filter((v, i, a) => a.indexOf(v) === i);
|
|
441
|
+
|
|
442
|
+
const summaryText = this.renderSummary(
|
|
443
|
+
newInfo.sessionIntent || existing.sessionIntent,
|
|
444
|
+
mergedFiles.slice(-20),
|
|
445
|
+
mergedDecisions.slice(-10),
|
|
446
|
+
mergedNextSteps.slice(-10),
|
|
447
|
+
allCompleted.slice(-20)
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
sessionIntent: newInfo.sessionIntent || existing.sessionIntent,
|
|
452
|
+
fileModifications: mergedFiles.slice(-20),
|
|
453
|
+
decisions: mergedDecisions.slice(-10),
|
|
454
|
+
nextSteps: mergedNextSteps.slice(-10),
|
|
455
|
+
completedActions: allCompleted.slice(-20),
|
|
456
|
+
tokenCount: this.estimateTokensText(summaryText),
|
|
457
|
+
compressionRatio: 0,
|
|
458
|
+
lastUpdated: Date.now(),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private processToolUse(
|
|
463
|
+
toolName: string,
|
|
464
|
+
input: Record<string, unknown>,
|
|
465
|
+
fileChanges: FileChange[],
|
|
466
|
+
completedActions: string[],
|
|
467
|
+
decisions: Decision[]
|
|
468
|
+
): void {
|
|
469
|
+
switch (toolName) {
|
|
470
|
+
case 'write_file': {
|
|
471
|
+
const path = String(input.file_path || input.filePath || 'unknown');
|
|
472
|
+
const summary = String(input.content || '').slice(0, 100);
|
|
473
|
+
fileChanges.push({
|
|
474
|
+
path,
|
|
475
|
+
action: 'created',
|
|
476
|
+
summary: `Created (${summary.length} chars)`,
|
|
477
|
+
timestamp: Date.now(),
|
|
478
|
+
});
|
|
479
|
+
completedActions.push(`Created ${path}`);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 'edit_file': {
|
|
483
|
+
const path = String(input.file_path || input.filePath || 'unknown');
|
|
484
|
+
// Check if we already have a recent change for this file
|
|
485
|
+
const existing = fileChanges.find((f) => f.path === path && Date.now() - f.timestamp < 60000);
|
|
486
|
+
if (existing) {
|
|
487
|
+
existing.action = 'modified';
|
|
488
|
+
existing.summary = `Modified (${Date.now() - existing.timestamp}ms ago)`;
|
|
489
|
+
existing.timestamp = Date.now();
|
|
490
|
+
} else {
|
|
491
|
+
fileChanges.push({
|
|
492
|
+
path,
|
|
493
|
+
action: 'modified',
|
|
494
|
+
summary: 'Modified',
|
|
495
|
+
timestamp: Date.now(),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
completedActions.push(`Edited ${path}`);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case 'execute_command': {
|
|
502
|
+
const cmd = String((input as any).command || (input as any).params?.command || 'unknown');
|
|
503
|
+
completedActions.push(`Ran: ${cmd.slice(0, 80)}`);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
case 'search_content':
|
|
507
|
+
case 'search_file': {
|
|
508
|
+
const query = String(input.query || input.pattern || input.glob_pattern || 'unknown');
|
|
509
|
+
completedActions.push(`Searched: ${query.slice(0, 60)}`);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
case 'read_file': {
|
|
513
|
+
const path = String(input.file_path || input.filePath || 'unknown');
|
|
514
|
+
completedActions.push(`Read ${path}`);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private extractFromText(
|
|
521
|
+
text: string,
|
|
522
|
+
decisions: Decision[],
|
|
523
|
+
nextSteps: string[],
|
|
524
|
+
completedActions: string[]
|
|
525
|
+
): void {
|
|
526
|
+
// Extract "decided to" or "chose to" patterns
|
|
527
|
+
const decisionPatterns = [
|
|
528
|
+
/(?:decided|chose|opted)\s+to\s+(.+)/gi,
|
|
529
|
+
/(?:going|will)\s+(?:use|implement|go with|try)\s+(.+)/gi,
|
|
530
|
+
];
|
|
531
|
+
for (const pattern of decisionPatterns) {
|
|
532
|
+
const matches = text.matchAll(pattern);
|
|
533
|
+
for (const match of matches) {
|
|
534
|
+
decisions.push({
|
|
535
|
+
id: `d-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
536
|
+
topic: match[1].slice(0, 50),
|
|
537
|
+
reasoning: '',
|
|
538
|
+
outcome: match[1].slice(0, 100),
|
|
539
|
+
timestamp: Date.now(),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Extract "next step" / "todo" / "need to" patterns
|
|
545
|
+
const nextPatterns = [
|
|
546
|
+
/(?:next\s+(?:step|up)|todo|need\s+to|should|must)\s*[:\-]?\s*(.+)/gi,
|
|
547
|
+
/\d+\.\s+\*\*(.+)\*\*/g, // Markdown numbered bold items
|
|
548
|
+
];
|
|
549
|
+
for (const pattern of nextPatterns) {
|
|
550
|
+
const matches = text.matchAll(pattern);
|
|
551
|
+
for (const match of matches) {
|
|
552
|
+
const step = match[1].trim().slice(0, 100);
|
|
553
|
+
if (step.length > 3) {
|
|
554
|
+
nextSteps.push(step);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private hasPreciseHistory(messages: Message[]): boolean {
|
|
561
|
+
// Check if there are any file edits or commands that represent precise work
|
|
562
|
+
for (const msg of messages) {
|
|
563
|
+
for (const block of msg.content) {
|
|
564
|
+
if (block.type === 'tool_use') {
|
|
565
|
+
if (block.name === 'edit_file' || block.name === 'write_file' || block.name === 'execute_command') {
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private deduplicateDecisions(decisions: Decision[]): Decision[] {
|
|
575
|
+
const seen = new Set<string>();
|
|
576
|
+
return decisions.filter((d) => {
|
|
577
|
+
const key = d.topic.toLowerCase().slice(0, 30);
|
|
578
|
+
if (seen.has(key)) return false;
|
|
579
|
+
seen.add(key);
|
|
580
|
+
return true;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private getCompressionReason(
|
|
585
|
+
action: CompressionAction,
|
|
586
|
+
removedCount: number,
|
|
587
|
+
originalTokens: number,
|
|
588
|
+
resultingTokens: number
|
|
589
|
+
): string {
|
|
590
|
+
const saved = originalTokens - resultingTokens;
|
|
591
|
+
const pct = originalTokens > 0 ? ((saved / originalTokens) * 100).toFixed(0) : '0';
|
|
592
|
+
switch (action) {
|
|
593
|
+
case 'compress':
|
|
594
|
+
return `Light compression: removed ${removedCount} old messages, saved ~${pct}% tokens`;
|
|
595
|
+
case 'summarize':
|
|
596
|
+
return `Summarized ${removedCount} old messages into structured summary, saved ~${pct}% tokens`;
|
|
597
|
+
case 'retrieve':
|
|
598
|
+
return `Aggressive compression with retrieval: summarized ${removedCount} messages, saved ~${pct}% tokens`;
|
|
599
|
+
default:
|
|
600
|
+
return `No compression needed`;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Exposed token estimation for messages (public API) */
|
|
605
|
+
estimateTokens(messages: Message[]): number {
|
|
606
|
+
return this.estimateMessagesTokens(messages);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Get compression history */
|
|
610
|
+
getCompressionHistory(): Array<{ action: CompressionAction; timestamp: Date; originalTokens: number; resultingTokens: number }> {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Get compressor stats */
|
|
615
|
+
getStats(): { totalCompressions: number; tokensSaved: number } {
|
|
616
|
+
return { totalCompressions: 0, tokensSaved: 0 };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private estimateTokensText(text: string): number {
|
|
620
|
+
return Math.ceil(text.length / 4);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private estimateMessagesTokens(messages: Message[]): number {
|
|
624
|
+
let total = 0;
|
|
625
|
+
for (const msg of messages) {
|
|
626
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
627
|
+
for (const block of content) {
|
|
628
|
+
if (block.type === 'text') {
|
|
629
|
+
total += this.estimateTokensText(block.text);
|
|
630
|
+
} else if (block.type === 'tool_use') {
|
|
631
|
+
total += this.estimateTokensText(JSON.stringify(block.input));
|
|
632
|
+
} else if (block.type === 'tool_result') {
|
|
633
|
+
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
634
|
+
total += this.estimateTokensText(content);
|
|
635
|
+
} else if (block.type === 'thinking') {
|
|
636
|
+
total += this.estimateTokensText(block.thinking);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return total;
|
|
641
|
+
}
|
|
642
|
+
}
|