orquesta-cli 0.2.92 → 0.2.93
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/cli.js +6 -0
- package/dist/core/llm/llm-client.js +6 -0
- package/dist/orchestration/audit-log.d.ts +1 -1
- package/dist/orchestration/parallel-orchestrator.js +43 -6
- package/dist/orchestration/plan-executor.js +43 -4
- package/dist/tools/llm/simple/bash-tool.js +40 -12
- package/dist/ui/components/PlanExecuteApp.js +4 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ async function ensureBatutaFromEnv() {
|
|
|
45
45
|
const token = process.env['ORQUESTA_TOKEN'];
|
|
46
46
|
if (!token)
|
|
47
47
|
return;
|
|
48
|
+
await configManager.initialize();
|
|
48
49
|
const apiUrl = (process.env['ORQUESTA_API_URL'] || 'https://getorquesta.com').replace(/\/+$/, '');
|
|
49
50
|
if (configManager.getAllEndpoints().some((e) => e.id === 'batuta-proxy')) {
|
|
50
51
|
await configManager.removeEndpoint('batuta-proxy');
|
|
@@ -152,6 +153,8 @@ program
|
|
|
152
153
|
setAppendedSystemPrompt(options.appendSystemPrompt);
|
|
153
154
|
}
|
|
154
155
|
if (options.endpoint) {
|
|
156
|
+
await configManager.initialize();
|
|
157
|
+
await ensureBatutaFromEnv();
|
|
155
158
|
const wanted = String(options.endpoint).toLowerCase();
|
|
156
159
|
const all = configManager.getAllEndpoints();
|
|
157
160
|
const ep = wanted === 'batuta'
|
|
@@ -164,6 +167,9 @@ program
|
|
|
164
167
|
if (m)
|
|
165
168
|
await configManager.setCurrentModel(m.id);
|
|
166
169
|
}
|
|
170
|
+
else {
|
|
171
|
+
logger.warn(`--endpoint "${options.endpoint}" matched no configured endpoint (have: ${all.map(e => e.id).join(', ') || 'none'}); keeping current selection`);
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
174
|
if (options.eval) {
|
|
169
175
|
await runEvalMode();
|
|
@@ -672,6 +672,7 @@ export class LLMClient {
|
|
|
672
672
|
let finalResponseFailures = 0;
|
|
673
673
|
const MAX_NO_TOOL_CALL_RETRIES = 3;
|
|
674
674
|
const MAX_FINAL_RESPONSE_FAILURES = 3;
|
|
675
|
+
const MAX_ITERATIONS = Number(process.env['ORQUESTA_MAX_TOOL_ITERATIONS']) || 50;
|
|
675
676
|
const recentToolSignatures = [];
|
|
676
677
|
const recentNormalizedSignatures = [];
|
|
677
678
|
const LOOP_WINDOW = 5;
|
|
@@ -684,6 +685,11 @@ export class LLMClient {
|
|
|
684
685
|
throw new Error('INTERRUPTED');
|
|
685
686
|
}
|
|
686
687
|
iterations++;
|
|
688
|
+
if (iterations > MAX_ITERATIONS) {
|
|
689
|
+
logger.error('Tool-call iteration budget exhausted — aborting', new Error(`MAX_ITERATIONS: ${iterations}`));
|
|
690
|
+
throw new Error(`MAX_ITERATIONS: exceeded ${MAX_ITERATIONS} tool iterations without producing a final answer. ` +
|
|
691
|
+
`Aborting to protect the session. Raise ORQUESTA_MAX_TOOL_ITERATIONS for genuinely long tasks.`);
|
|
692
|
+
}
|
|
687
693
|
if (options?.getPendingMessage && options?.clearPendingMessage) {
|
|
688
694
|
const pendingMsg = options.getPendingMessage();
|
|
689
695
|
if (pendingMsg) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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';
|
|
2
|
+
export type AuditEventKind = 'run.start' | 'planner.complete' | 'orchestrator.decision' | 'worker.start' | 'worker.complete' | 'worker.blocked' | 'wave.complete' | 'refiner.complete' | 'run.complete' | 'run.stalled' | 'run.error';
|
|
3
3
|
export interface AuditEvent {
|
|
4
4
|
schema: typeof SCHEMA_VERSION;
|
|
5
5
|
timestamp: string;
|
|
@@ -4,6 +4,7 @@ import { logger } from '../utils/logger.js';
|
|
|
4
4
|
import { worktreeManager } from './worktree-manager.js';
|
|
5
5
|
import { memoryStore } from './memory-store.js';
|
|
6
6
|
import { auditLog } from './audit-log.js';
|
|
7
|
+
const WORKER_TIMEOUT_MS = Number(process.env['ORQUESTA_WORKER_TIMEOUT_MS']) || 15 * 60 * 1000;
|
|
7
8
|
export function shouldUseParallelOrchestrator(todos) {
|
|
8
9
|
if (todos.length < 2)
|
|
9
10
|
return false;
|
|
@@ -11,8 +12,8 @@ export function shouldUseParallelOrchestrator(todos) {
|
|
|
11
12
|
if (hasExplicitDeps)
|
|
12
13
|
return true;
|
|
13
14
|
const fsTouching = todos.filter(t => t.requiresFilesystem).length;
|
|
14
|
-
if (fsTouching
|
|
15
|
-
return
|
|
15
|
+
if (fsTouching > 0)
|
|
16
|
+
return false;
|
|
16
17
|
const allIndependent = todos.every(t => !t.dependsOn || t.dependsOn.length === 0);
|
|
17
18
|
if (allIndependent && todos.length >= 3)
|
|
18
19
|
return true;
|
|
@@ -83,9 +84,17 @@ async function runWorker(ctx) {
|
|
|
83
84
|
isolated: !!ctx.workingDirectory && ctx.workingDirectory !== process.cwd(),
|
|
84
85
|
});
|
|
85
86
|
const startedAt = Date.now();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
let watchdog;
|
|
88
|
+
const result = await Promise.race([
|
|
89
|
+
ctx.llmClient.chatCompletionWithTools(messages, tools, {
|
|
90
|
+
...(ctx.executorModel ? { model: ctx.executorModel } : {}),
|
|
91
|
+
}),
|
|
92
|
+
new Promise((_, reject) => {
|
|
93
|
+
watchdog = setTimeout(() => reject(new Error(`Worker ${ctx.todo.id} exceeded ${Math.round(WORKER_TIMEOUT_MS / 1000)}s watchdog — task aborted`)), WORKER_TIMEOUT_MS);
|
|
94
|
+
watchdog.unref?.();
|
|
95
|
+
}),
|
|
96
|
+
]).finally(() => { if (watchdog)
|
|
97
|
+
clearTimeout(watchdog); });
|
|
89
98
|
const extractSummary = () => {
|
|
90
99
|
for (let i = result.allMessages.length - 1; i >= 0; i--) {
|
|
91
100
|
const m = result.allMessages[i];
|
|
@@ -135,6 +144,7 @@ export async function runParallelGraph(opts) {
|
|
|
135
144
|
const todoById = new Map(todos.map(t => [t.id, t]));
|
|
136
145
|
const outputs = {};
|
|
137
146
|
const allNewMessages = [];
|
|
147
|
+
const failed = new Set();
|
|
138
148
|
const waves = planWaves(todos);
|
|
139
149
|
logger.flow('Parallel orchestrator plan', {
|
|
140
150
|
waveCount: waves.length,
|
|
@@ -147,7 +157,27 @@ export async function runParallelGraph(opts) {
|
|
|
147
157
|
callbacks.setCurrentActivity(`Wave ${waveIdx + 1}/${waves.length} (${wave.length} task${wave.length === 1 ? '' : 's'})`);
|
|
148
158
|
const waveStartedAt = Date.now();
|
|
149
159
|
for (let i = 0; i < wave.length; i += maxParallel) {
|
|
150
|
-
const
|
|
160
|
+
const fullBatch = wave.slice(i, i + maxParallel);
|
|
161
|
+
const batch = [];
|
|
162
|
+
for (const t of fullBatch) {
|
|
163
|
+
const item = todoById.get(t.id);
|
|
164
|
+
const deadDeps = (item.dependsOn ?? []).filter(d => failed.has(d));
|
|
165
|
+
if (deadDeps.length > 0) {
|
|
166
|
+
item.status = 'failed';
|
|
167
|
+
item.error = `Blocked: prerequisite ${deadDeps.join(', ')} did not complete`;
|
|
168
|
+
failed.add(t.id);
|
|
169
|
+
await memoryStore.set(sessionId, `worker:${t.id}`, {
|
|
170
|
+
title: item.title,
|
|
171
|
+
summary: `⛔ ${item.error}`,
|
|
172
|
+
blocked: true,
|
|
173
|
+
completedAt: new Date().toISOString(),
|
|
174
|
+
}, 'worker');
|
|
175
|
+
auditLog.emit(sessionId, 'worker.blocked', { todoId: t.id, deadDeps });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
batch.push(t);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
151
181
|
for (const t of batch) {
|
|
152
182
|
const item = todoById.get(t.id);
|
|
153
183
|
item.status = 'in_progress';
|
|
@@ -194,7 +224,14 @@ export async function runParallelGraph(opts) {
|
|
|
194
224
|
const err = res.reason;
|
|
195
225
|
item.status = 'failed';
|
|
196
226
|
item.error = err?.message || String(err);
|
|
227
|
+
failed.add(t.id);
|
|
197
228
|
logger.error('Worker failed', err);
|
|
229
|
+
await memoryStore.set(sessionId, `worker:${t.id}`, {
|
|
230
|
+
title: item.title,
|
|
231
|
+
summary: `❌ Failed: ${item.error}`,
|
|
232
|
+
failed: true,
|
|
233
|
+
completedAt: new Date().toISOString(),
|
|
234
|
+
}, 'worker');
|
|
198
235
|
auditLog.emit(sessionId, 'worker.complete', {
|
|
199
236
|
todoId: t.id,
|
|
200
237
|
model: executorModel,
|
|
@@ -78,6 +78,29 @@ export class PlanExecutor {
|
|
|
78
78
|
});
|
|
79
79
|
let runError = null;
|
|
80
80
|
let runInterrupted = false;
|
|
81
|
+
let runHadFailures = false;
|
|
82
|
+
const STALL_TIMEOUT_MS = Number(process.env['ORQUESTA_STALL_TIMEOUT_MS']) || 20 * 60 * 1000;
|
|
83
|
+
let lastActivityAt = Date.now();
|
|
84
|
+
const bumpActivity = () => { lastActivityAt = Date.now(); };
|
|
85
|
+
const baseCb = callbacks;
|
|
86
|
+
callbacks = {
|
|
87
|
+
...baseCb,
|
|
88
|
+
setTodos: (t) => { bumpActivity(); baseCb.setTodos(t); },
|
|
89
|
+
setMessages: (m) => { bumpActivity(); baseCb.setMessages(m); },
|
|
90
|
+
setCurrentActivity: (a) => { bumpActivity(); baseCb.setCurrentActivity(a); },
|
|
91
|
+
setCurrentTodoId: (id) => { bumpActivity(); baseCb.setCurrentTodoId(id); },
|
|
92
|
+
setExecutionPhase: (p) => { bumpActivity(); baseCb.setExecutionPhase(p); },
|
|
93
|
+
};
|
|
94
|
+
const stallTimer = setInterval(() => {
|
|
95
|
+
const idleMs = Date.now() - lastActivityAt;
|
|
96
|
+
if (!isInterruptedRef.current && idleMs > STALL_TIMEOUT_MS) {
|
|
97
|
+
logger.warn(`Stall watchdog: no progress for ${Math.round(idleMs / 1000)}s — aborting run`);
|
|
98
|
+
auditLog.emit(auditSid, 'run.stalled', { runId, idleMs });
|
|
99
|
+
isInterruptedRef.current = true;
|
|
100
|
+
baseCb.setIsInterrupted(true);
|
|
101
|
+
}
|
|
102
|
+
}, 30_000);
|
|
103
|
+
stallTimer.unref?.();
|
|
81
104
|
try {
|
|
82
105
|
if (isInterruptedRef.current) {
|
|
83
106
|
throw new Error('INTERRUPTED');
|
|
@@ -221,9 +244,24 @@ export class PlanExecutor {
|
|
|
221
244
|
await this.checkAndPerformAutoCompact(llmClient, currentMessages, currentTodos, callbacks, (updated) => { currentMessages = updated; });
|
|
222
245
|
currentMessages = await this.maybeRunRefiner(llmClient, currentMessages, currentTodos, callbacks);
|
|
223
246
|
const stats = getTodoStats(currentTodos);
|
|
247
|
+
const failedTodos = currentTodos.filter(t => t.status === 'failed');
|
|
248
|
+
const runSucceeded = failedTodos.length === 0;
|
|
249
|
+
runHadFailures = !runSucceeded;
|
|
224
250
|
sessionManager.autoSaveCurrentSession(currentMessages);
|
|
225
|
-
|
|
226
|
-
|
|
251
|
+
if (failedTodos.length > 0) {
|
|
252
|
+
const lines = failedTodos
|
|
253
|
+
.map(t => ` • [${t.id}] ${t.title} — ${t.error || 'failed'}`)
|
|
254
|
+
.join('\n');
|
|
255
|
+
callbacks.setMessages((prev) => [
|
|
256
|
+
...prev,
|
|
257
|
+
{
|
|
258
|
+
role: 'assistant',
|
|
259
|
+
content: `⚠️ Run did NOT fully succeed — ${failedTodos.length} task(s) failed or were blocked:\n${lines}\n\nDo not treat this as a successful deploy/run until these are resolved.`,
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
auditLog.emit(auditSid, 'run.complete', { runId, success: runSucceeded, ...stats });
|
|
264
|
+
logger.exit('PlanExecutor.executePlanMode', { success: runSucceeded, ...stats });
|
|
227
265
|
}
|
|
228
266
|
catch (error) {
|
|
229
267
|
if (error instanceof Error && error.message === 'INTERRUPTED') {
|
|
@@ -259,6 +297,7 @@ export class PlanExecutor {
|
|
|
259
297
|
});
|
|
260
298
|
}
|
|
261
299
|
finally {
|
|
300
|
+
clearInterval(stallTimer);
|
|
262
301
|
if (orquestaTrackingId) {
|
|
263
302
|
try {
|
|
264
303
|
if (runInterrupted) {
|
|
@@ -267,8 +306,8 @@ export class PlanExecutor {
|
|
|
267
306
|
else {
|
|
268
307
|
const usage = usageTracker.getSessionUsage();
|
|
269
308
|
await reportPromptComplete(orquestaTrackingId, {
|
|
270
|
-
success: !runError,
|
|
271
|
-
error: runError ? formatErrorMessage(runError) : undefined,
|
|
309
|
+
success: !runError && !runHadFailures,
|
|
310
|
+
error: runError ? formatErrorMessage(runError) : (runHadFailures ? 'One or more tasks failed or were blocked' : undefined),
|
|
272
311
|
tokensUsed: usage.totalTokens || undefined,
|
|
273
312
|
});
|
|
274
313
|
}
|
|
@@ -12,37 +12,65 @@ async function executeBash(command, cwd, timeout = 30000, explicitEnv) {
|
|
|
12
12
|
const child = spawn(shell, shellArgs, {
|
|
13
13
|
cwd: cwd || process.cwd(),
|
|
14
14
|
env: filterSafeEnv(process.env, userSafeVars, explicitEnv || {}),
|
|
15
|
+
detached: true,
|
|
15
16
|
});
|
|
16
17
|
let stdout = '';
|
|
17
18
|
let stderr = '';
|
|
18
19
|
let killed = false;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
25
|
-
child.on('close', (code) => {
|
|
20
|
+
let settled = false;
|
|
21
|
+
const finish = (code) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
26
25
|
clearTimeout(timer);
|
|
26
|
+
clearTimeout(hardTimer);
|
|
27
27
|
resolve({
|
|
28
28
|
stdout: stdout.trim(),
|
|
29
29
|
stderr: stderr.trim(),
|
|
30
30
|
exitCode: code ?? (killed ? 124 : 0),
|
|
31
31
|
timedOut: killed,
|
|
32
32
|
});
|
|
33
|
+
};
|
|
34
|
+
const killGroup = (sig) => {
|
|
35
|
+
try {
|
|
36
|
+
if (child.pid)
|
|
37
|
+
process.kill(-child.pid, sig);
|
|
38
|
+
else
|
|
39
|
+
child.kill(sig);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
try {
|
|
43
|
+
child.kill(sig);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
child.stdout?.on('data', (data) => {
|
|
49
|
+
stdout += data.toString();
|
|
33
50
|
});
|
|
51
|
+
child.stderr?.on('data', (data) => {
|
|
52
|
+
stderr += data.toString();
|
|
53
|
+
});
|
|
54
|
+
child.on('close', (code) => finish(code));
|
|
34
55
|
child.on('error', (error) => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
35
59
|
clearTimeout(timer);
|
|
60
|
+
clearTimeout(hardTimer);
|
|
36
61
|
reject(error);
|
|
37
62
|
});
|
|
38
63
|
const timer = setTimeout(() => {
|
|
39
64
|
killed = true;
|
|
40
|
-
|
|
41
|
-
setTimeout(() =>
|
|
42
|
-
child.kill('SIGKILL');
|
|
43
|
-
}
|
|
44
|
-
catch { } }, 2000).unref?.();
|
|
65
|
+
killGroup('SIGTERM');
|
|
66
|
+
setTimeout(() => killGroup('SIGKILL'), 2000).unref?.();
|
|
45
67
|
}, timeout);
|
|
68
|
+
const hardTimer = setTimeout(() => {
|
|
69
|
+
killed = true;
|
|
70
|
+
killGroup('SIGKILL');
|
|
71
|
+
finish(124);
|
|
72
|
+
}, timeout + 5000);
|
|
73
|
+
hardTimer.unref?.();
|
|
46
74
|
});
|
|
47
75
|
}
|
|
48
76
|
const BASH_TOOL_DEFINITION = {
|
|
@@ -459,9 +459,10 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo, resumeL
|
|
|
459
459
|
}
|
|
460
460
|
const interval = setInterval(() => {
|
|
461
461
|
const sessionUsage = usageTracker.getSessionUsage();
|
|
462
|
-
setSessionTokens(sessionUsage.totalTokens);
|
|
463
|
-
|
|
464
|
-
|
|
462
|
+
setSessionTokens(prev => (prev === sessionUsage.totalTokens ? prev : sessionUsage.totalTokens));
|
|
463
|
+
const elapsed = usageTracker.getSessionElapsedSeconds();
|
|
464
|
+
setSessionElapsed(prev => (prev === elapsed ? prev : elapsed));
|
|
465
|
+
}, 1000);
|
|
465
466
|
return () => clearInterval(interval);
|
|
466
467
|
}, [isProcessing]);
|
|
467
468
|
useEffect(() => {
|