@loxia-labs/loxia-autopilot-one 1.0.1
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/LICENSE +267 -0
- package/README.md +509 -0
- package/bin/cli.js +117 -0
- package/package.json +94 -0
- package/scripts/install-scanners.js +236 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +266 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +203 -0
- package/src/analyzers/codeCloneDetector/index.js +160 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +59 -0
- package/src/core/agentPool.js +1474 -0
- package/src/core/agentScheduler.js +2147 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/messageProcessor.js +732 -0
- package/src/core/orchestrator.js +548 -0
- package/src/core/stateManager.js +877 -0
- package/src/index.js +631 -0
- package/src/interfaces/cli.js +549 -0
- package/src/interfaces/webServer.js +2162 -0
- package/src/modules/fileExplorer/controller.js +280 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +125 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/aiService.js +1232 -0
- package/src/services/apiKeyManager.js +164 -0
- package/src/services/benchmarkService.js +366 -0
- package/src/services/budgetService.js +539 -0
- package/src/services/contextInjectionService.js +247 -0
- package/src/services/conversationCompactionService.js +637 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +544 -0
- package/src/services/modelRouterService.js +366 -0
- package/src/services/modelsService.js +322 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/tokenCountingService.js +536 -0
- package/src/tools/agentCommunicationTool.js +1344 -0
- package/src/tools/agentDelayTool.js +485 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +800 -0
- package/src/tools/browserTool.js +920 -0
- package/src/tools/cloneDetectionTool.js +621 -0
- package/src/tools/dependencyResolverTool.js +1215 -0
- package/src/tools/fileContentReplaceTool.js +875 -0
- package/src/tools/fileSystemTool.js +1107 -0
- package/src/tools/fileTreeTool.js +853 -0
- package/src/tools/imageTool.js +901 -0
- package/src/tools/importAnalyzerTool.js +1060 -0
- package/src/tools/jobDoneTool.js +248 -0
- package/src/tools/seekTool.js +956 -0
- package/src/tools/staticAnalysisTool.js +1778 -0
- package/src/tools/taskManagerTool.js +2873 -0
- package/src/tools/terminalTool.js +2304 -0
- package/src/tools/webTool.js +1430 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +292 -0
- package/src/utilities/configManager.js +582 -0
- package/src/utilities/constants.js +722 -0
- package/src/utilities/directoryAccessManager.js +535 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/tagParser.js +1246 -0
- package/src/utilities/toolConstants.js +317 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-CjkkcnFA.js +344 -0
- package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateManager - Handles state persistence, recovery, and project state management
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Project state persistence and recovery
|
|
6
|
+
* - Agent state management across sessions
|
|
7
|
+
* - Multi-model conversation state handling
|
|
8
|
+
* - Context reference state management
|
|
9
|
+
* - Session recovery and resume functionality
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
class StateManager {
|
|
16
|
+
constructor(config, logger) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
|
|
20
|
+
this.stateDirectory = config.system?.stateDirectory || '.loxia-state';
|
|
21
|
+
this.stateVersion = '1.0.0';
|
|
22
|
+
|
|
23
|
+
// State file paths
|
|
24
|
+
this.stateFiles = {
|
|
25
|
+
projectState: 'project-state.json',
|
|
26
|
+
agentIndex: 'agent-index.json',
|
|
27
|
+
conversationIndex: 'conversation-index.json',
|
|
28
|
+
lastSession: 'last-session.json',
|
|
29
|
+
contextReferences: 'context-references.json',
|
|
30
|
+
asyncOperations: 'operations/async-operations.json',
|
|
31
|
+
pausedAgents: 'operations/paused-agents.json',
|
|
32
|
+
toolHistory: 'operations/tool-history.json',
|
|
33
|
+
modelRouterCache: 'models/model-router-cache.json',
|
|
34
|
+
errorRecoveryLog: 'models/error-recovery-log.json'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize state directory structure
|
|
40
|
+
* @param {string} projectDir - Project directory path
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async initializeStateDirectory(projectDir) {
|
|
44
|
+
const stateDir = path.join(projectDir, this.stateDirectory);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Create main state directory
|
|
48
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
// Create subdirectories
|
|
51
|
+
await fs.mkdir(path.join(stateDir, 'agents'), { recursive: true });
|
|
52
|
+
await fs.mkdir(path.join(stateDir, 'operations'), { recursive: true });
|
|
53
|
+
await fs.mkdir(path.join(stateDir, 'models'), { recursive: true });
|
|
54
|
+
|
|
55
|
+
this.logger.info(`State directory initialized: ${stateDir}`);
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger.error(`Failed to initialize state directory: ${error.message}`);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resume project from saved state
|
|
65
|
+
* @param {string} projectDir - Project directory path
|
|
66
|
+
* @returns {Promise<Object>} Resumed project state
|
|
67
|
+
*/
|
|
68
|
+
async resumeProject(projectDir) {
|
|
69
|
+
try {
|
|
70
|
+
await this.initializeStateDirectory(projectDir);
|
|
71
|
+
|
|
72
|
+
// Load project state
|
|
73
|
+
const projectState = await this.loadProjectState(projectDir);
|
|
74
|
+
const agentIndex = await this.loadAgentIndex(projectDir);
|
|
75
|
+
|
|
76
|
+
// Restore agents with multi-model conversations
|
|
77
|
+
const restoredAgents = [];
|
|
78
|
+
for (const [agentId, agentInfo] of Object.entries(agentIndex)) {
|
|
79
|
+
try {
|
|
80
|
+
const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
|
|
81
|
+
restoredAgents.push(agent);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.logger.warn(`Failed to restore agent: ${agentId}`, error.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Restore async operations
|
|
88
|
+
const asyncOperations = await this.restoreAsyncOperations(projectDir);
|
|
89
|
+
|
|
90
|
+
// Restore paused agents
|
|
91
|
+
const pausedAgents = await this.restorePausedAgents(projectDir);
|
|
92
|
+
|
|
93
|
+
// Restore context references
|
|
94
|
+
const contextReferences = await this.restoreContextReferences(projectDir);
|
|
95
|
+
|
|
96
|
+
const resumedState = {
|
|
97
|
+
projectState,
|
|
98
|
+
agents: restoredAgents,
|
|
99
|
+
asyncOperations,
|
|
100
|
+
pausedAgents,
|
|
101
|
+
contextReferences,
|
|
102
|
+
resumedSuccessfully: true,
|
|
103
|
+
resumedAt: new Date().toISOString()
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Update last session
|
|
107
|
+
await this.saveLastSession(projectDir, {
|
|
108
|
+
resumedAt: new Date().toISOString(),
|
|
109
|
+
agentCount: restoredAgents.length,
|
|
110
|
+
operationCount: asyncOperations.length
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.logger.info(`Project resumed successfully`, {
|
|
114
|
+
projectDir,
|
|
115
|
+
agentCount: restoredAgents.length,
|
|
116
|
+
operationCount: asyncOperations.length
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return resumedState;
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
this.logger.error(`Project resume failed: ${error.message}`, {
|
|
123
|
+
projectDir,
|
|
124
|
+
error: error.stack
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
projectState: null,
|
|
129
|
+
agents: [],
|
|
130
|
+
asyncOperations: [],
|
|
131
|
+
pausedAgents: [],
|
|
132
|
+
contextReferences: [],
|
|
133
|
+
resumedSuccessfully: false,
|
|
134
|
+
error: error.message
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Persist agent state to storage
|
|
141
|
+
* @param {Object} agent - Agent object to persist
|
|
142
|
+
* @param {string} projectDir - Project directory path
|
|
143
|
+
* @returns {Promise<void>}
|
|
144
|
+
*/
|
|
145
|
+
async persistAgentState(agent, projectDir = process.cwd()) {
|
|
146
|
+
const stateDir = path.join(projectDir, this.stateDirectory);
|
|
147
|
+
const agentStateFile = path.join(stateDir, 'agents', `agent-${agent.id}-state.json`);
|
|
148
|
+
const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agent.id}-conversations.json`);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Separate conversations from main agent state
|
|
152
|
+
const { conversations, ...agentState } = agent;
|
|
153
|
+
|
|
154
|
+
// Save agent state
|
|
155
|
+
await this.saveJSON(agentStateFile, {
|
|
156
|
+
version: this.stateVersion,
|
|
157
|
+
agentId: agent.id,
|
|
158
|
+
state: agentState,
|
|
159
|
+
lastPersisted: new Date().toISOString()
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Save conversations separately
|
|
163
|
+
await this.saveJSON(agentConversationsFile, {
|
|
164
|
+
version: this.stateVersion,
|
|
165
|
+
agentId: agent.id,
|
|
166
|
+
conversations,
|
|
167
|
+
lastPersisted: new Date().toISOString()
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Update agent index
|
|
171
|
+
await this.updateAgentIndex(agent, projectDir);
|
|
172
|
+
|
|
173
|
+
this.logger.debug(`Agent state persisted: ${agent.id}`);
|
|
174
|
+
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.logger.error(`Failed to persist agent state: ${error.message}`, {
|
|
177
|
+
agentId: agent.id,
|
|
178
|
+
error: error.stack
|
|
179
|
+
});
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get project state
|
|
186
|
+
* @param {string} projectDir - Project directory path
|
|
187
|
+
* @returns {Promise<Object>} Project state object
|
|
188
|
+
*/
|
|
189
|
+
async getProjectState(projectDir) {
|
|
190
|
+
return await this.loadProjectState(projectDir);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Load project state from storage
|
|
195
|
+
* @param {string} projectDir - Project directory path
|
|
196
|
+
* @returns {Promise<Object>} Project state object
|
|
197
|
+
*/
|
|
198
|
+
async loadProjectState(projectDir) {
|
|
199
|
+
const stateFile = path.join(projectDir, this.stateDirectory, this.stateFiles.projectState);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const data = await this.loadJSON(stateFile);
|
|
203
|
+
return data;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// Return default project state if file doesn't exist
|
|
206
|
+
const defaultState = {
|
|
207
|
+
version: this.stateVersion,
|
|
208
|
+
projectDir,
|
|
209
|
+
createdAt: new Date().toISOString(),
|
|
210
|
+
lastModified: new Date().toISOString(),
|
|
211
|
+
activeAgents: [],
|
|
212
|
+
lastActiveSession: null,
|
|
213
|
+
configuration: {
|
|
214
|
+
defaultModel: this.config.system?.defaultModel || 'anthropic-sonnet',
|
|
215
|
+
allowedTools: ['terminal', 'filesystem', 'browser'],
|
|
216
|
+
budgetLimit: 100.00
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
await this.saveProjectState(projectDir, defaultState);
|
|
221
|
+
return defaultState;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Save project state to storage
|
|
227
|
+
* @param {string} projectDir - Project directory path
|
|
228
|
+
* @param {Object} projectState - Project state object
|
|
229
|
+
* @returns {Promise<void>}
|
|
230
|
+
*/
|
|
231
|
+
async saveProjectState(projectDir, projectState) {
|
|
232
|
+
const stateFile = path.join(projectDir, this.stateDirectory, this.stateFiles.projectState);
|
|
233
|
+
|
|
234
|
+
const stateData = {
|
|
235
|
+
...projectState,
|
|
236
|
+
lastModified: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
await this.saveJSON(stateFile, stateData);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Load agent index
|
|
244
|
+
* @param {string} projectDir - Project directory path
|
|
245
|
+
* @returns {Promise<Object>} Agent index object
|
|
246
|
+
*/
|
|
247
|
+
async loadAgentIndex(projectDir) {
|
|
248
|
+
const indexFile = path.join(projectDir, this.stateDirectory, this.stateFiles.agentIndex);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
return await this.loadJSON(indexFile);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return {}; // Return empty index if file doesn't exist
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Update agent index
|
|
259
|
+
* @param {Object} agent - Agent object
|
|
260
|
+
* @param {string} projectDir - Project directory path
|
|
261
|
+
* @returns {Promise<void>}
|
|
262
|
+
*/
|
|
263
|
+
async updateAgentIndex(agent, projectDir) {
|
|
264
|
+
const indexFile = path.join(projectDir, this.stateDirectory, this.stateFiles.agentIndex);
|
|
265
|
+
|
|
266
|
+
let agentIndex;
|
|
267
|
+
try {
|
|
268
|
+
agentIndex = await this.loadJSON(indexFile);
|
|
269
|
+
} catch {
|
|
270
|
+
agentIndex = {};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
agentIndex[agent.id] = {
|
|
274
|
+
name: agent.name,
|
|
275
|
+
type: agent.type,
|
|
276
|
+
stateFile: `agents/agent-${agent.id}-state.json`,
|
|
277
|
+
conversationsFile: `agents/agent-${agent.id}-conversations.json`,
|
|
278
|
+
lastActivity: agent.lastActivity,
|
|
279
|
+
model: agent.currentModel,
|
|
280
|
+
status: agent.status
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
await this.saveJSON(indexFile, agentIndex);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Restore agent from saved state
|
|
288
|
+
* @param {string} agentId - Agent identifier
|
|
289
|
+
* @param {Object} agentInfo - Agent info from index
|
|
290
|
+
* @param {string} projectDir - Project directory path
|
|
291
|
+
* @returns {Promise<Object>} Restored agent object
|
|
292
|
+
*/
|
|
293
|
+
async restoreAgent(agentId, agentInfo, projectDir) {
|
|
294
|
+
const stateDir = path.join(projectDir, this.stateDirectory);
|
|
295
|
+
const stateFile = path.join(stateDir, agentInfo.stateFile);
|
|
296
|
+
const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Load agent state
|
|
300
|
+
const stateData = await this.loadJSON(stateFile);
|
|
301
|
+
const conversationsData = await this.loadJSON(conversationsFile);
|
|
302
|
+
|
|
303
|
+
// Validate model conversations integrity
|
|
304
|
+
await this.validateModelConversations(conversationsData.conversations);
|
|
305
|
+
|
|
306
|
+
// Check if agent is paused
|
|
307
|
+
const pauseStatus = await this.checkAgentPauseStatus(agentId, projectDir);
|
|
308
|
+
|
|
309
|
+
const restoredAgent = {
|
|
310
|
+
...stateData.state,
|
|
311
|
+
conversations: conversationsData.conversations,
|
|
312
|
+
isPaused: pauseStatus.isPaused,
|
|
313
|
+
pausedUntil: pauseStatus.pausedUntil,
|
|
314
|
+
isRestored: true,
|
|
315
|
+
restoredAt: new Date().toISOString()
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
this.logger.info(`Agent restored: ${agentId}`, {
|
|
319
|
+
name: restoredAgent.name,
|
|
320
|
+
status: restoredAgent.status,
|
|
321
|
+
messageCount: restoredAgent.conversations?.full?.messages?.length || 0
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return restoredAgent;
|
|
325
|
+
|
|
326
|
+
} catch (error) {
|
|
327
|
+
this.logger.error(`Agent restoration failed: ${agentId}`, error.message);
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Restore async operations
|
|
334
|
+
* @param {string} projectDir - Project directory path
|
|
335
|
+
* @returns {Promise<Array>} Array of active async operations
|
|
336
|
+
*/
|
|
337
|
+
async restoreAsyncOperations(projectDir) {
|
|
338
|
+
const operationsFile = path.join(projectDir, this.stateDirectory, this.stateFiles.asyncOperations);
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const data = await this.loadJSON(operationsFile);
|
|
342
|
+
return data.operations || [];
|
|
343
|
+
} catch {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Restore paused agents
|
|
350
|
+
* @param {string} projectDir - Project directory path
|
|
351
|
+
* @returns {Promise<Object>} Paused agents data
|
|
352
|
+
*/
|
|
353
|
+
async restorePausedAgents(projectDir) {
|
|
354
|
+
const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const data = await this.loadJSON(pausedFile);
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
|
|
360
|
+
// Check which agents should be resumed
|
|
361
|
+
const toResume = [];
|
|
362
|
+
for (const [agentId, pauseInfo] of Object.entries(data.pausedAgents || {})) {
|
|
363
|
+
const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
|
|
364
|
+
|
|
365
|
+
if (now >= pausedUntil) {
|
|
366
|
+
toResume.push(agentId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Move expired pauses to history
|
|
371
|
+
for (const agentId of toResume) {
|
|
372
|
+
const pauseInfo = data.pausedAgents[agentId];
|
|
373
|
+
delete data.pausedAgents[agentId];
|
|
374
|
+
|
|
375
|
+
data.pauseHistory = data.pauseHistory || [];
|
|
376
|
+
data.pauseHistory.push({
|
|
377
|
+
agentId,
|
|
378
|
+
pausedAt: pauseInfo.pausedAt,
|
|
379
|
+
resumedAt: new Date().toISOString(),
|
|
380
|
+
reason: pauseInfo.reason,
|
|
381
|
+
actualDuration: Math.round((now - new Date(pauseInfo.pausedAt).getTime()) / 1000)
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Save updated data
|
|
386
|
+
await this.saveJSON(pausedFile, data);
|
|
387
|
+
|
|
388
|
+
return data;
|
|
389
|
+
|
|
390
|
+
} catch {
|
|
391
|
+
return {
|
|
392
|
+
pausedAgents: {},
|
|
393
|
+
pauseHistory: []
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Restore context references
|
|
400
|
+
* @param {string} projectDir - Project directory path
|
|
401
|
+
* @returns {Promise<Object>} Context references data
|
|
402
|
+
*/
|
|
403
|
+
async restoreContextReferences(projectDir) {
|
|
404
|
+
const contextFile = path.join(projectDir, this.stateDirectory, this.stateFiles.contextReferences);
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const data = await this.loadJSON(contextFile);
|
|
408
|
+
|
|
409
|
+
// Validate context references (implementation would validate file existence, etc.)
|
|
410
|
+
const validatedReferences = [];
|
|
411
|
+
for (const reference of data.references || []) {
|
|
412
|
+
// Add validation logic here
|
|
413
|
+
reference.isValid = true; // Placeholder
|
|
414
|
+
reference.lastValidated = new Date().toISOString();
|
|
415
|
+
validatedReferences.push(reference);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
data.references = validatedReferences;
|
|
419
|
+
await this.saveJSON(contextFile, data);
|
|
420
|
+
|
|
421
|
+
return data;
|
|
422
|
+
|
|
423
|
+
} catch {
|
|
424
|
+
return {
|
|
425
|
+
references: [],
|
|
426
|
+
lastCleanup: new Date().toISOString()
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Save last session data
|
|
433
|
+
* @param {string} projectDir - Project directory path
|
|
434
|
+
* @param {Object} sessionData - Session data to save
|
|
435
|
+
* @returns {Promise<void>}
|
|
436
|
+
*/
|
|
437
|
+
async saveLastSession(projectDir, sessionData) {
|
|
438
|
+
const sessionFile = path.join(projectDir, this.stateDirectory, this.stateFiles.lastSession);
|
|
439
|
+
|
|
440
|
+
const data = {
|
|
441
|
+
...sessionData,
|
|
442
|
+
savedAt: new Date().toISOString(),
|
|
443
|
+
projectDir
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
await this.saveJSON(sessionFile, data);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Load last session data
|
|
451
|
+
* @param {string} projectDir - Project directory path
|
|
452
|
+
* @returns {Promise<Object>} Last session data
|
|
453
|
+
*/
|
|
454
|
+
async loadLastSession(projectDir) {
|
|
455
|
+
const sessionFile = path.join(projectDir, this.stateDirectory, this.stateFiles.lastSession);
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
return await this.loadJSON(sessionFile);
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Save paused agent data
|
|
466
|
+
* @param {string} projectDir - Project directory path
|
|
467
|
+
* @param {string} agentId - Agent identifier
|
|
468
|
+
* @param {Object} pauseData - Pause information
|
|
469
|
+
* @returns {Promise<void>}
|
|
470
|
+
*/
|
|
471
|
+
async savePausedAgent(projectDir, agentId, pauseData) {
|
|
472
|
+
const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
|
|
473
|
+
|
|
474
|
+
let data;
|
|
475
|
+
try {
|
|
476
|
+
data = await this.loadJSON(pausedFile);
|
|
477
|
+
} catch {
|
|
478
|
+
data = { pausedAgents: {}, pauseHistory: [] };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
data.pausedAgents[agentId] = pauseData;
|
|
482
|
+
await this.saveJSON(pausedFile, data);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Remove paused agent data
|
|
487
|
+
* @param {string} projectDir - Project directory path
|
|
488
|
+
* @param {string} agentId - Agent identifier
|
|
489
|
+
* @returns {Promise<void>}
|
|
490
|
+
*/
|
|
491
|
+
async removePausedAgent(projectDir, agentId) {
|
|
492
|
+
const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const data = await this.loadJSON(pausedFile);
|
|
496
|
+
delete data.pausedAgents[agentId];
|
|
497
|
+
await this.saveJSON(pausedFile, data);
|
|
498
|
+
} catch {
|
|
499
|
+
// File doesn't exist, nothing to remove
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Check agent pause status
|
|
505
|
+
* @param {string} agentId - Agent identifier
|
|
506
|
+
* @param {string} projectDir - Project directory path
|
|
507
|
+
* @returns {Promise<Object>} Pause status
|
|
508
|
+
*/
|
|
509
|
+
async checkAgentPauseStatus(agentId, projectDir) {
|
|
510
|
+
const pausedFile = path.join(projectDir, this.stateDirectory, this.stateFiles.pausedAgents);
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const data = await this.loadJSON(pausedFile);
|
|
514
|
+
const pauseInfo = data.pausedAgents[agentId];
|
|
515
|
+
|
|
516
|
+
if (!pauseInfo) {
|
|
517
|
+
return { isPaused: false, pausedUntil: null };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const now = Date.now();
|
|
521
|
+
const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
isPaused: now < pausedUntil,
|
|
525
|
+
pausedUntil: pauseInfo.pausedUntil,
|
|
526
|
+
reason: pauseInfo.reason
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
} catch {
|
|
530
|
+
return { isPaused: false, pausedUntil: null };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Validate model conversations integrity
|
|
536
|
+
* @param {Object} conversations - Conversations object
|
|
537
|
+
* @returns {Promise<void>}
|
|
538
|
+
*/
|
|
539
|
+
async validateModelConversations(conversations) {
|
|
540
|
+
if (!conversations || !conversations.full) {
|
|
541
|
+
throw new Error('Invalid conversations structure - missing full conversation');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const fullMessages = conversations.full.messages || [];
|
|
545
|
+
const fullLastUpdated = new Date(conversations.full.lastUpdated);
|
|
546
|
+
|
|
547
|
+
// Validate each model conversation against full conversation
|
|
548
|
+
for (const [modelName, modelConv] of Object.entries(conversations)) {
|
|
549
|
+
if (modelName === 'full') continue;
|
|
550
|
+
|
|
551
|
+
if (!modelConv.messages) {
|
|
552
|
+
this.logger.warn(`Model conversation ${modelName} missing messages array`);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const modelLastUpdated = new Date(modelConv.lastUpdated);
|
|
557
|
+
|
|
558
|
+
if (fullLastUpdated > modelLastUpdated) {
|
|
559
|
+
this.logger.warn(`Model conversation ${modelName} is outdated, will sync on next use`);
|
|
560
|
+
modelConv.needsSync = true;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Save JSON data to file
|
|
567
|
+
* @private
|
|
568
|
+
*/
|
|
569
|
+
async saveJSON(filePath, data) {
|
|
570
|
+
const dir = path.dirname(filePath);
|
|
571
|
+
await fs.mkdir(dir, { recursive: true });
|
|
572
|
+
|
|
573
|
+
const jsonData = JSON.stringify(data, null, 2);
|
|
574
|
+
await fs.writeFile(filePath, jsonData, 'utf8');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Load JSON data from file
|
|
579
|
+
* @private
|
|
580
|
+
*/
|
|
581
|
+
async loadJSON(filePath) {
|
|
582
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
583
|
+
return JSON.parse(data);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Delete agent state from storage
|
|
588
|
+
* @param {string} agentId - Agent identifier
|
|
589
|
+
* @param {string} projectDir - Project directory path
|
|
590
|
+
* @returns {Promise<void>}
|
|
591
|
+
*/
|
|
592
|
+
async deleteAgentState(agentId, projectDir = process.cwd()) {
|
|
593
|
+
const stateDir = path.join(projectDir, this.stateDirectory);
|
|
594
|
+
const agentStateFile = path.join(stateDir, 'agents', `agent-${agentId}-state.json`);
|
|
595
|
+
const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agentId}-conversations.json`);
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
// Delete agent state file
|
|
599
|
+
try {
|
|
600
|
+
await fs.unlink(agentStateFile);
|
|
601
|
+
this.logger.debug(`Deleted agent state file: ${agentId}`);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (error.code !== 'ENOENT') {
|
|
604
|
+
this.logger.warn(`Failed to delete agent state file: ${error.message}`, { agentId });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Delete agent conversations file
|
|
609
|
+
try {
|
|
610
|
+
await fs.unlink(agentConversationsFile);
|
|
611
|
+
this.logger.debug(`Deleted agent conversations file: ${agentId}`);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (error.code !== 'ENOENT') {
|
|
614
|
+
this.logger.warn(`Failed to delete agent conversations file: ${error.message}`, { agentId });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Remove from agent index
|
|
619
|
+
await this.removeFromAgentIndex(agentId, projectDir);
|
|
620
|
+
|
|
621
|
+
this.logger.info(`Agent state deleted: ${agentId}`);
|
|
622
|
+
|
|
623
|
+
} catch (error) {
|
|
624
|
+
this.logger.error(`Failed to delete agent state: ${error.message}`, {
|
|
625
|
+
agentId,
|
|
626
|
+
error: error.stack
|
|
627
|
+
});
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Remove agent from agent index
|
|
634
|
+
* @param {string} agentId - Agent identifier
|
|
635
|
+
* @param {string} projectDir - Project directory path
|
|
636
|
+
* @returns {Promise<void>}
|
|
637
|
+
*/
|
|
638
|
+
async removeFromAgentIndex(agentId, projectDir) {
|
|
639
|
+
const indexFile = path.join(projectDir, this.stateDirectory, this.stateFiles.agentIndex);
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const agentIndex = await this.loadJSON(indexFile);
|
|
643
|
+
delete agentIndex[agentId];
|
|
644
|
+
await this.saveJSON(indexFile, agentIndex);
|
|
645
|
+
this.logger.debug(`Removed agent from index: ${agentId}`);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
// If index doesn't exist or can't be updated, log but don't throw
|
|
648
|
+
this.logger.warn(`Failed to remove agent from index: ${error.message}`, { agentId });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Check if state directory exists
|
|
654
|
+
* @param {string} projectDir - Project directory path
|
|
655
|
+
* @returns {Promise<boolean>} True if state directory exists
|
|
656
|
+
*/
|
|
657
|
+
async stateDirectoryExists(projectDir) {
|
|
658
|
+
const stateDir = path.join(projectDir, this.stateDirectory);
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
const stats = await fs.stat(stateDir);
|
|
662
|
+
return stats.isDirectory();
|
|
663
|
+
} catch {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Clean up old state files
|
|
670
|
+
* @param {string} projectDir - Project directory path
|
|
671
|
+
* @param {number} maxAge - Maximum age in days
|
|
672
|
+
* @returns {Promise<void>}
|
|
673
|
+
*/
|
|
674
|
+
async cleanupOldState(projectDir, maxAge = 30) {
|
|
675
|
+
const stateDir = path.join(projectDir, this.stateDirectory);
|
|
676
|
+
const cutoffDate = Date.now() - (maxAge * 24 * 60 * 60 * 1000);
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const agentsDir = path.join(stateDir, 'agents');
|
|
680
|
+
const files = await fs.readdir(agentsDir);
|
|
681
|
+
|
|
682
|
+
for (const file of files) {
|
|
683
|
+
const filePath = path.join(agentsDir, file);
|
|
684
|
+
const stats = await fs.stat(filePath);
|
|
685
|
+
|
|
686
|
+
if (stats.mtime.getTime() < cutoffDate) {
|
|
687
|
+
await fs.unlink(filePath);
|
|
688
|
+
this.logger.info(`Cleaned up old state file: ${file}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
} catch (error) {
|
|
693
|
+
this.logger.warn(`State cleanup failed: ${error.message}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Get all available agents (active + archived) from filesystem
|
|
699
|
+
* @param {string} projectDir - Project directory path
|
|
700
|
+
* @param {Object} agentPool - Agent pool instance to check active agents
|
|
701
|
+
* @returns {Promise<Array>} List of all agents with metadata
|
|
702
|
+
*/
|
|
703
|
+
async getAllAvailableAgents(projectDir, agentPool) {
|
|
704
|
+
try {
|
|
705
|
+
const agentIndex = await this.loadAgentIndex(projectDir);
|
|
706
|
+
const activeAgentIds = agentPool ? (await agentPool.getAllAgents()).map(a => a.id) : [];
|
|
707
|
+
|
|
708
|
+
const agents = [];
|
|
709
|
+
for (const [agentId, info] of Object.entries(agentIndex)) {
|
|
710
|
+
// Skip invalid or undefined entries
|
|
711
|
+
if (!info || !info.name) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
agents.push({
|
|
716
|
+
agentId,
|
|
717
|
+
name: info.name,
|
|
718
|
+
type: info.type,
|
|
719
|
+
model: info.model,
|
|
720
|
+
lastActivity: info.lastActivity,
|
|
721
|
+
status: info.status,
|
|
722
|
+
stateFile: info.stateFile,
|
|
723
|
+
conversationsFile: info.conversationsFile,
|
|
724
|
+
isLoaded: activeAgentIds.includes(agentId),
|
|
725
|
+
canImport: !activeAgentIds.includes(agentId)
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Sort by last activity (most recent first)
|
|
730
|
+
agents.sort((a, b) => {
|
|
731
|
+
const dateA = a.lastActivity ? new Date(a.lastActivity) : new Date(0);
|
|
732
|
+
const dateB = b.lastActivity ? new Date(b.lastActivity) : new Date(0);
|
|
733
|
+
return dateB - dateA;
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
this.logger.info(`Found ${agents.length} available agents (${agents.filter(a => a.isLoaded).length} active, ${agents.filter(a => !a.isLoaded).length} archived)`);
|
|
737
|
+
|
|
738
|
+
return agents;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
this.logger.error(`Failed to get available agents: ${error.message}`);
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get agent metadata without full restoration (lightweight preview)
|
|
747
|
+
* @param {string} agentId - Agent ID
|
|
748
|
+
* @param {string} projectDir - Project directory path
|
|
749
|
+
* @returns {Promise<Object>} Agent metadata for preview
|
|
750
|
+
*/
|
|
751
|
+
async getAgentMetadata(agentId, projectDir) {
|
|
752
|
+
try {
|
|
753
|
+
// Load agent index
|
|
754
|
+
const agentIndex = await this.loadAgentIndex(projectDir);
|
|
755
|
+
const agentInfo = agentIndex[agentId];
|
|
756
|
+
|
|
757
|
+
if (!agentInfo) {
|
|
758
|
+
throw new Error(`Agent ${agentId} not found in index`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Load just the state file (lightweight)
|
|
762
|
+
const stateDir = path.join(projectDir, this.stateDirectory);
|
|
763
|
+
const stateFile = path.join(stateDir, agentInfo.stateFile);
|
|
764
|
+
const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
|
|
765
|
+
|
|
766
|
+
// Check if files exist
|
|
767
|
+
const stateExists = await fs.access(stateFile).then(() => true).catch(() => false);
|
|
768
|
+
const conversationsExist = await fs.access(conversationsFile).then(() => true).catch(() => false);
|
|
769
|
+
|
|
770
|
+
if (!stateExists) {
|
|
771
|
+
throw new Error(`State file not found for agent ${agentId}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Load state
|
|
775
|
+
const stateData = await this.loadJSON(stateFile);
|
|
776
|
+
const state = stateData.state || {};
|
|
777
|
+
|
|
778
|
+
// Load conversation count without loading full messages (for performance)
|
|
779
|
+
let messageCount = 0;
|
|
780
|
+
let lastMessage = null;
|
|
781
|
+
if (conversationsExist) {
|
|
782
|
+
try {
|
|
783
|
+
const conversations = await this.loadJSON(conversationsFile);
|
|
784
|
+
messageCount = Array.isArray(conversations) ? conversations.length : 0;
|
|
785
|
+
|
|
786
|
+
// Get last message for preview
|
|
787
|
+
if (messageCount > 0) {
|
|
788
|
+
const lastMsg = conversations[conversations.length - 1];
|
|
789
|
+
lastMessage = lastMsg?.content?.substring(0, 100) || null;
|
|
790
|
+
}
|
|
791
|
+
} catch (error) {
|
|
792
|
+
this.logger.warn(`Failed to load conversations for ${agentId}: ${error.message}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const metadata = {
|
|
797
|
+
agentId,
|
|
798
|
+
name: agentInfo.name || state.name,
|
|
799
|
+
model: agentInfo.model || state.preferredModel || state.currentModel,
|
|
800
|
+
lastActivity: agentInfo.lastActivity,
|
|
801
|
+
status: agentInfo.status,
|
|
802
|
+
capabilities: state.capabilities || [],
|
|
803
|
+
messageCount,
|
|
804
|
+
lastMessage,
|
|
805
|
+
taskCount: state.taskList?.tasks?.length || 0,
|
|
806
|
+
createdAt: state.createdAt,
|
|
807
|
+
workingDirectory: state.directoryAccess?.workingDirectory,
|
|
808
|
+
mode: state.mode,
|
|
809
|
+
systemPrompt: state.originalSystemPrompt
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
this.logger.info(`Loaded metadata for agent ${agentId}: ${metadata.messageCount} messages, ${metadata.taskCount} tasks`);
|
|
813
|
+
|
|
814
|
+
return metadata;
|
|
815
|
+
} catch (error) {
|
|
816
|
+
this.logger.error(`Failed to get agent metadata for ${agentId}: ${error.message}`);
|
|
817
|
+
throw error;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Import archived agent from filesystem and add to agent pool
|
|
823
|
+
* @param {string} agentId - Agent ID to import
|
|
824
|
+
* @param {string} projectDir - Project directory path
|
|
825
|
+
* @param {Object} agentPool - Agent pool instance
|
|
826
|
+
* @returns {Promise<Object>} Imported agent object
|
|
827
|
+
*/
|
|
828
|
+
async importArchivedAgent(agentId, projectDir, agentPool) {
|
|
829
|
+
try {
|
|
830
|
+
// Validate agent ID format for security
|
|
831
|
+
const AGENT_ID_REGEX = /^agent-[a-z0-9-]+-\d+$/;
|
|
832
|
+
if (!AGENT_ID_REGEX.test(agentId)) {
|
|
833
|
+
throw new Error('Invalid agent ID format');
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Check if already loaded in agent pool
|
|
837
|
+
if (agentPool && await agentPool.getAgent(agentId)) {
|
|
838
|
+
throw new Error(`Agent ${agentId} is already loaded. Use switchAgent() instead.`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Load from agent index
|
|
842
|
+
const agentIndex = await this.loadAgentIndex(projectDir);
|
|
843
|
+
const agentInfo = agentIndex[agentId];
|
|
844
|
+
|
|
845
|
+
if (!agentInfo) {
|
|
846
|
+
throw new Error(`Agent ${agentId} not found in index`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
this.logger.info(`Importing archived agent: ${agentId} (${agentInfo.name})`);
|
|
850
|
+
|
|
851
|
+
// Restore agent using existing restore logic
|
|
852
|
+
const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
|
|
853
|
+
|
|
854
|
+
// Update agent's last activity
|
|
855
|
+
agent.lastActivity = new Date().toISOString();
|
|
856
|
+
|
|
857
|
+
// Add to agent pool if provided
|
|
858
|
+
if (agentPool) {
|
|
859
|
+
agentPool.agents.set(agent.id, agent);
|
|
860
|
+
agentPool._updateAgentDirectory(agent);
|
|
861
|
+
this.logger.info(`Agent ${agentId} added to agent pool`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Update agent index with new last activity
|
|
865
|
+
await this.updateAgentIndex(agent, projectDir);
|
|
866
|
+
|
|
867
|
+
this.logger.info(`Successfully imported agent ${agentId}: ${agent.name}`);
|
|
868
|
+
|
|
869
|
+
return agent;
|
|
870
|
+
} catch (error) {
|
|
871
|
+
this.logger.error(`Failed to import agent ${agentId}: ${error.message}`);
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export default StateManager;
|