orquesta-cli 0.1.27 → 0.2.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/dist/agents/planner/index.d.ts +2 -1
- package/dist/agents/planner/index.js +7 -1
- package/dist/core/config/config-manager.d.ts +11 -1
- package/dist/core/config/config-manager.js +60 -0
- package/dist/core/config/providers.js +31 -0
- package/dist/core/git-auto-updater.d.ts +58 -0
- package/dist/core/git-auto-updater.js +374 -0
- package/dist/core/llm/llm-client.d.ts +1 -0
- package/dist/core/llm/llm-client.js +2 -0
- package/dist/core/pricing.js +2 -0
- package/dist/core/slash-command-handler.js +139 -0
- package/dist/orchestration/audit-log.d.ts +40 -0
- package/dist/orchestration/audit-log.js +156 -0
- package/dist/orchestration/index.d.ts +3 -0
- package/dist/orchestration/index.js +3 -0
- package/dist/orchestration/memory-store.d.ts +21 -0
- package/dist/orchestration/memory-store.js +102 -0
- package/dist/orchestration/parallel-orchestrator.d.ts +22 -0
- package/dist/orchestration/parallel-orchestrator.js +234 -0
- package/dist/orchestration/plan-executor.d.ts +1 -0
- package/dist/orchestration/plan-executor.js +156 -15
- package/dist/orchestration/worktree-manager.d.ts +25 -0
- package/dist/orchestration/worktree-manager.js +124 -0
- package/dist/tools/llm/simple/planning-tools.js +16 -2
- package/dist/types/index.d.ts +16 -0
- package/dist/ui/components/TodoListView.js +53 -15
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { logger } from '../utils/logger.js';
|
|
|
4
4
|
import { fullSync } from '../orquesta/config-sync.js';
|
|
5
5
|
import { configManager } from './config/config-manager.js';
|
|
6
6
|
import { getForcedTier, setForcedTier, resetBatutaSession } from './routing-state.js';
|
|
7
|
+
import { auditLog } from '../orchestration/audit-log.js';
|
|
7
8
|
export async function executeSlashCommand(command, context) {
|
|
8
9
|
const trimmedCommand = command.trim();
|
|
9
10
|
logger.enter('executeSlashCommand', { command: trimmedCommand });
|
|
@@ -169,6 +170,144 @@ export async function executeSlashCommand(command, context) {
|
|
|
169
170
|
updatedContext: { messages: updatedMessages },
|
|
170
171
|
};
|
|
171
172
|
}
|
|
173
|
+
if (trimmedCommand === '/role' || trimmedCommand.startsWith('/role ')) {
|
|
174
|
+
const args = trimmedCommand.slice('/role'.length).trim().split(/\s+/).filter(Boolean);
|
|
175
|
+
const currentModelId = configManager.getConfig().currentModel ?? 'none';
|
|
176
|
+
const formatStatus = () => {
|
|
177
|
+
const roles = ['planner', 'executor', 'refiner'];
|
|
178
|
+
const lines = roles.map(r => {
|
|
179
|
+
const override = configManager.getOrchestrationConfig().roleModels?.[r];
|
|
180
|
+
const effective = override ?? currentModelId;
|
|
181
|
+
const suffix = override ? '' : ' (fallback to currentModel)';
|
|
182
|
+
return ` ${r.padEnd(10)} → ${effective}${suffix}`;
|
|
183
|
+
});
|
|
184
|
+
const parallel = configManager.getMaxParallelWorkers();
|
|
185
|
+
const refiner = configManager.isRefinerEnabled() ? 'on' : 'off';
|
|
186
|
+
const worktree = configManager.isWorktreeIsolationEnabled() ? 'on' : 'off';
|
|
187
|
+
return `Multi-role orchestration\n\n${lines.join('\n')}\n\n max parallel workers: ${parallel}\n refiner pass: ${refiner}\n worktree isolation: ${worktree}\n\nUsage:\n /role planner <model-id>\n /role executor <model-id>\n /role refiner <model-id> | off\n /role parallel <N>\n /role worktree on|off\n /role clear`;
|
|
188
|
+
};
|
|
189
|
+
let body;
|
|
190
|
+
try {
|
|
191
|
+
if (args.length === 0) {
|
|
192
|
+
body = formatStatus();
|
|
193
|
+
}
|
|
194
|
+
else if (args[0] === 'clear') {
|
|
195
|
+
await configManager.clearRoleModels();
|
|
196
|
+
body = 'Role pins cleared. Every role falls back to currentModel.';
|
|
197
|
+
}
|
|
198
|
+
else if (args[0] === 'parallel' && args[1]) {
|
|
199
|
+
const n = parseInt(args[1], 10);
|
|
200
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
201
|
+
body = 'parallel must be an integer >= 1';
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
await configManager.setMaxParallelWorkers(n);
|
|
205
|
+
body = `Max parallel workers set to ${configManager.getMaxParallelWorkers()}.`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (args[0] === 'worktree' && (args[1] === 'on' || args[1] === 'off')) {
|
|
209
|
+
await configManager.setWorktreeIsolationEnabled(args[1] === 'on');
|
|
210
|
+
body = `Worktree isolation: ${args[1]}.`;
|
|
211
|
+
}
|
|
212
|
+
else if ((args[0] === 'planner' || args[0] === 'executor' || args[0] === 'refiner') && args[1]) {
|
|
213
|
+
const role = args[0];
|
|
214
|
+
if (role === 'refiner' && args[1] === 'off') {
|
|
215
|
+
await configManager.setRefinerEnabled(false);
|
|
216
|
+
body = 'Refiner pass disabled.';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await configManager.setRoleModel(role, args[1]);
|
|
220
|
+
if (role === 'refiner') {
|
|
221
|
+
await configManager.setRefinerEnabled(true);
|
|
222
|
+
body = `Refiner pinned to ${args[1]} and refiner pass enabled.`;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
body = `${role} pinned to ${args[1]}.`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
body = `Unknown /role usage. Run /role with no args for help.`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
body = `Error: ${error.message}`;
|
|
235
|
+
}
|
|
236
|
+
const updatedMessages = [
|
|
237
|
+
...context.messages,
|
|
238
|
+
{ role: 'assistant', content: body },
|
|
239
|
+
];
|
|
240
|
+
context.setMessages(updatedMessages);
|
|
241
|
+
return {
|
|
242
|
+
handled: true,
|
|
243
|
+
shouldContinue: false,
|
|
244
|
+
updatedContext: { messages: updatedMessages },
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (trimmedCommand === '/audit' || trimmedCommand.startsWith('/audit ')) {
|
|
248
|
+
const args = trimmedCommand.slice('/audit'.length).trim().split(/\s+/).filter(Boolean);
|
|
249
|
+
let body;
|
|
250
|
+
try {
|
|
251
|
+
if (args[0] === 'tail') {
|
|
252
|
+
const n = Number(args[1]) || 20;
|
|
253
|
+
const events = await auditLog.tail(n);
|
|
254
|
+
if (events.length === 0) {
|
|
255
|
+
body = '(no audit events yet)';
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
body = `Last ${events.length} audit events:\n\n` + events
|
|
259
|
+
.map(e => `[${e.timestamp.slice(11, 19)}] ${e.kind.padEnd(22)} ${JSON.stringify(e.data).slice(0, 140)}`)
|
|
260
|
+
.join('\n');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (args[0] === 'clear') {
|
|
264
|
+
await auditLog.clear();
|
|
265
|
+
body = 'Audit log cleared.';
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const sinceDays = args[0] === 'day' ? 1 : args[0] === 'week' ? 7 : undefined;
|
|
269
|
+
const s = await auditLog.stats({ sinceDays });
|
|
270
|
+
const period = sinceDays ? `last ${sinceDays}d` : 'lifetime';
|
|
271
|
+
const plannerLines = Object.entries(s.byPlannerModel)
|
|
272
|
+
.map(([m, c]) => ` ${m}: ${c}`).join('\n') || ' (none)';
|
|
273
|
+
const executorLines = Object.entries(s.byExecutorModel)
|
|
274
|
+
.map(([m, c]) => ` ${m}: ${c}`).join('\n') || ' (none)';
|
|
275
|
+
body = `Orchestration audit (${period})
|
|
276
|
+
|
|
277
|
+
runs: ${s.runs}
|
|
278
|
+
parallel runs: ${s.parallelRuns}
|
|
279
|
+
sequential runs: ${s.sequentialRuns}
|
|
280
|
+
total workers: ${s.totalWorkers}
|
|
281
|
+
worker failures: ${s.workerFailures}
|
|
282
|
+
avg planner ms: ${s.avgPlannerMs}
|
|
283
|
+
avg worker ms: ${s.avgWorkerMs}
|
|
284
|
+
avg waves per run: ${s.avgWavesPerRun}
|
|
285
|
+
refiner runs: ${s.refinerRuns}
|
|
286
|
+
refiner rewrites: ${s.refinerRewrites}
|
|
287
|
+
|
|
288
|
+
by planner model:
|
|
289
|
+
${plannerLines}
|
|
290
|
+
|
|
291
|
+
by executor model:
|
|
292
|
+
${executorLines}
|
|
293
|
+
|
|
294
|
+
Subcommands: /audit | /audit day | /audit week | /audit tail [N] | /audit clear`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
body = `Error: ${error.message}`;
|
|
299
|
+
}
|
|
300
|
+
const updatedMessages = [
|
|
301
|
+
...context.messages,
|
|
302
|
+
{ role: 'assistant', content: body },
|
|
303
|
+
];
|
|
304
|
+
context.setMessages(updatedMessages);
|
|
305
|
+
return {
|
|
306
|
+
handled: true,
|
|
307
|
+
shouldContinue: false,
|
|
308
|
+
updatedContext: { messages: updatedMessages },
|
|
309
|
+
};
|
|
310
|
+
}
|
|
172
311
|
if (trimmedCommand === '/cost') {
|
|
173
312
|
const costMessage = usageTracker.formatCostDisplay();
|
|
174
313
|
const updatedMessages = [
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
declare const SCHEMA_VERSION = 1;
|
|
2
|
+
export type AuditEventKind = 'run.start' | 'planner.complete' | 'orchestrator.decision' | 'worker.start' | 'worker.complete' | 'wave.complete' | 'refiner.complete' | 'run.complete' | 'run.error';
|
|
3
|
+
export interface AuditEvent {
|
|
4
|
+
schema: typeof SCHEMA_VERSION;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
runId: string;
|
|
8
|
+
kind: AuditEventKind;
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
declare class AuditLogger {
|
|
12
|
+
private initialized;
|
|
13
|
+
private writeQueue;
|
|
14
|
+
private currentRunId;
|
|
15
|
+
startRun(sessionId: string, data?: Record<string, unknown>): string;
|
|
16
|
+
emit(sessionId: string, kind: AuditEventKind, data?: Record<string, unknown>): void;
|
|
17
|
+
private write;
|
|
18
|
+
tail(n?: number): Promise<AuditEvent[]>;
|
|
19
|
+
stats(opts?: {
|
|
20
|
+
sinceDays?: number;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
runs: number;
|
|
23
|
+
parallelRuns: number;
|
|
24
|
+
sequentialRuns: number;
|
|
25
|
+
totalWorkers: number;
|
|
26
|
+
workerFailures: number;
|
|
27
|
+
avgPlannerMs: number;
|
|
28
|
+
avgWorkerMs: number;
|
|
29
|
+
avgWavesPerRun: number;
|
|
30
|
+
byPlannerModel: Record<string, number>;
|
|
31
|
+
byExecutorModel: Record<string, number>;
|
|
32
|
+
refinerRuns: number;
|
|
33
|
+
refinerRewrites: number;
|
|
34
|
+
}>;
|
|
35
|
+
private readAll;
|
|
36
|
+
clear(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export declare const auditLog: AuditLogger;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=audit-log.d.ts.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { LOCAL_HOME_DIR } from '../constants.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
const AUDIT_LOG_PATH = path.join(LOCAL_HOME_DIR, 'audit.jsonl');
|
|
6
|
+
const SCHEMA_VERSION = 1;
|
|
7
|
+
class AuditLogger {
|
|
8
|
+
initialized = false;
|
|
9
|
+
writeQueue = Promise.resolve();
|
|
10
|
+
currentRunId = null;
|
|
11
|
+
startRun(sessionId, data = {}) {
|
|
12
|
+
this.currentRunId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
13
|
+
this.emit(sessionId, 'run.start', data);
|
|
14
|
+
return this.currentRunId;
|
|
15
|
+
}
|
|
16
|
+
emit(sessionId, kind, data = {}) {
|
|
17
|
+
const runId = (typeof data['runId'] === 'string' ? data['runId'] : null) ?? this.currentRunId ?? 'unknown';
|
|
18
|
+
const event = {
|
|
19
|
+
schema: SCHEMA_VERSION,
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
sessionId,
|
|
22
|
+
runId,
|
|
23
|
+
kind,
|
|
24
|
+
data,
|
|
25
|
+
};
|
|
26
|
+
this.writeQueue = this.writeQueue.then(() => this.write(event)).catch(() => undefined);
|
|
27
|
+
}
|
|
28
|
+
async write(event) {
|
|
29
|
+
try {
|
|
30
|
+
if (!this.initialized) {
|
|
31
|
+
await fs.mkdir(LOCAL_HOME_DIR, { recursive: true });
|
|
32
|
+
this.initialized = true;
|
|
33
|
+
}
|
|
34
|
+
await fs.appendFile(AUDIT_LOG_PATH, JSON.stringify(event) + '\n', 'utf8');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.warn('Audit log write failed', { error: error.message });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async tail(n = 50) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await fs.readFile(AUDIT_LOG_PATH, 'utf8');
|
|
43
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
44
|
+
const last = lines.slice(-n);
|
|
45
|
+
const events = [];
|
|
46
|
+
for (const line of last) {
|
|
47
|
+
try {
|
|
48
|
+
events.push(JSON.parse(line));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return events.reverse();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async stats(opts) {
|
|
60
|
+
const events = await this.readAll();
|
|
61
|
+
const cutoff = opts?.sinceDays ? Date.now() - opts.sinceDays * 86400_000 : 0;
|
|
62
|
+
const filtered = cutoff ? events.filter(e => Date.parse(e.timestamp) >= cutoff) : events;
|
|
63
|
+
const runs = new Set();
|
|
64
|
+
let parallelRuns = 0;
|
|
65
|
+
let sequentialRuns = 0;
|
|
66
|
+
let totalWorkers = 0;
|
|
67
|
+
let workerFailures = 0;
|
|
68
|
+
let plannerDurMs = 0;
|
|
69
|
+
let plannerCount = 0;
|
|
70
|
+
let workerDurMs = 0;
|
|
71
|
+
let workerCount = 0;
|
|
72
|
+
let waveCount = 0;
|
|
73
|
+
let refinerRuns = 0;
|
|
74
|
+
let refinerRewrites = 0;
|
|
75
|
+
const byPlannerModel = {};
|
|
76
|
+
const byExecutorModel = {};
|
|
77
|
+
for (const e of filtered) {
|
|
78
|
+
runs.add(e.runId);
|
|
79
|
+
const d = e.data;
|
|
80
|
+
if (e.kind === 'orchestrator.decision') {
|
|
81
|
+
if (d['parallel'])
|
|
82
|
+
parallelRuns++;
|
|
83
|
+
else
|
|
84
|
+
sequentialRuns++;
|
|
85
|
+
}
|
|
86
|
+
if (e.kind === 'planner.complete') {
|
|
87
|
+
plannerCount++;
|
|
88
|
+
const dur = d['durationMs'];
|
|
89
|
+
if (typeof dur === 'number')
|
|
90
|
+
plannerDurMs += dur;
|
|
91
|
+
const model = d['model'];
|
|
92
|
+
if (typeof model === 'string')
|
|
93
|
+
byPlannerModel[model] = (byPlannerModel[model] || 0) + 1;
|
|
94
|
+
}
|
|
95
|
+
if (e.kind === 'worker.complete') {
|
|
96
|
+
totalWorkers++;
|
|
97
|
+
if (d['success'] === false)
|
|
98
|
+
workerFailures++;
|
|
99
|
+
const dur = d['durationMs'];
|
|
100
|
+
if (typeof dur === 'number') {
|
|
101
|
+
workerDurMs += dur;
|
|
102
|
+
workerCount++;
|
|
103
|
+
}
|
|
104
|
+
const model = d['model'];
|
|
105
|
+
if (typeof model === 'string')
|
|
106
|
+
byExecutorModel[model] = (byExecutorModel[model] || 0) + 1;
|
|
107
|
+
}
|
|
108
|
+
if (e.kind === 'wave.complete')
|
|
109
|
+
waveCount++;
|
|
110
|
+
if (e.kind === 'refiner.complete') {
|
|
111
|
+
refinerRuns++;
|
|
112
|
+
if (d['rewrote'])
|
|
113
|
+
refinerRewrites++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
runs: runs.size,
|
|
118
|
+
parallelRuns,
|
|
119
|
+
sequentialRuns,
|
|
120
|
+
totalWorkers,
|
|
121
|
+
workerFailures,
|
|
122
|
+
avgPlannerMs: plannerCount ? Math.round(plannerDurMs / plannerCount) : 0,
|
|
123
|
+
avgWorkerMs: workerCount ? Math.round(workerDurMs / workerCount) : 0,
|
|
124
|
+
avgWavesPerRun: runs.size ? Number((waveCount / runs.size).toFixed(2)) : 0,
|
|
125
|
+
byPlannerModel,
|
|
126
|
+
byExecutorModel,
|
|
127
|
+
refinerRuns,
|
|
128
|
+
refinerRewrites,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async readAll() {
|
|
132
|
+
try {
|
|
133
|
+
const raw = await fs.readFile(AUDIT_LOG_PATH, 'utf8');
|
|
134
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
135
|
+
try {
|
|
136
|
+
return [JSON.parse(line)];
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async clear() {
|
|
148
|
+
try {
|
|
149
|
+
await fs.rm(AUDIT_LOG_PATH, { force: true });
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export const auditLog = new AuditLogger();
|
|
156
|
+
//# sourceMappingURL=audit-log.js.map
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export type { ExecutionPhase, PlanExecutionState, AskUserRequest, AskUserResponse, AskUserState, StateCallbacks, ExecutionContext, ExecutionResult, PlanExecutionActions, } from './types.js';
|
|
2
2
|
export { formatErrorMessage, buildTodoContext, areAllTodosCompleted, findActiveTodo, getTodoStats, } from './utils.js';
|
|
3
3
|
export { PlanExecutor, planExecutor } from './plan-executor.js';
|
|
4
|
+
export { WorktreeManager, worktreeManager, type WorktreeAllocation, type WorktreeBackend, type MergeResult, } from './worktree-manager.js';
|
|
5
|
+
export { MemoryStore, memoryStore, type MemoryEntry, } from './memory-store.js';
|
|
6
|
+
export { auditLog, type AuditEvent, type AuditEventKind, } from './audit-log.js';
|
|
4
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { formatErrorMessage, buildTodoContext, areAllTodosCompleted, findActiveTodo, getTodoStats, } from './utils.js';
|
|
2
2
|
export { PlanExecutor, planExecutor } from './plan-executor.js';
|
|
3
|
+
export { WorktreeManager, worktreeManager, } from './worktree-manager.js';
|
|
4
|
+
export { MemoryStore, memoryStore, } from './memory-store.js';
|
|
5
|
+
export { auditLog, } from './audit-log.js';
|
|
3
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MemoryEntry {
|
|
2
|
+
writtenAt: string;
|
|
3
|
+
source?: 'planner' | 'executor' | 'worker' | 'refiner' | 'user' | string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
}
|
|
6
|
+
export declare class MemoryStore {
|
|
7
|
+
private cache;
|
|
8
|
+
private pendingFlush;
|
|
9
|
+
private pendingLoad;
|
|
10
|
+
private filePath;
|
|
11
|
+
load(sessionId: string): Promise<void>;
|
|
12
|
+
get(sessionId: string, key: string): MemoryEntry | null;
|
|
13
|
+
all(sessionId: string): Record<string, MemoryEntry>;
|
|
14
|
+
bySource(sessionId: string, source: MemoryEntry['source']): MemoryEntry[];
|
|
15
|
+
set(sessionId: string, key: string, value: unknown, source?: MemoryEntry['source']): Promise<void>;
|
|
16
|
+
delete(sessionId: string, key: string): Promise<void>;
|
|
17
|
+
clear(sessionId: string): Promise<void>;
|
|
18
|
+
private scheduleFlush;
|
|
19
|
+
}
|
|
20
|
+
export declare const memoryStore: MemoryStore;
|
|
21
|
+
//# sourceMappingURL=memory-store.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { LOCAL_HOME_DIR } from '../constants.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
export class MemoryStore {
|
|
6
|
+
cache = new Map();
|
|
7
|
+
pendingFlush = new Map();
|
|
8
|
+
pendingLoad = new Map();
|
|
9
|
+
filePath(sessionId) {
|
|
10
|
+
return path.join(LOCAL_HOME_DIR, 'sessions', sessionId, 'memory.json');
|
|
11
|
+
}
|
|
12
|
+
async load(sessionId) {
|
|
13
|
+
if (this.cache.has(sessionId))
|
|
14
|
+
return;
|
|
15
|
+
const inFlight = this.pendingLoad.get(sessionId);
|
|
16
|
+
if (inFlight)
|
|
17
|
+
return inFlight;
|
|
18
|
+
const loadPromise = (async () => {
|
|
19
|
+
const fp = this.filePath(sessionId);
|
|
20
|
+
try {
|
|
21
|
+
const raw = await fs.readFile(fp, 'utf8');
|
|
22
|
+
const obj = JSON.parse(raw);
|
|
23
|
+
if (!this.cache.has(sessionId)) {
|
|
24
|
+
this.cache.set(sessionId, new Map(Object.entries(obj)));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error.code;
|
|
29
|
+
if (code !== 'ENOENT') {
|
|
30
|
+
logger.warn('MemoryStore load failed; starting empty', { sessionId, error: error.message });
|
|
31
|
+
}
|
|
32
|
+
if (!this.cache.has(sessionId)) {
|
|
33
|
+
this.cache.set(sessionId, new Map());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
this.pendingLoad.delete(sessionId);
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
this.pendingLoad.set(sessionId, loadPromise);
|
|
41
|
+
return loadPromise;
|
|
42
|
+
}
|
|
43
|
+
get(sessionId, key) {
|
|
44
|
+
return this.cache.get(sessionId)?.get(key) ?? null;
|
|
45
|
+
}
|
|
46
|
+
all(sessionId) {
|
|
47
|
+
const map = this.cache.get(sessionId);
|
|
48
|
+
if (!map)
|
|
49
|
+
return {};
|
|
50
|
+
return Object.fromEntries(map);
|
|
51
|
+
}
|
|
52
|
+
bySource(sessionId, source) {
|
|
53
|
+
const map = this.cache.get(sessionId);
|
|
54
|
+
if (!map)
|
|
55
|
+
return [];
|
|
56
|
+
return Array.from(map.values()).filter(e => e.source === source);
|
|
57
|
+
}
|
|
58
|
+
async set(sessionId, key, value, source) {
|
|
59
|
+
if (!this.cache.has(sessionId))
|
|
60
|
+
await this.load(sessionId);
|
|
61
|
+
const map = this.cache.get(sessionId);
|
|
62
|
+
map.set(key, { writtenAt: new Date().toISOString(), source, value });
|
|
63
|
+
await this.scheduleFlush(sessionId);
|
|
64
|
+
}
|
|
65
|
+
async delete(sessionId, key) {
|
|
66
|
+
const map = this.cache.get(sessionId);
|
|
67
|
+
if (!map)
|
|
68
|
+
return;
|
|
69
|
+
map.delete(key);
|
|
70
|
+
await this.scheduleFlush(sessionId);
|
|
71
|
+
}
|
|
72
|
+
async clear(sessionId) {
|
|
73
|
+
this.cache.delete(sessionId);
|
|
74
|
+
const fp = this.filePath(sessionId);
|
|
75
|
+
await fs.rm(fp, { force: true }).catch(() => { });
|
|
76
|
+
}
|
|
77
|
+
async scheduleFlush(sessionId) {
|
|
78
|
+
const existing = this.pendingFlush.get(sessionId);
|
|
79
|
+
if (existing)
|
|
80
|
+
return existing;
|
|
81
|
+
const flushPromise = (async () => {
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
83
|
+
this.pendingFlush.delete(sessionId);
|
|
84
|
+
const map = this.cache.get(sessionId);
|
|
85
|
+
if (!map)
|
|
86
|
+
return;
|
|
87
|
+
const fp = this.filePath(sessionId);
|
|
88
|
+
try {
|
|
89
|
+
await fs.mkdir(path.dirname(fp), { recursive: true });
|
|
90
|
+
const obj = Object.fromEntries(map);
|
|
91
|
+
await fs.writeFile(fp, JSON.stringify(obj, null, 2), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.warn('MemoryStore flush failed', { sessionId, error: error.message });
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
this.pendingFlush.set(sessionId, flushPromise);
|
|
98
|
+
return flushPromise;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export const memoryStore = new MemoryStore();
|
|
102
|
+
//# sourceMappingURL=memory-store.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LLMClient } from '../core/llm/llm-client.js';
|
|
2
|
+
import { Message, TodoItem } from '../types/index.js';
|
|
3
|
+
import type { StateCallbacks } from './types.js';
|
|
4
|
+
export interface ParallelRunResult {
|
|
5
|
+
newMessages: Message[];
|
|
6
|
+
todos: TodoItem[];
|
|
7
|
+
workerOutputs: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare function shouldUseParallelOrchestrator(todos: TodoItem[]): boolean;
|
|
10
|
+
export declare function planWaves(todos: TodoItem[]): TodoItem[][];
|
|
11
|
+
export interface RunGraphOptions {
|
|
12
|
+
llmClient: LLMClient;
|
|
13
|
+
todos: TodoItem[];
|
|
14
|
+
baseSystemPrompt: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
callbacks: StateCallbacks;
|
|
17
|
+
isInterruptedRef: {
|
|
18
|
+
current: boolean;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function runParallelGraph(opts: RunGraphOptions): Promise<ParallelRunResult>;
|
|
22
|
+
//# sourceMappingURL=parallel-orchestrator.d.ts.map
|