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,657 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// LayeredMemoryManager - L1-L4 layered memory architecture
|
|
3
|
+
// Reference: Letta/MemGPT memory system with context budget management
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import type { Message, SessionId } from '../types/session.js';
|
|
10
|
+
import { ContextCompressor, type StructuredSummary, type FileChange, type Decision } from './ContextCompressor.js';
|
|
11
|
+
|
|
12
|
+
// --- Memory Layer Types ---
|
|
13
|
+
|
|
14
|
+
export interface MemoryEntry {
|
|
15
|
+
id: string;
|
|
16
|
+
content: string;
|
|
17
|
+
layer: MemoryLayer;
|
|
18
|
+
category: MemoryCategory;
|
|
19
|
+
tags: string[];
|
|
20
|
+
createdAt: number;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
accessCount: number;
|
|
23
|
+
lastAccessedAt: number;
|
|
24
|
+
/** Approximate token count */
|
|
25
|
+
tokenCount: number;
|
|
26
|
+
/** Optional TTL in ms; entry expires after this */
|
|
27
|
+
ttl?: number;
|
|
28
|
+
/** Relevance score (0-1), used for retrieval ranking */
|
|
29
|
+
relevanceScore?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type MemoryLayer = 'L1_immediate' | 'L2_working' | 'L3_longterm' | 'L4_archival';
|
|
33
|
+
|
|
34
|
+
export type MemoryCategory = 'project' | 'user' | 'convention' | 'decision' | 'pattern' | 'error';
|
|
35
|
+
|
|
36
|
+
export interface MemoryContext {
|
|
37
|
+
/** L1 messages - always injected into the context */
|
|
38
|
+
immediate: Message[];
|
|
39
|
+
/** L2 working summary - session-level persistence */
|
|
40
|
+
workingSummary: StructuredSummary | null;
|
|
41
|
+
/** L3 relevant long-term memory entries */
|
|
42
|
+
longTermEntries: MemoryEntry[];
|
|
43
|
+
/** L4 archival entries (only loaded on demand) */
|
|
44
|
+
archivalEntries: MemoryEntry[];
|
|
45
|
+
/** Total estimated token count of all loaded memory */
|
|
46
|
+
totalTokens: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MemoryQuery {
|
|
50
|
+
/** The task or query to match against */
|
|
51
|
+
query: string;
|
|
52
|
+
/** Maximum number of entries to retrieve */
|
|
53
|
+
maxEntries?: number;
|
|
54
|
+
/** Minimum relevance score (0-1) */
|
|
55
|
+
minScore?: number;
|
|
56
|
+
/** Which layers to search */
|
|
57
|
+
layers?: MemoryLayer[];
|
|
58
|
+
/** Which categories to search */
|
|
59
|
+
categories?: MemoryCategory[];
|
|
60
|
+
/** Maximum total tokens for returned entries */
|
|
61
|
+
maxTokens?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LayeredMemoryOptions {
|
|
65
|
+
/** Session ID for persistence isolation */
|
|
66
|
+
sessionId: SessionId;
|
|
67
|
+
/** Base directory for memory persistence */
|
|
68
|
+
storageDir?: string;
|
|
69
|
+
/** L1: Maximum number of recent messages to keep (default: 10) */
|
|
70
|
+
l1MaxMessages?: number;
|
|
71
|
+
/** L2: Maximum token budget for working summary (default: 2000) */
|
|
72
|
+
l2MaxTokens?: number;
|
|
73
|
+
/** L3: Maximum entries to load into context (default: 20) */
|
|
74
|
+
l3MaxEntries?: number;
|
|
75
|
+
/** L4: Maximum entries to retrieve from archival (default: 5) */
|
|
76
|
+
l4MaxRetrieveEntries?: number;
|
|
77
|
+
/** Total token budget for all memory layers combined (default: 20000) */
|
|
78
|
+
totalTokenBudget?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Circular Buffer for L1 ---
|
|
82
|
+
|
|
83
|
+
class CircularBuffer<T> {
|
|
84
|
+
private buffer: T[] = [];
|
|
85
|
+
private maxSize: number;
|
|
86
|
+
|
|
87
|
+
constructor(maxSize: number) {
|
|
88
|
+
this.maxSize = maxSize;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
push(item: T): void {
|
|
92
|
+
if (this.buffer.length >= this.maxSize) {
|
|
93
|
+
this.buffer.shift();
|
|
94
|
+
}
|
|
95
|
+
this.buffer.push(item);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getAll(): T[] {
|
|
99
|
+
return [...this.buffer];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get size(): number {
|
|
103
|
+
return this.buffer.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
clear(): void {
|
|
107
|
+
this.buffer = [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- LayeredMemoryManager ---
|
|
112
|
+
|
|
113
|
+
export class LayeredMemoryManager {
|
|
114
|
+
private readonly sessionId: SessionId;
|
|
115
|
+
private readonly storageDir: string;
|
|
116
|
+
|
|
117
|
+
// L1: Immediate memory - circular buffer of recent messages
|
|
118
|
+
private immediateBuffer: CircularBuffer<Message>;
|
|
119
|
+
|
|
120
|
+
// L2: Working memory - structured session summary
|
|
121
|
+
private compressor: ContextCompressor;
|
|
122
|
+
private workingSummary: StructuredSummary | null = null;
|
|
123
|
+
|
|
124
|
+
// L3: Long-term memory - in-memory entries with persistence
|
|
125
|
+
private longTermMemory: Map<string, MemoryEntry> = new Map();
|
|
126
|
+
|
|
127
|
+
// L4: Archival memory - persisted to disk, loaded on demand
|
|
128
|
+
private archivalIndex: Map<string, MemoryEntry> = new Map();
|
|
129
|
+
|
|
130
|
+
// Configuration
|
|
131
|
+
private readonly l2MaxTokens: number;
|
|
132
|
+
private readonly l3MaxEntries: number;
|
|
133
|
+
private readonly l4MaxRetrieveEntries: number;
|
|
134
|
+
private readonly totalTokenBudget: number;
|
|
135
|
+
|
|
136
|
+
// Persistence dirty flag
|
|
137
|
+
private dirty = false;
|
|
138
|
+
|
|
139
|
+
constructor(options: LayeredMemoryOptions) {
|
|
140
|
+
this.sessionId = options.sessionId;
|
|
141
|
+
this.storageDir = options.storageDir || path.join(os.homedir(), '.nova', 'memory', this.sessionId.slice(0, 8));
|
|
142
|
+
this.immediateBuffer = new CircularBuffer(options.l1MaxMessages || 10);
|
|
143
|
+
this.compressor = new ContextCompressor(this.sessionId);
|
|
144
|
+
this.l2MaxTokens = options.l2MaxTokens || 2000;
|
|
145
|
+
this.l3MaxEntries = options.l3MaxEntries || 20;
|
|
146
|
+
this.l4MaxRetrieveEntries = options.l4MaxRetrieveEntries || 5;
|
|
147
|
+
this.totalTokenBudget = options.totalTokenBudget || 20000;
|
|
148
|
+
|
|
149
|
+
this.ensureStorageDir();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ========================================================================
|
|
153
|
+
// L1: Immediate Memory
|
|
154
|
+
// ========================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add a message to L1 immediate memory.
|
|
158
|
+
* This is the most recent conversation context.
|
|
159
|
+
*/
|
|
160
|
+
addImmediateMessage(message: Message): void {
|
|
161
|
+
this.immediateBuffer.push(message);
|
|
162
|
+
this.dirty = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get all L1 immediate messages.
|
|
167
|
+
*/
|
|
168
|
+
getImmediateMessages(): Message[] {
|
|
169
|
+
return this.immediateBuffer.getAll();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clear L1 immediate memory (e.g., on session reset).
|
|
174
|
+
*/
|
|
175
|
+
clearImmediateMemory(): void {
|
|
176
|
+
this.immediateBuffer.clear();
|
|
177
|
+
this.dirty = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ========================================================================
|
|
181
|
+
// L2: Working Memory (Session-level)
|
|
182
|
+
// ========================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Promote messages from L1 to L2 working memory via compression.
|
|
186
|
+
* Called when L1 is full and messages need to be "graduated" to summary.
|
|
187
|
+
*/
|
|
188
|
+
async promoteToWorkingMemory(): Promise<void> {
|
|
189
|
+
const messages = this.immediateBuffer.getAll();
|
|
190
|
+
if (messages.length < 2) return;
|
|
191
|
+
|
|
192
|
+
// Use the compressor to extract structured info
|
|
193
|
+
const newSummary = this.compressor.incrementalCompress(messages, this.workingSummary);
|
|
194
|
+
|
|
195
|
+
// Only update if compressor returned a valid summary
|
|
196
|
+
if (newSummary) {
|
|
197
|
+
this.workingSummary = newSummary;
|
|
198
|
+
|
|
199
|
+
// Ensure summary doesn't exceed token budget
|
|
200
|
+
if (this.workingSummary.tokenCount > this.l2MaxTokens) {
|
|
201
|
+
this.workingSummary = this.trimSummary(this.workingSummary, this.l2MaxTokens);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Clear immediate memory after promotion
|
|
206
|
+
this.immediateBuffer.clear();
|
|
207
|
+
this.dirty = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the L2 working memory summary.
|
|
212
|
+
*/
|
|
213
|
+
getWorkingSummary(): StructuredSummary | null {
|
|
214
|
+
return this.workingSummary;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Set the working summary directly (e.g., restored from persistence).
|
|
219
|
+
*/
|
|
220
|
+
setWorkingSummary(summary: StructuredSummary): void {
|
|
221
|
+
this.workingSummary = summary;
|
|
222
|
+
this.dirty = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ========================================================================
|
|
226
|
+
// L3: Long-term Memory
|
|
227
|
+
// ========================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Store an entry in L3 long-term memory.
|
|
231
|
+
*/
|
|
232
|
+
async storeLongTerm(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt' | 'accessCount' | 'lastAccessedAt' | 'tokenCount'>): Promise<MemoryEntry> {
|
|
233
|
+
const fullEntry: MemoryEntry = {
|
|
234
|
+
...entry,
|
|
235
|
+
id: `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
236
|
+
createdAt: Date.now(),
|
|
237
|
+
updatedAt: Date.now(),
|
|
238
|
+
accessCount: 0,
|
|
239
|
+
lastAccessedAt: Date.now(),
|
|
240
|
+
tokenCount: this.estimateTokens(entry.content),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
this.longTermMemory.set(fullEntry.id, fullEntry);
|
|
244
|
+
this.dirty = true;
|
|
245
|
+
|
|
246
|
+
return fullEntry;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Retrieve relevant entries from L3 long-term memory based on a query.
|
|
251
|
+
* Uses simple keyword matching (can be upgraded to embeddings later).
|
|
252
|
+
*/
|
|
253
|
+
retrieveLongTerm(query: MemoryQuery): MemoryEntry[] {
|
|
254
|
+
const queryLower = query.query.toLowerCase();
|
|
255
|
+
const queryTokens = queryLower.split(/\s+/);
|
|
256
|
+
|
|
257
|
+
let entries = Array.from(this.longTermMemory.values());
|
|
258
|
+
|
|
259
|
+
// Filter by layers and categories if specified
|
|
260
|
+
if (query.layers) {
|
|
261
|
+
entries = entries.filter((e) => query.layers!.includes(e.layer));
|
|
262
|
+
}
|
|
263
|
+
if (query.categories) {
|
|
264
|
+
entries = entries.filter((e) => query.categories!.includes(e.category));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Filter expired entries
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
entries = entries.filter((e) => {
|
|
270
|
+
if (e.ttl && now - e.createdAt > e.ttl) return false;
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Score and rank by relevance
|
|
275
|
+
const scored = entries.map((entry) => {
|
|
276
|
+
const contentStr = typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content);
|
|
277
|
+
const contentLower = contentStr.toLowerCase();
|
|
278
|
+
const tagsLower = entry.tags.join(' ').toLowerCase();
|
|
279
|
+
|
|
280
|
+
// Simple TF scoring: count matching tokens
|
|
281
|
+
let score = 0;
|
|
282
|
+
for (const token of queryTokens) {
|
|
283
|
+
if (token.length < 2) continue;
|
|
284
|
+
const regex = new RegExp(token, 'gi');
|
|
285
|
+
const contentMatches = contentLower.match(regex);
|
|
286
|
+
const tagMatches = tagsLower.match(regex);
|
|
287
|
+
score += (contentMatches?.length || 0) * 2; // Content matches count more
|
|
288
|
+
score += (tagMatches?.length || 0) * 3; // Tag matches count even more
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Boost recently accessed entries slightly
|
|
292
|
+
const recencyBoost = 1 + (entry.accessCount * 0.05);
|
|
293
|
+
score *= recencyBoost;
|
|
294
|
+
|
|
295
|
+
entry.relevanceScore = Math.min(score / 10, 1);
|
|
296
|
+
return { entry, score };
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Sort by relevance descending
|
|
300
|
+
scored.sort((a, b) => b.score - a.score);
|
|
301
|
+
|
|
302
|
+
// Apply min score filter
|
|
303
|
+
const filtered = scored.filter(
|
|
304
|
+
(s) => !query.minScore || s.entry.relevanceScore! >= query.minScore
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Apply token budget
|
|
308
|
+
let totalTokens = 0;
|
|
309
|
+
const maxTokens = query.maxTokens || this.totalTokenBudget * 0.4;
|
|
310
|
+
const maxEntries = query.maxEntries || this.l3MaxEntries;
|
|
311
|
+
const result: MemoryEntry[] = [];
|
|
312
|
+
|
|
313
|
+
for (const { entry } of filtered) {
|
|
314
|
+
if (result.length >= maxEntries) break;
|
|
315
|
+
if (totalTokens + entry.tokenCount > maxTokens) break;
|
|
316
|
+
|
|
317
|
+
// Update access metadata
|
|
318
|
+
entry.accessCount++;
|
|
319
|
+
entry.lastAccessedAt = Date.now();
|
|
320
|
+
result.push(entry);
|
|
321
|
+
totalTokens += entry.tokenCount;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get all L3 entries (for management purposes).
|
|
329
|
+
*/
|
|
330
|
+
getAllLongTermEntries(): MemoryEntry[] {
|
|
331
|
+
return Array.from(this.longTermMemory.values());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Delete an entry from L3.
|
|
336
|
+
*/
|
|
337
|
+
deleteLongTerm(id: string): boolean {
|
|
338
|
+
const deleted = this.longTermMemory.delete(id);
|
|
339
|
+
if (deleted) this.dirty = true;
|
|
340
|
+
return deleted;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ========================================================================
|
|
344
|
+
// L4: Archival Memory
|
|
345
|
+
// ========================================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Archive an entry to L4 (persist to disk).
|
|
349
|
+
*/
|
|
350
|
+
async archiveEntry(entry: MemoryEntry): Promise<void> {
|
|
351
|
+
this.archivalIndex.set(entry.id, entry);
|
|
352
|
+
await this.persistArchivalEntry(entry);
|
|
353
|
+
this.dirty = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Retrieve relevant entries from L4 archival.
|
|
358
|
+
* Currently uses same keyword matching as L3, but from disk-backed index.
|
|
359
|
+
*/
|
|
360
|
+
async retrieveArchival(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
361
|
+
const queryLower = query.query.toLowerCase();
|
|
362
|
+
const queryTokens = queryLower.split(/\s+/);
|
|
363
|
+
|
|
364
|
+
let entries = Array.from(this.archivalIndex.values());
|
|
365
|
+
|
|
366
|
+
// Filter expired
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
entries = entries.filter((e) => !e.ttl || now - e.createdAt <= e.ttl);
|
|
369
|
+
|
|
370
|
+
// Score relevance
|
|
371
|
+
const scored = entries.map((entry) => {
|
|
372
|
+
const contentStr = typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content);
|
|
373
|
+
const contentLower = contentStr.toLowerCase();
|
|
374
|
+
let score = 0;
|
|
375
|
+
for (const token of queryTokens) {
|
|
376
|
+
if (token.length < 2) continue;
|
|
377
|
+
const regex = new RegExp(token, 'gi');
|
|
378
|
+
score += (contentLower.match(regex)?.length || 0);
|
|
379
|
+
}
|
|
380
|
+
entry.relevanceScore = Math.min(score / 5, 1);
|
|
381
|
+
return { entry, score };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
scored.sort((a, b) => b.score - a.score);
|
|
385
|
+
|
|
386
|
+
const minScore = query.minScore || 0.3;
|
|
387
|
+
const maxEntries = query.maxEntries || this.l4MaxRetrieveEntries;
|
|
388
|
+
const result: MemoryEntry[] = [];
|
|
389
|
+
|
|
390
|
+
for (const { entry } of scored) {
|
|
391
|
+
if (result.length >= maxEntries) break;
|
|
392
|
+
if (entry.relevanceScore! >= minScore) {
|
|
393
|
+
result.push(entry);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ========================================================================
|
|
401
|
+
// Unified Memory Loading
|
|
402
|
+
// ========================================================================
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Load all relevant memory into a MemoryContext for injection into the LLM.
|
|
406
|
+
* This is the main entry point for the AgentLoop to get context.
|
|
407
|
+
*/
|
|
408
|
+
async loadMemory(query?: string): Promise<MemoryContext> {
|
|
409
|
+
let totalTokens = 0;
|
|
410
|
+
|
|
411
|
+
// L1: Immediate messages
|
|
412
|
+
const immediate = this.immediateBuffer.getAll();
|
|
413
|
+
totalTokens += this.estimateMessagesTokens(immediate);
|
|
414
|
+
|
|
415
|
+
// L2: Working summary
|
|
416
|
+
const workingSummary = this.workingSummary;
|
|
417
|
+
if (workingSummary) {
|
|
418
|
+
totalTokens += workingSummary.tokenCount;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// L3: Long-term memory (keyword search)
|
|
422
|
+
let longTermEntries: MemoryEntry[] = [];
|
|
423
|
+
if (query) {
|
|
424
|
+
longTermEntries = this.retrieveLongTerm({
|
|
425
|
+
query,
|
|
426
|
+
maxEntries: this.l3MaxEntries,
|
|
427
|
+
maxTokens: this.totalTokenBudget * 0.3,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
totalTokens += longTermEntries.reduce((sum, e) => sum + e.tokenCount, 0);
|
|
431
|
+
|
|
432
|
+
// L4: Archival memory (only if we have budget remaining)
|
|
433
|
+
let archivalEntries: MemoryEntry[] = [];
|
|
434
|
+
if (query && totalTokens < this.totalTokenBudget * 0.8) {
|
|
435
|
+
archivalEntries = await this.retrieveArchival({
|
|
436
|
+
query,
|
|
437
|
+
maxEntries: this.l4MaxRetrieveEntries,
|
|
438
|
+
maxTokens: this.totalTokenBudget - totalTokens,
|
|
439
|
+
minScore: 0.5, // Higher threshold for archival
|
|
440
|
+
});
|
|
441
|
+
totalTokens += archivalEntries.reduce((sum, e) => sum + e.tokenCount, 0);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
immediate,
|
|
446
|
+
workingSummary,
|
|
447
|
+
longTermEntries,
|
|
448
|
+
archivalEntries,
|
|
449
|
+
totalTokens,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Build a system prompt section from loaded memory context.
|
|
455
|
+
*/
|
|
456
|
+
buildMemoryPrompt(context: MemoryContext): string {
|
|
457
|
+
const parts: string[] = [];
|
|
458
|
+
|
|
459
|
+
if (context.workingSummary) {
|
|
460
|
+
parts.push(this.compressor.renderSummaryToText(context.workingSummary));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (context.longTermEntries.length > 0) {
|
|
464
|
+
parts.push('<long-term-memory>');
|
|
465
|
+
for (const entry of context.longTermEntries) {
|
|
466
|
+
const tagStr = entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : '';
|
|
467
|
+
parts.push(`- [${entry.category}]${tagStr} ${entry.content}`);
|
|
468
|
+
}
|
|
469
|
+
parts.push('</long-term-memory>');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (context.archivalEntries.length > 0) {
|
|
473
|
+
parts.push('<archival-context>');
|
|
474
|
+
for (const entry of context.archivalEntries) {
|
|
475
|
+
parts.push(`- ${entry.content}`);
|
|
476
|
+
}
|
|
477
|
+
parts.push('</archival-context>');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return parts.join('\n\n');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ========================================================================
|
|
484
|
+
// Persistence
|
|
485
|
+
// ========================================================================
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Persist all memory layers to disk.
|
|
489
|
+
*/
|
|
490
|
+
async persist(): Promise<void> {
|
|
491
|
+
if (!this.dirty) return;
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
await fs.mkdir(this.storageDir, { recursive: true });
|
|
495
|
+
|
|
496
|
+
// Persist L2 working summary
|
|
497
|
+
if (this.workingSummary) {
|
|
498
|
+
const summaryPath = path.join(this.storageDir, 'working-summary.json');
|
|
499
|
+
await fs.writeFile(summaryPath, JSON.stringify(this.workingSummary, null, 2), 'utf-8');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Persist L3 long-term memory
|
|
503
|
+
if (this.longTermMemory.size > 0) {
|
|
504
|
+
const ltPath = path.join(this.storageDir, 'long-term.json');
|
|
505
|
+
const entries = Array.from(this.longTermMemory.values());
|
|
506
|
+
await fs.writeFile(ltPath, JSON.stringify(entries, null, 2), 'utf-8');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Persist L4 archival index
|
|
510
|
+
if (this.archivalIndex.size > 0) {
|
|
511
|
+
const archPath = path.join(this.storageDir, 'archival-index.json');
|
|
512
|
+
const entries = Array.from(this.archivalIndex.values());
|
|
513
|
+
await fs.writeFile(archPath, JSON.stringify(entries, null, 2), 'utf-8');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.dirty = false;
|
|
517
|
+
} catch (err) {
|
|
518
|
+
// Memory persistence is best-effort, never block
|
|
519
|
+
console.warn(`Memory persistence warning: ${(err as Error).message}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Restore memory from disk.
|
|
525
|
+
*/
|
|
526
|
+
async restore(): Promise<void> {
|
|
527
|
+
try {
|
|
528
|
+
// Restore L2 working summary
|
|
529
|
+
const summaryPath = path.join(this.storageDir, 'working-summary.json');
|
|
530
|
+
try {
|
|
531
|
+
const data = await fs.readFile(summaryPath, 'utf-8');
|
|
532
|
+
this.workingSummary = JSON.parse(data);
|
|
533
|
+
} catch {
|
|
534
|
+
// No summary file, that's OK
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Restore L3 long-term memory
|
|
538
|
+
const ltPath = path.join(this.storageDir, 'long-term.json');
|
|
539
|
+
try {
|
|
540
|
+
const data = await fs.readFile(ltPath, 'utf-8');
|
|
541
|
+
const entries: MemoryEntry[] = JSON.parse(data);
|
|
542
|
+
for (const entry of entries) {
|
|
543
|
+
this.longTermMemory.set(entry.id, entry);
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// No long-term memory, that's OK
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Restore L4 archival index
|
|
550
|
+
const archPath = path.join(this.storageDir, 'archival-index.json');
|
|
551
|
+
try {
|
|
552
|
+
const data = await fs.readFile(archPath, 'utf-8');
|
|
553
|
+
const entries: MemoryEntry[] = JSON.parse(data);
|
|
554
|
+
for (const entry of entries) {
|
|
555
|
+
this.archivalIndex.set(entry.id, entry);
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
// No archival memory, that's OK
|
|
559
|
+
}
|
|
560
|
+
} catch (err) {
|
|
561
|
+
console.warn(`Memory restore warning: ${(err as Error).message}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ========================================================================
|
|
566
|
+
// Helpers
|
|
567
|
+
// ========================================================================
|
|
568
|
+
|
|
569
|
+
private async ensureStorageDir(): Promise<void> {
|
|
570
|
+
try {
|
|
571
|
+
await fs.mkdir(this.storageDir, { recursive: true });
|
|
572
|
+
} catch {
|
|
573
|
+
// Ignore
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private async persistArchivalEntry(entry: MemoryEntry): Promise<void> {
|
|
578
|
+
try {
|
|
579
|
+
const entryPath = path.join(this.storageDir, 'archival', `${entry.id}.json`);
|
|
580
|
+
await fs.mkdir(path.dirname(entryPath), { recursive: true });
|
|
581
|
+
await fs.writeFile(entryPath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
582
|
+
} catch {
|
|
583
|
+
// Best-effort
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private trimSummary(summary: StructuredSummary, maxTokens: number): StructuredSummary {
|
|
588
|
+
// Trim each section to fit within budget
|
|
589
|
+
const entries = [
|
|
590
|
+
...summary.fileModifications.slice(0, 10),
|
|
591
|
+
...summary.decisions.slice(0, 5),
|
|
592
|
+
...summary.nextSteps.slice(0, 5),
|
|
593
|
+
];
|
|
594
|
+
|
|
595
|
+
let text = `Session goal: ${summary.sessionIntent}\n`;
|
|
596
|
+
text += `Completed: ${summary.completedActions.slice(0, 10).join('; ')}\n`;
|
|
597
|
+
text += `Files: ${summary.fileModifications.slice(0, 10).map((f) => `${f.action} ${f.path}`).join('; ')}`;
|
|
598
|
+
|
|
599
|
+
if (this.estimateTokens(text) > maxTokens) {
|
|
600
|
+
// Just truncate the intent and keep only the most essential info
|
|
601
|
+
text = `Session goal: ${summary.sessionIntent.slice(0, 100)}\n`;
|
|
602
|
+
text += `Files changed: ${summary.fileModifications.length}, Decisions: ${summary.decisions.length}`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
...summary,
|
|
607
|
+
fileModifications: summary.fileModifications.slice(0, 10),
|
|
608
|
+
decisions: summary.decisions.slice(0, 5),
|
|
609
|
+
nextSteps: summary.nextSteps.slice(0, 5),
|
|
610
|
+
completedActions: summary.completedActions.slice(0, 10),
|
|
611
|
+
tokenCount: this.estimateTokens(text),
|
|
612
|
+
lastUpdated: Date.now(),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private estimateTokens(text: string): number {
|
|
617
|
+
return Math.ceil(text.length / 4);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private estimateMessagesTokens(messages: Message[]): number {
|
|
621
|
+
let total = 0;
|
|
622
|
+
for (const msg of messages) {
|
|
623
|
+
for (const block of msg.content) {
|
|
624
|
+
if (block.type === 'text') {
|
|
625
|
+
total += this.estimateTokens(block.text);
|
|
626
|
+
} else if (block.type === 'tool_use') {
|
|
627
|
+
total += this.estimateTokens(JSON.stringify(block.input));
|
|
628
|
+
} else if (block.type === 'tool_result') {
|
|
629
|
+
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
630
|
+
total += this.estimateTokens(content);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return total;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ========================================================================
|
|
638
|
+
// Memory Statistics
|
|
639
|
+
// ========================================================================
|
|
640
|
+
|
|
641
|
+
getStats(): {
|
|
642
|
+
l1Count: number;
|
|
643
|
+
l2Tokens: number;
|
|
644
|
+
l3Count: number;
|
|
645
|
+
l4Count: number;
|
|
646
|
+
totalTokens: number;
|
|
647
|
+
} {
|
|
648
|
+
return {
|
|
649
|
+
l1Count: this.immediateBuffer.size,
|
|
650
|
+
l2Tokens: this.workingSummary?.tokenCount || 0,
|
|
651
|
+
l3Count: this.longTermMemory.size,
|
|
652
|
+
l4Count: this.archivalIndex.size,
|
|
653
|
+
totalTokens: this.workingSummary?.tokenCount || 0 +
|
|
654
|
+
Array.from(this.longTermMemory.values()).reduce((s, e) => s + e.tokenCount, 0),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MemoryDiscovery.d.ts","sourceRoot":"","sources":["MemoryDiscovery.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC;CACtD;AAED,MAAM,WAAW,qBAAqB;IACpC,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAeD,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,KAAK,CAAiC;gBAElC,MAAM,EAAE,qBAAqB;IAKzC,+CAA+C;IACzC,QAAQ,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IA2DvC,iCAAiC;IAC3B,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAmB3D,sBAAsB;IACtB,UAAU,IAAI,IAAI;IAIlB,sDAAsD;IACtD,OAAO,CAAC,iBAAiB;IAuBzB,+BAA+B;IAC/B,OAAO,CAAC,UAAU;CAenB"}
|