orquesta-cli 0.1.27 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +16 -0
- package/dist/core/llm/llm-client.d.ts +1 -0
- package/dist/core/llm/llm-client.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
|
@@ -7,7 +7,8 @@ export interface PlanningWithDocsResult extends PlanningResult {
|
|
|
7
7
|
export declare class PlanningLLM {
|
|
8
8
|
private llmClient;
|
|
9
9
|
private askUserCallback;
|
|
10
|
-
|
|
10
|
+
private modelOverride?;
|
|
11
|
+
constructor(llmClient: LLMClient, modelOverride?: string);
|
|
11
12
|
setAskUserCallback(callback: AskUserCallback): void;
|
|
12
13
|
clearAskUserCallback(): void;
|
|
13
14
|
generateTODOList(userRequest: string, contextMessages?: Message[]): Promise<PlanningResult>;
|
|
@@ -5,8 +5,10 @@ import { getProjectContext } from '../../core/project-context.js';
|
|
|
5
5
|
export class PlanningLLM {
|
|
6
6
|
llmClient;
|
|
7
7
|
askUserCallback = null;
|
|
8
|
-
|
|
8
|
+
modelOverride;
|
|
9
|
+
constructor(llmClient, modelOverride) {
|
|
9
10
|
this.llmClient = llmClient;
|
|
11
|
+
this.modelOverride = modelOverride;
|
|
10
12
|
}
|
|
11
13
|
setAskUserCallback(callback) {
|
|
12
14
|
logger.flow('Setting ask-user callback for Planning LLM');
|
|
@@ -73,6 +75,7 @@ Choose one of your 3 tools now.`,
|
|
|
73
75
|
tool_choice: 'required',
|
|
74
76
|
temperature: 0.7,
|
|
75
77
|
max_tokens: 2000,
|
|
78
|
+
...(this.modelOverride ? { model: this.modelOverride } : {}),
|
|
76
79
|
});
|
|
77
80
|
const choicesCount = response.choices?.length ?? 0;
|
|
78
81
|
const message = response.choices?.[0]?.message;
|
|
@@ -174,6 +177,9 @@ Choose one of your 3 tools now.`,
|
|
|
174
177
|
id: todo.id || `todo-${Date.now()}-${index}`,
|
|
175
178
|
title: todo.title || 'Untitled task',
|
|
176
179
|
status: (index === 0 ? 'in_progress' : 'pending'),
|
|
180
|
+
...(Array.isArray(todo.dependsOn) && todo.dependsOn.length > 0 ? { dependsOn: todo.dependsOn } : {}),
|
|
181
|
+
...(typeof todo.requiresFilesystem === 'boolean' ? { requiresFilesystem: todo.requiresFilesystem } : {}),
|
|
182
|
+
...(typeof todo.parallelGroup === 'string' && todo.parallelGroup ? { parallelGroup: todo.parallelGroup } : {}),
|
|
177
183
|
}));
|
|
178
184
|
return {
|
|
179
185
|
todos,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OpenConfig, EndpointConfig, ModelInfo, OrquestaConfig } from '../../types/index.js';
|
|
1
|
+
import { OpenConfig, EndpointConfig, ModelInfo, OrquestaConfig, OrchestrationConfig, OrchestrationRole } from '../../types/index.js';
|
|
2
2
|
export declare class ConfigManager {
|
|
3
3
|
private config;
|
|
4
4
|
private initialized;
|
|
@@ -51,6 +51,16 @@ export declare class ConfigManager {
|
|
|
51
51
|
unchanged: number;
|
|
52
52
|
}>;
|
|
53
53
|
getLocalOnlyEndpoints(): EndpointConfig[];
|
|
54
|
+
getOrchestrationConfig(): OrchestrationConfig;
|
|
55
|
+
getRoleModel(role: OrchestrationRole): string | null;
|
|
56
|
+
setRoleModel(role: OrchestrationRole, modelId: string | null): Promise<void>;
|
|
57
|
+
clearRoleModels(): Promise<void>;
|
|
58
|
+
getMaxParallelWorkers(): number;
|
|
59
|
+
setMaxParallelWorkers(n: number): Promise<void>;
|
|
60
|
+
isRefinerEnabled(): boolean;
|
|
61
|
+
setRefinerEnabled(enabled: boolean): Promise<void>;
|
|
62
|
+
isWorktreeIsolationEnabled(): boolean;
|
|
63
|
+
setWorktreeIsolationEnabled(enabled: boolean): Promise<void>;
|
|
54
64
|
}
|
|
55
65
|
export declare const configManager: ConfigManager;
|
|
56
66
|
//# sourceMappingURL=config-manager.d.ts.map
|
|
@@ -337,6 +337,66 @@ export class ConfigManager {
|
|
|
337
337
|
const config = this.getConfig();
|
|
338
338
|
return config.endpoints;
|
|
339
339
|
}
|
|
340
|
+
getOrchestrationConfig() {
|
|
341
|
+
return this.config?.orchestration ?? {};
|
|
342
|
+
}
|
|
343
|
+
getRoleModel(role) {
|
|
344
|
+
const override = this.config?.orchestration?.roleModels?.[role];
|
|
345
|
+
if (override)
|
|
346
|
+
return override;
|
|
347
|
+
return this.config?.currentModel ?? null;
|
|
348
|
+
}
|
|
349
|
+
async setRoleModel(role, modelId) {
|
|
350
|
+
const config = this.getConfig();
|
|
351
|
+
if (!config.orchestration)
|
|
352
|
+
config.orchestration = {};
|
|
353
|
+
if (!config.orchestration.roleModels)
|
|
354
|
+
config.orchestration.roleModels = {};
|
|
355
|
+
if (modelId === null) {
|
|
356
|
+
delete config.orchestration.roleModels[role];
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
config.orchestration.roleModels[role] = modelId;
|
|
360
|
+
}
|
|
361
|
+
await this.saveConfig();
|
|
362
|
+
}
|
|
363
|
+
async clearRoleModels() {
|
|
364
|
+
const config = this.getConfig();
|
|
365
|
+
if (config.orchestration) {
|
|
366
|
+
config.orchestration.roleModels = {};
|
|
367
|
+
await this.saveConfig();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
getMaxParallelWorkers() {
|
|
371
|
+
return this.config?.orchestration?.maxParallelWorkers ?? 3;
|
|
372
|
+
}
|
|
373
|
+
async setMaxParallelWorkers(n) {
|
|
374
|
+
const config = this.getConfig();
|
|
375
|
+
if (!config.orchestration)
|
|
376
|
+
config.orchestration = {};
|
|
377
|
+
config.orchestration.maxParallelWorkers = Math.max(1, Math.min(10, n));
|
|
378
|
+
await this.saveConfig();
|
|
379
|
+
}
|
|
380
|
+
isRefinerEnabled() {
|
|
381
|
+
return this.config?.orchestration?.refinerEnabled ?? false;
|
|
382
|
+
}
|
|
383
|
+
async setRefinerEnabled(enabled) {
|
|
384
|
+
const config = this.getConfig();
|
|
385
|
+
if (!config.orchestration)
|
|
386
|
+
config.orchestration = {};
|
|
387
|
+
config.orchestration.refinerEnabled = enabled;
|
|
388
|
+
await this.saveConfig();
|
|
389
|
+
}
|
|
390
|
+
isWorktreeIsolationEnabled() {
|
|
391
|
+
return this.config?.orchestration?.worktreeIsolation ?? false;
|
|
392
|
+
}
|
|
393
|
+
async setWorktreeIsolationEnabled(enabled) {
|
|
394
|
+
const config = this.getConfig();
|
|
395
|
+
if (!config.orchestration)
|
|
396
|
+
config.orchestration = {};
|
|
397
|
+
config.orchestration.worktreeIsolation = enabled;
|
|
398
|
+
await this.saveConfig();
|
|
399
|
+
}
|
|
340
400
|
}
|
|
341
401
|
export const configManager = new ConfigManager();
|
|
342
402
|
//# sourceMappingURL=config-manager.js.map
|
|
@@ -36,6 +36,22 @@ export const PROVIDERS = [
|
|
|
36
36
|
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', maxTokens: 200000, capabilities: ['vision', 'tools', 'streaming'] },
|
|
37
37
|
],
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
id: 'anthropic-direct',
|
|
41
|
+
name: 'Anthropic (OpenAI-compat)',
|
|
42
|
+
baseUrl: 'https://api.anthropic.com/v1/openai',
|
|
43
|
+
authMethod: 'bearer',
|
|
44
|
+
envVars: ['ANTHROPIC_API_KEY'],
|
|
45
|
+
requiresApiKey: true,
|
|
46
|
+
isLocal: false,
|
|
47
|
+
modelsEndpoint: '/models',
|
|
48
|
+
openaiCompatible: true,
|
|
49
|
+
knownModels: [
|
|
50
|
+
{ id: 'claude-opus-4-7', name: 'Claude Opus 4.7', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
51
|
+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
52
|
+
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', maxTokens: 200000, capabilities: ['vision', 'tools', 'streaming'] },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
39
55
|
{
|
|
40
56
|
id: 'google',
|
|
41
57
|
name: 'Google Gemini',
|
|
@@ -61,6 +61,7 @@ export declare class LLMClient {
|
|
|
61
61
|
chatCompletionWithTools(messages: Message[], tools: import('../../types/index.js').ToolDefinition[], options?: {
|
|
62
62
|
getPendingMessage?: () => string | null;
|
|
63
63
|
clearPendingMessage?: () => void;
|
|
64
|
+
model?: string;
|
|
64
65
|
}): Promise<{
|
|
65
66
|
message: Message;
|
|
66
67
|
toolCalls: Array<{
|
|
@@ -415,6 +415,7 @@ export class LLMClient {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
async chatCompletionWithTools(messages, tools, options) {
|
|
418
|
+
const roleModel = options?.model;
|
|
418
419
|
let workingMessages = [...messages];
|
|
419
420
|
const toolCallHistory = [];
|
|
420
421
|
let iterations = 0;
|
|
@@ -443,6 +444,7 @@ export class LLMClient {
|
|
|
443
444
|
messages: workingMessages,
|
|
444
445
|
tools,
|
|
445
446
|
tool_choice: 'required',
|
|
447
|
+
...(roleModel ? { model: roleModel } : {}),
|
|
446
448
|
});
|
|
447
449
|
}
|
|
448
450
|
catch (error) {
|
|
@@ -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
|