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
|
@@ -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
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { configManager } from '../core/config/config-manager.js';
|
|
2
|
+
import { toolRegistry } from '../tools/registry.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { worktreeManager } from './worktree-manager.js';
|
|
5
|
+
import { memoryStore } from './memory-store.js';
|
|
6
|
+
import { auditLog } from './audit-log.js';
|
|
7
|
+
export function shouldUseParallelOrchestrator(todos) {
|
|
8
|
+
if (todos.length < 2)
|
|
9
|
+
return false;
|
|
10
|
+
const hasExplicitDeps = todos.some(t => Array.isArray(t.dependsOn) && t.dependsOn.length > 0);
|
|
11
|
+
if (hasExplicitDeps)
|
|
12
|
+
return true;
|
|
13
|
+
const fsTouching = todos.filter(t => t.requiresFilesystem).length;
|
|
14
|
+
if (fsTouching >= 2)
|
|
15
|
+
return true;
|
|
16
|
+
const allIndependent = todos.every(t => !t.dependsOn || t.dependsOn.length === 0);
|
|
17
|
+
if (allIndependent && todos.length >= 3)
|
|
18
|
+
return true;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export function planWaves(todos) {
|
|
22
|
+
const remaining = new Map(todos.map(t => [t.id, t]));
|
|
23
|
+
const completed = new Set();
|
|
24
|
+
const waves = [];
|
|
25
|
+
while (remaining.size > 0) {
|
|
26
|
+
const wave = [];
|
|
27
|
+
for (const todo of remaining.values()) {
|
|
28
|
+
const deps = todo.dependsOn ?? [];
|
|
29
|
+
if (deps.every(d => completed.has(d))) {
|
|
30
|
+
wave.push(todo);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (wave.length === 0) {
|
|
34
|
+
logger.warn('Dependency cycle or missing reference detected; dispatching remaining TODOs as final wave', {
|
|
35
|
+
remaining: Array.from(remaining.keys()),
|
|
36
|
+
});
|
|
37
|
+
wave.push(...remaining.values());
|
|
38
|
+
}
|
|
39
|
+
for (const t of wave) {
|
|
40
|
+
remaining.delete(t.id);
|
|
41
|
+
completed.add(t.id);
|
|
42
|
+
}
|
|
43
|
+
waves.push(wave);
|
|
44
|
+
}
|
|
45
|
+
return waves;
|
|
46
|
+
}
|
|
47
|
+
function buildWorkerMessages(ctx) {
|
|
48
|
+
const upstreamSection = Object.keys(ctx.upstreamOutputs).length > 0
|
|
49
|
+
? `\n\nUpstream task outputs (your dependencies):\n${Object.entries(ctx.upstreamOutputs)
|
|
50
|
+
.map(([id, out]) => `- [${id}] ${truncate(out, 800)}`)
|
|
51
|
+
.join('\n')}`
|
|
52
|
+
: '';
|
|
53
|
+
const cwdNote = ctx.workingDirectory && ctx.workingDirectory !== process.cwd()
|
|
54
|
+
? `\n\nIMPORTANT: Run all filesystem and shell operations from this directory: ${ctx.workingDirectory}\nDO NOT cd elsewhere.`
|
|
55
|
+
: '';
|
|
56
|
+
return [
|
|
57
|
+
{ role: 'system', content: ctx.baseSystemPrompt },
|
|
58
|
+
{
|
|
59
|
+
role: 'user',
|
|
60
|
+
content: `Execute ONLY this single task and then call final_response with a short summary of what you did. Do not start other tasks.
|
|
61
|
+
|
|
62
|
+
Task [${ctx.todo.id}]: ${ctx.todo.title}${upstreamSection}${cwdNote}`,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function truncate(s, n) {
|
|
67
|
+
if (!s || s.length <= n)
|
|
68
|
+
return s;
|
|
69
|
+
return s.slice(0, n) + '… [truncated]';
|
|
70
|
+
}
|
|
71
|
+
async function runWorker(ctx) {
|
|
72
|
+
const tools = toolRegistry.getLLMToolDefinitions();
|
|
73
|
+
const messages = buildWorkerMessages(ctx);
|
|
74
|
+
logger.flow('Worker dispatch', {
|
|
75
|
+
todoId: ctx.todo.id,
|
|
76
|
+
model: ctx.executorModel ?? '(default)',
|
|
77
|
+
cwd: ctx.workingDirectory ?? process.cwd(),
|
|
78
|
+
});
|
|
79
|
+
auditLog.emit(ctx.sessionId, 'worker.start', {
|
|
80
|
+
todoId: ctx.todo.id,
|
|
81
|
+
model: ctx.executorModel,
|
|
82
|
+
upstreamCount: Object.keys(ctx.upstreamOutputs).length,
|
|
83
|
+
isolated: !!ctx.workingDirectory && ctx.workingDirectory !== process.cwd(),
|
|
84
|
+
});
|
|
85
|
+
const startedAt = Date.now();
|
|
86
|
+
const result = await ctx.llmClient.chatCompletionWithTools(messages, tools, {
|
|
87
|
+
...(ctx.executorModel ? { model: ctx.executorModel } : {}),
|
|
88
|
+
});
|
|
89
|
+
const extractSummary = () => {
|
|
90
|
+
for (let i = result.allMessages.length - 1; i >= 0; i--) {
|
|
91
|
+
const m = result.allMessages[i];
|
|
92
|
+
if (m?.role === 'assistant' && Array.isArray(m.tool_calls)) {
|
|
93
|
+
for (const tc of m.tool_calls) {
|
|
94
|
+
if (tc?.function?.name === 'final_response') {
|
|
95
|
+
try {
|
|
96
|
+
const args = JSON.parse(tc.function.arguments || '{}');
|
|
97
|
+
const msg = args.message ?? args.response ?? args.summary ?? args.content;
|
|
98
|
+
if (typeof msg === 'string' && msg.trim())
|
|
99
|
+
return msg.trim();
|
|
100
|
+
}
|
|
101
|
+
catch { }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (let i = result.allMessages.length - 1; i >= 0; i--) {
|
|
107
|
+
const m = result.allMessages[i];
|
|
108
|
+
if (m?.role === 'assistant' && m.content && m.content.trim()) {
|
|
109
|
+
return m.content.trim();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return `(no summary from worker ${ctx.todo.id})`;
|
|
113
|
+
};
|
|
114
|
+
const summary = extractSummary();
|
|
115
|
+
await memoryStore.set(ctx.sessionId, `worker:${ctx.todo.id}`, {
|
|
116
|
+
title: ctx.todo.title,
|
|
117
|
+
summary,
|
|
118
|
+
completedAt: new Date().toISOString(),
|
|
119
|
+
}, 'worker');
|
|
120
|
+
auditLog.emit(ctx.sessionId, 'worker.complete', {
|
|
121
|
+
todoId: ctx.todo.id,
|
|
122
|
+
model: ctx.executorModel,
|
|
123
|
+
durationMs: Date.now() - startedAt,
|
|
124
|
+
summaryLength: summary.length,
|
|
125
|
+
success: true,
|
|
126
|
+
});
|
|
127
|
+
return { summary, messages: result.allMessages };
|
|
128
|
+
}
|
|
129
|
+
export async function runParallelGraph(opts) {
|
|
130
|
+
const { llmClient, callbacks, isInterruptedRef, sessionId, baseSystemPrompt } = opts;
|
|
131
|
+
const executorModel = configManager.getRoleModel('executor') ?? undefined;
|
|
132
|
+
const maxParallel = configManager.getMaxParallelWorkers();
|
|
133
|
+
const useWorktrees = configManager.isWorktreeIsolationEnabled();
|
|
134
|
+
const todos = opts.todos.map(t => ({ ...t }));
|
|
135
|
+
const todoById = new Map(todos.map(t => [t.id, t]));
|
|
136
|
+
const outputs = {};
|
|
137
|
+
const allNewMessages = [];
|
|
138
|
+
const waves = planWaves(todos);
|
|
139
|
+
logger.flow('Parallel orchestrator plan', {
|
|
140
|
+
waveCount: waves.length,
|
|
141
|
+
waveSizes: waves.map(w => w.length),
|
|
142
|
+
maxParallel,
|
|
143
|
+
});
|
|
144
|
+
for (const [waveIdx, wave] of waves.entries()) {
|
|
145
|
+
if (isInterruptedRef.current)
|
|
146
|
+
throw new Error('INTERRUPTED');
|
|
147
|
+
callbacks.setCurrentActivity(`Wave ${waveIdx + 1}/${waves.length} (${wave.length} task${wave.length === 1 ? '' : 's'})`);
|
|
148
|
+
const waveStartedAt = Date.now();
|
|
149
|
+
for (let i = 0; i < wave.length; i += maxParallel) {
|
|
150
|
+
const batch = wave.slice(i, i + maxParallel);
|
|
151
|
+
for (const t of batch) {
|
|
152
|
+
const item = todoById.get(t.id);
|
|
153
|
+
item.status = 'in_progress';
|
|
154
|
+
}
|
|
155
|
+
callbacks.setTodos([...todos]);
|
|
156
|
+
const allocations = new Map();
|
|
157
|
+
if (useWorktrees) {
|
|
158
|
+
for (const t of batch) {
|
|
159
|
+
if (t.requiresFilesystem) {
|
|
160
|
+
try {
|
|
161
|
+
const alloc = await worktreeManager.allocate(t.id);
|
|
162
|
+
allocations.set(t.id, alloc.path);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logger.warn('Worktree allocation failed; worker will run in host cwd', { todoId: t.id, error: error.message });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const settled = await Promise.allSettled(batch.map(t => runWorker({
|
|
171
|
+
todo: todoById.get(t.id),
|
|
172
|
+
sessionId,
|
|
173
|
+
llmClient,
|
|
174
|
+
baseSystemPrompt,
|
|
175
|
+
executorModel,
|
|
176
|
+
upstreamOutputs: (t.dependsOn ?? []).reduce((acc, depId) => {
|
|
177
|
+
if (outputs[depId])
|
|
178
|
+
acc[depId] = outputs[depId];
|
|
179
|
+
return acc;
|
|
180
|
+
}, {}),
|
|
181
|
+
workingDirectory: allocations.get(t.id),
|
|
182
|
+
})));
|
|
183
|
+
for (let k = 0; k < settled.length; k++) {
|
|
184
|
+
const t = batch[k];
|
|
185
|
+
const item = todoById.get(t.id);
|
|
186
|
+
const res = settled[k];
|
|
187
|
+
if (res.status === 'fulfilled') {
|
|
188
|
+
outputs[t.id] = res.value.summary;
|
|
189
|
+
item.status = 'completed';
|
|
190
|
+
item.result = res.value.summary;
|
|
191
|
+
allNewMessages.push(...res.value.messages);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const err = res.reason;
|
|
195
|
+
item.status = 'failed';
|
|
196
|
+
item.error = err?.message || String(err);
|
|
197
|
+
logger.error('Worker failed', err);
|
|
198
|
+
auditLog.emit(sessionId, 'worker.complete', {
|
|
199
|
+
todoId: t.id,
|
|
200
|
+
model: executorModel,
|
|
201
|
+
success: false,
|
|
202
|
+
error: err?.message,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
callbacks.setTodos([...todos]);
|
|
207
|
+
auditLog.emit(sessionId, 'wave.complete', {
|
|
208
|
+
waveIdx,
|
|
209
|
+
size: batch.length,
|
|
210
|
+
durationMs: Date.now() - waveStartedAt,
|
|
211
|
+
succeeded: batch.filter((_, k) => settled[k].status === 'fulfilled').length,
|
|
212
|
+
failed: batch.filter((_, k) => settled[k].status === 'rejected').length,
|
|
213
|
+
});
|
|
214
|
+
if (useWorktrees) {
|
|
215
|
+
for (const [todoId] of allocations) {
|
|
216
|
+
const item = todoById.get(todoId);
|
|
217
|
+
if (item.status === 'completed') {
|
|
218
|
+
const merge = await worktreeManager.mergeBack(todoId);
|
|
219
|
+
if (!merge.success) {
|
|
220
|
+
logger.warn('Worktree merge had conflicts; left in place for manual resolution', {
|
|
221
|
+
todoId,
|
|
222
|
+
conflicts: merge.conflicts,
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
await worktreeManager.release(todoId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { newMessages: allNewMessages, todos, workerOutputs: outputs };
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=parallel-orchestrator.js.map
|
|
@@ -14,6 +14,9 @@ import { logger } from '../utils/logger.js';
|
|
|
14
14
|
import { getStreamLogger } from '../utils/json-stream-logger.js';
|
|
15
15
|
import { detectGitRepo } from '../utils/git-utils.js';
|
|
16
16
|
import { formatErrorMessage, buildTodoContext, findActiveTodo, getTodoStats } from './utils.js';
|
|
17
|
+
import { runParallelGraph, shouldUseParallelOrchestrator } from './parallel-orchestrator.js';
|
|
18
|
+
import { memoryStore } from './memory-store.js';
|
|
19
|
+
import { auditLog } from './audit-log.js';
|
|
17
20
|
function buildSystemPrompt() {
|
|
18
21
|
const isGitRepo = detectGitRepo();
|
|
19
22
|
const projectContext = getProjectContext();
|
|
@@ -41,17 +44,36 @@ export class PlanExecutor {
|
|
|
41
44
|
callbacks.setExecutionPhase('planning');
|
|
42
45
|
callbacks.setCurrentActivity('Planning tasks');
|
|
43
46
|
let currentTodos = [];
|
|
47
|
+
const auditSid = sessionManager.getCurrentSessionId() || 'no-session';
|
|
48
|
+
const runId = auditLog.startRun(auditSid, {
|
|
49
|
+
currentModel: configManager.getConfig().currentModel,
|
|
50
|
+
plannerModel: configManager.getRoleModel('planner'),
|
|
51
|
+
executorModel: configManager.getRoleModel('executor'),
|
|
52
|
+
refinerModel: configManager.getRoleModel('refiner'),
|
|
53
|
+
refinerEnabled: configManager.isRefinerEnabled(),
|
|
54
|
+
worktreeEnabled: configManager.isWorktreeIsolationEnabled(),
|
|
55
|
+
maxParallel: configManager.getMaxParallelWorkers(),
|
|
56
|
+
});
|
|
44
57
|
try {
|
|
45
58
|
if (isInterruptedRef.current) {
|
|
46
59
|
throw new Error('INTERRUPTED');
|
|
47
60
|
}
|
|
48
61
|
let currentMessages = messages;
|
|
49
62
|
callbacks.setCurrentActivity('Thinking');
|
|
50
|
-
const
|
|
63
|
+
const plannerModel = configManager.getRoleModel('planner');
|
|
64
|
+
const planningLLM = new PlanningLLM(llmClient, plannerModel ?? undefined);
|
|
65
|
+
const plannerStartedAt = Date.now();
|
|
51
66
|
if (callbacks.askUser) {
|
|
52
67
|
planningLLM.setAskUserCallback(callbacks.askUser);
|
|
53
68
|
}
|
|
54
69
|
const planResult = await planningLLM.generateTODOListWithDocsDecision(userMessage, currentMessages);
|
|
70
|
+
auditLog.emit(auditSid, 'planner.complete', {
|
|
71
|
+
runId,
|
|
72
|
+
model: plannerModel,
|
|
73
|
+
durationMs: Date.now() - plannerStartedAt,
|
|
74
|
+
todoCount: planResult.todos.length,
|
|
75
|
+
directResponse: !!planResult.directResponse,
|
|
76
|
+
});
|
|
55
77
|
if (planResult.clarificationMessages?.length) {
|
|
56
78
|
currentMessages = [...currentMessages, ...planResult.clarificationMessages];
|
|
57
79
|
callbacks.setMessages([...currentMessages]);
|
|
@@ -119,28 +141,69 @@ export class PlanExecutor {
|
|
|
119
141
|
}
|
|
120
142
|
const activeTodo = findActiveTodo(currentTodos);
|
|
121
143
|
callbacks.setCurrentActivity(activeTodo?.title || 'Working on tasks');
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
const sessionId = sessionManager.getCurrentSessionId();
|
|
145
|
+
if (sessionId) {
|
|
146
|
+
await memoryStore.set(sessionId, 'plan', {
|
|
147
|
+
todos: currentTodos.map(t => ({
|
|
148
|
+
id: t.id, title: t.title, dependsOn: t.dependsOn, requiresFilesystem: t.requiresFilesystem,
|
|
149
|
+
})),
|
|
150
|
+
userMessage,
|
|
151
|
+
}, 'planner');
|
|
152
|
+
}
|
|
153
|
+
const useParallel = shouldUseParallelOrchestrator(currentTodos);
|
|
154
|
+
auditLog.emit(auditSid, 'orchestrator.decision', {
|
|
155
|
+
runId,
|
|
156
|
+
parallel: useParallel,
|
|
157
|
+
todoCount: currentTodos.length,
|
|
158
|
+
explicitDeps: currentTodos.filter(t => Array.isArray(t.dependsOn) && t.dependsOn.length > 0).length,
|
|
159
|
+
requiresFilesystem: currentTodos.filter(t => t.requiresFilesystem).length,
|
|
132
160
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
if (useParallel && sessionId) {
|
|
162
|
+
logger.flow('Dispatching parallel orchestrator', { todoCount: currentTodos.length });
|
|
163
|
+
const baseSystem = currentMessages.find(m => m.role === 'system')?.content || buildSystemPrompt();
|
|
164
|
+
const graphResult = await runParallelGraph({
|
|
165
|
+
llmClient,
|
|
166
|
+
todos: currentTodos,
|
|
167
|
+
baseSystemPrompt: baseSystem,
|
|
168
|
+
sessionId,
|
|
169
|
+
callbacks,
|
|
170
|
+
isInterruptedRef,
|
|
171
|
+
});
|
|
172
|
+
currentTodos = graphResult.todos;
|
|
173
|
+
callbacks.setTodos([...currentTodos]);
|
|
174
|
+
currentMessages = [...currentMessages, ...graphResult.newMessages];
|
|
175
|
+
callbacks.setMessages([...currentMessages]);
|
|
176
|
+
sessionManager.autoSaveCurrentSession(currentMessages);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const todoContext = buildTodoContext(currentTodos);
|
|
180
|
+
const lastUserMsgIndex = currentMessages.map(m => m.role).lastIndexOf('user');
|
|
181
|
+
const messagesForLLM = lastUserMsgIndex >= 0
|
|
182
|
+
? currentMessages.map((m, i) => i === lastUserMsgIndex
|
|
183
|
+
? { ...m, content: m.content + todoContext }
|
|
184
|
+
: m)
|
|
185
|
+
: [...currentMessages, { role: 'user', content: `Execute the TODO list.${todoContext}` }];
|
|
186
|
+
const executorModel = configManager.getRoleModel('executor');
|
|
187
|
+
const result = await llmClient.chatCompletionWithTools(messagesForLLM, tools, {
|
|
188
|
+
getPendingMessage: callbacks.getPendingMessage,
|
|
189
|
+
clearPendingMessage: callbacks.clearPendingMessage,
|
|
190
|
+
...(executorModel ? { model: executorModel } : {}),
|
|
191
|
+
});
|
|
192
|
+
const newMessages = result.allMessages.slice(currentMessages.length);
|
|
193
|
+
currentMessages = [...currentMessages, ...newMessages];
|
|
194
|
+
callbacks.setMessages([...currentMessages]);
|
|
195
|
+
sessionManager.autoSaveCurrentSession(currentMessages);
|
|
196
|
+
}
|
|
137
197
|
await this.checkAndPerformAutoCompact(llmClient, currentMessages, currentTodos, callbacks, (updated) => { currentMessages = updated; });
|
|
198
|
+
currentMessages = await this.maybeRunRefiner(llmClient, currentMessages, currentTodos, callbacks);
|
|
138
199
|
const stats = getTodoStats(currentTodos);
|
|
139
200
|
sessionManager.autoSaveCurrentSession(currentMessages);
|
|
201
|
+
auditLog.emit(auditSid, 'run.complete', { runId, ...stats });
|
|
140
202
|
logger.exit('PlanExecutor.executePlanMode', { success: true, ...stats });
|
|
141
203
|
}
|
|
142
204
|
catch (error) {
|
|
143
205
|
if (error instanceof Error && error.message === 'INTERRUPTED') {
|
|
206
|
+
auditLog.emit(auditSid, 'run.error', { runId, reason: 'interrupted' });
|
|
144
207
|
logger.flow('Plan mode interrupted by user');
|
|
145
208
|
callbacks.setMessages((prev) => {
|
|
146
209
|
const updatedMessages = [
|
|
@@ -152,6 +215,7 @@ export class PlanExecutor {
|
|
|
152
215
|
});
|
|
153
216
|
return;
|
|
154
217
|
}
|
|
218
|
+
auditLog.emit(auditSid, 'run.error', { runId, message: error?.message });
|
|
155
219
|
logger.error('Plan mode execution failed', error);
|
|
156
220
|
const errorMessage = formatErrorMessage(error);
|
|
157
221
|
callbacks.setMessages((prev) => {
|
|
@@ -211,9 +275,11 @@ export class PlanExecutor {
|
|
|
211
275
|
? { ...m, content: m.content + todoContext }
|
|
212
276
|
: m)
|
|
213
277
|
: [...currentMessages, { role: 'user', content: `Resume the TODO list.${todoContext}` }];
|
|
278
|
+
const executorModel = configManager.getRoleModel('executor');
|
|
214
279
|
const result = await llmClient.chatCompletionWithTools(messagesForLLM, tools, {
|
|
215
280
|
getPendingMessage: callbacks.getPendingMessage,
|
|
216
281
|
clearPendingMessage: callbacks.clearPendingMessage,
|
|
282
|
+
...(executorModel ? { model: executorModel } : {}),
|
|
217
283
|
});
|
|
218
284
|
const newMessages = result.allMessages.slice(currentMessages.length);
|
|
219
285
|
currentMessages = [...currentMessages, ...newMessages];
|
|
@@ -350,6 +416,81 @@ export class PlanExecutor {
|
|
|
350
416
|
emitAssistantResponse(message);
|
|
351
417
|
});
|
|
352
418
|
}
|
|
419
|
+
async maybeRunRefiner(llmClient, currentMessages, currentTodos, callbacks) {
|
|
420
|
+
if (!configManager.isRefinerEnabled())
|
|
421
|
+
return currentMessages;
|
|
422
|
+
const refinerModel = configManager.getRoleModel('refiner');
|
|
423
|
+
if (!refinerModel)
|
|
424
|
+
return currentMessages;
|
|
425
|
+
const lastAssistantIdx = [...currentMessages].map(m => m.role).lastIndexOf('assistant');
|
|
426
|
+
if (lastAssistantIdx < 0)
|
|
427
|
+
return currentMessages;
|
|
428
|
+
const draft = currentMessages[lastAssistantIdx].content || '';
|
|
429
|
+
if (!draft.trim())
|
|
430
|
+
return currentMessages;
|
|
431
|
+
callbacks.setExecutionPhase('executing');
|
|
432
|
+
callbacks.setCurrentActivity('Refining response');
|
|
433
|
+
try {
|
|
434
|
+
const todoSummary = currentTodos
|
|
435
|
+
.map(t => `- [${t.status}] ${t.title}`)
|
|
436
|
+
.join('\n');
|
|
437
|
+
const sessionId = sessionManager.getCurrentSessionId();
|
|
438
|
+
let workerSummariesBlock = '';
|
|
439
|
+
if (sessionId) {
|
|
440
|
+
await memoryStore.load(sessionId);
|
|
441
|
+
const workerEntries = memoryStore.bySource(sessionId, 'worker');
|
|
442
|
+
if (workerEntries.length > 0) {
|
|
443
|
+
workerSummariesBlock = '\n\nPer-task worker outputs:\n' + workerEntries
|
|
444
|
+
.map(e => {
|
|
445
|
+
const v = e.value;
|
|
446
|
+
return `- ${v.title ?? '(untitled)'}: ${(v.summary ?? '').slice(0, 600)}`;
|
|
447
|
+
})
|
|
448
|
+
.join('\n');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const refinerSystem = `You are a refiner reviewing another model's final answer.
|
|
452
|
+
Tasks completed:
|
|
453
|
+
${todoSummary || '(none)'}${workerSummariesBlock}
|
|
454
|
+
|
|
455
|
+
Read the draft response below. If it is coherent, accurate, and well-structured, repeat it verbatim. If it is unclear, contradicts the completed tasks, or could be tighter, rewrite it concisely. Output ONLY the final user-facing message — no commentary, no preface.`;
|
|
456
|
+
const response = await llmClient.chatCompletion({
|
|
457
|
+
messages: [
|
|
458
|
+
{ role: 'system', content: refinerSystem },
|
|
459
|
+
{ role: 'user', content: `Draft response:\n\n${draft}` },
|
|
460
|
+
],
|
|
461
|
+
model: refinerModel,
|
|
462
|
+
temperature: 0.2,
|
|
463
|
+
max_tokens: 2000,
|
|
464
|
+
});
|
|
465
|
+
const refined = response.choices?.[0]?.message?.content?.trim();
|
|
466
|
+
const auditSid = sessionManager.getCurrentSessionId() || 'no-session';
|
|
467
|
+
if (refined && refined !== draft.trim()) {
|
|
468
|
+
const next = [...currentMessages];
|
|
469
|
+
next[lastAssistantIdx] = { ...next[lastAssistantIdx], content: refined };
|
|
470
|
+
callbacks.setMessages([...next]);
|
|
471
|
+
logger.flow('Refiner rewrote final response', {
|
|
472
|
+
originalLength: draft.length,
|
|
473
|
+
refinedLength: refined.length,
|
|
474
|
+
});
|
|
475
|
+
auditLog.emit(auditSid, 'refiner.complete', {
|
|
476
|
+
model: refinerModel,
|
|
477
|
+
rewrote: true,
|
|
478
|
+
originalLength: draft.length,
|
|
479
|
+
refinedLength: refined.length,
|
|
480
|
+
});
|
|
481
|
+
return next;
|
|
482
|
+
}
|
|
483
|
+
logger.flow('Refiner kept original response');
|
|
484
|
+
auditLog.emit(auditSid, 'refiner.complete', { model: refinerModel, rewrote: false });
|
|
485
|
+
return currentMessages;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
logger.warn('Refiner pass failed, keeping original draft', { error: error.message });
|
|
489
|
+
const auditSid = sessionManager.getCurrentSessionId() || 'no-session';
|
|
490
|
+
auditLog.emit(auditSid, 'refiner.complete', { model: refinerModel, rewrote: false, error: error.message });
|
|
491
|
+
return currentMessages;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
353
494
|
async checkAndPerformAutoCompact(llmClient, currentMessages, currentTodos, callbacks, updateLocalMessages) {
|
|
354
495
|
const model = configManager.getCurrentModel();
|
|
355
496
|
const maxTokens = model?.maxTokens || 128000;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type WorktreeBackend = 'git' | 'copy';
|
|
2
|
+
export interface WorktreeAllocation {
|
|
3
|
+
id: string;
|
|
4
|
+
path: string;
|
|
5
|
+
backend: WorktreeBackend;
|
|
6
|
+
branch?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MergeResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
filesChanged: string[];
|
|
11
|
+
conflicts: string[];
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class WorktreeManager {
|
|
15
|
+
private allocations;
|
|
16
|
+
private readonly tmpRoot;
|
|
17
|
+
constructor(tmpRoot?: string);
|
|
18
|
+
allocate(id: string, hostCwd?: string): Promise<WorktreeAllocation>;
|
|
19
|
+
mergeBack(id: string, hostCwd?: string): Promise<MergeResult>;
|
|
20
|
+
release(id: string, hostCwd?: string): Promise<void>;
|
|
21
|
+
releaseAll(hostCwd?: string): Promise<void>;
|
|
22
|
+
listActive(): WorktreeAllocation[];
|
|
23
|
+
}
|
|
24
|
+
export declare const worktreeManager: WorktreeManager;
|
|
25
|
+
//# sourceMappingURL=worktree-manager.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
async function isGitRepo(cwd) {
|
|
9
|
+
try {
|
|
10
|
+
await execAsync('git rev-parse --is-inside-work-tree', { cwd });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function gitRepoRoot(cwd) {
|
|
18
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', { cwd });
|
|
19
|
+
return stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
export class WorktreeManager {
|
|
22
|
+
allocations = new Map();
|
|
23
|
+
tmpRoot;
|
|
24
|
+
constructor(tmpRoot) {
|
|
25
|
+
this.tmpRoot = tmpRoot ?? path.join(os.tmpdir(), 'orquesta-cli-worktrees');
|
|
26
|
+
}
|
|
27
|
+
async allocate(id, hostCwd = process.cwd()) {
|
|
28
|
+
if (this.allocations.has(id)) {
|
|
29
|
+
throw new Error(`Worktree already allocated for id=${id}`);
|
|
30
|
+
}
|
|
31
|
+
const gitMode = await isGitRepo(hostCwd);
|
|
32
|
+
let allocation;
|
|
33
|
+
if (gitMode) {
|
|
34
|
+
const repoRoot = await gitRepoRoot(hostCwd);
|
|
35
|
+
const branch = `orquesta/worker-${id}-${Date.now()}`;
|
|
36
|
+
const worktreeDir = path.join(this.tmpRoot, 'git', id);
|
|
37
|
+
await fs.mkdir(path.dirname(worktreeDir), { recursive: true });
|
|
38
|
+
await execAsync(`git worktree add -b ${branch} "${worktreeDir}" HEAD`, { cwd: repoRoot });
|
|
39
|
+
allocation = { id, path: worktreeDir, backend: 'git', branch };
|
|
40
|
+
logger.flow('Allocated git worktree', { id, branch, path: worktreeDir });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const copyDir = path.join(this.tmpRoot, 'copy', id);
|
|
44
|
+
await fs.mkdir(copyDir, { recursive: true });
|
|
45
|
+
try {
|
|
46
|
+
await execAsync(`rsync -a --exclude='.git' "${hostCwd}/" "${copyDir}/"`);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
await execAsync(`cp -a "${hostCwd}/." "${copyDir}/"`);
|
|
50
|
+
}
|
|
51
|
+
allocation = { id, path: copyDir, backend: 'copy' };
|
|
52
|
+
logger.flow('Allocated copy worktree', { id, path: copyDir });
|
|
53
|
+
}
|
|
54
|
+
this.allocations.set(id, allocation);
|
|
55
|
+
return allocation;
|
|
56
|
+
}
|
|
57
|
+
async mergeBack(id, hostCwd = process.cwd()) {
|
|
58
|
+
const alloc = this.allocations.get(id);
|
|
59
|
+
if (!alloc) {
|
|
60
|
+
return { success: false, filesChanged: [], conflicts: [], error: `No allocation for id=${id}` };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (alloc.backend === 'git' && alloc.branch) {
|
|
64
|
+
const repoRoot = await gitRepoRoot(hostCwd);
|
|
65
|
+
const { stdout: diffStat } = await execAsync(`git diff --name-only HEAD ${alloc.branch}`, { cwd: repoRoot });
|
|
66
|
+
const filesChanged = diffStat.split('\n').map(s => s.trim()).filter(Boolean);
|
|
67
|
+
try {
|
|
68
|
+
await execAsync(`git merge --no-ff ${alloc.branch}`, { cwd: repoRoot });
|
|
69
|
+
return { success: true, filesChanged, conflicts: [] };
|
|
70
|
+
}
|
|
71
|
+
catch (mergeError) {
|
|
72
|
+
const { stdout: conflicts } = await execAsync('git diff --name-only --diff-filter=U', { cwd: repoRoot }).catch(() => ({ stdout: '' }));
|
|
73
|
+
await execAsync('git merge --abort', { cwd: repoRoot }).catch(() => { });
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
filesChanged,
|
|
77
|
+
conflicts: conflicts.split('\n').map(s => s.trim()).filter(Boolean),
|
|
78
|
+
error: mergeError.message,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const { stdout: rawDiff } = await execAsync(`rsync -avc --dry-run --exclude='.git' "${alloc.path}/" "${hostCwd}/"`).catch(() => ({ stdout: '' }));
|
|
83
|
+
const filesChanged = rawDiff
|
|
84
|
+
.split('\n')
|
|
85
|
+
.filter(line => line && !line.startsWith('sending') && !line.startsWith('total') && !line.startsWith('sent ') && !line.includes('xfer#'))
|
|
86
|
+
.map(s => s.trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
await execAsync(`rsync -a --exclude='.git' "${alloc.path}/" "${hostCwd}/"`);
|
|
89
|
+
return { success: true, filesChanged, conflicts: [] };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return { success: false, filesChanged: [], conflicts: [], error: error.message };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async release(id, hostCwd = process.cwd()) {
|
|
96
|
+
const alloc = this.allocations.get(id);
|
|
97
|
+
if (!alloc)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
if (alloc.backend === 'git') {
|
|
101
|
+
const repoRoot = await gitRepoRoot(hostCwd);
|
|
102
|
+
await execAsync(`git worktree remove --force "${alloc.path}"`, { cwd: repoRoot }).catch(() => { });
|
|
103
|
+
if (alloc.branch) {
|
|
104
|
+
await execAsync(`git branch -D ${alloc.branch}`, { cwd: repoRoot }).catch(() => { });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
await fs.rm(alloc.path, { recursive: true, force: true }).catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.allocations.delete(id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async releaseAll(hostCwd = process.cwd()) {
|
|
116
|
+
const ids = Array.from(this.allocations.keys());
|
|
117
|
+
await Promise.all(ids.map(id => this.release(id, hostCwd)));
|
|
118
|
+
}
|
|
119
|
+
listActive() {
|
|
120
|
+
return Array.from(this.allocations.values());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export const worktreeManager = new WorktreeManager();
|
|
124
|
+
//# sourceMappingURL=worktree-manager.js.map
|