orquesta-cli 0.2.45 → 0.2.46
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.js +2 -1
- package/dist/cli.js +17 -16
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/commands/clear.d.ts +3 -0
- package/dist/core/commands/clear.js +22 -0
- package/dist/core/commands/compact.d.ts +3 -0
- package/dist/core/commands/compact.js +45 -0
- package/dist/core/commands/help.d.ts +3 -0
- package/dist/core/commands/help.js +50 -0
- package/dist/core/commands/index.d.ts +3 -0
- package/dist/core/commands/index.js +11 -0
- package/dist/core/commands/memory.d.ts +3 -0
- package/dist/core/commands/memory.js +40 -0
- package/dist/core/commands/registry.d.ts +11 -0
- package/dist/core/commands/registry.js +25 -0
- package/dist/core/commands/types.d.ts +10 -0
- package/dist/core/commands/types.js +2 -0
- package/dist/core/event-bus.d.ts +20 -0
- package/dist/core/event-bus.js +35 -0
- package/dist/core/git-context.d.ts +11 -0
- package/dist/core/git-context.js +62 -0
- package/dist/core/ignore-filter.d.ts +4 -0
- package/dist/core/ignore-filter.js +50 -0
- package/dist/core/llm/llm-client.d.ts +1 -0
- package/dist/core/llm/llm-client.js +118 -40
- package/dist/core/onboarding.d.ts +3 -0
- package/dist/core/onboarding.js +48 -0
- package/dist/core/slash-command-handler.js +8 -135
- package/dist/orchestration/plan-executor.js +77 -71
- package/dist/prompts/shared/tool-usage.js +0 -1
- package/dist/prompts/system/plan-execute.js +50 -57
- package/dist/tools/llm/simple/file-tools.js +12 -1
- package/dist/tools/llm/simple/final-response-tool.js +7 -11
- package/dist/tools/registry.js +63 -10
- package/dist/ui/components/PlanExecuteApp.d.ts +1 -0
- package/dist/ui/components/PlanExecuteApp.js +59 -22
- package/package.json +8 -4
|
@@ -108,6 +108,7 @@ export class LLMClient {
|
|
|
108
108
|
modelName;
|
|
109
109
|
currentAbortController = null;
|
|
110
110
|
isInterrupted = false;
|
|
111
|
+
onStreamingContent = null;
|
|
111
112
|
static DEFAULT_MAX_RETRIES = 3;
|
|
112
113
|
constructor() {
|
|
113
114
|
const endpoint = configManager.getCurrentEndpoint();
|
|
@@ -183,11 +184,11 @@ export class LLMClient {
|
|
|
183
184
|
const modelId = options.model || this.model;
|
|
184
185
|
const processedMessages = options.messages ?
|
|
185
186
|
this.preprocessMessages(options.messages, modelId) : [];
|
|
186
|
-
logger.vars({ name: 'modelId', value: modelId }, { name: 'originalMessages', value: options.messages?.length || 0 }, { name: 'processedMessages', value: processedMessages.length }, { name: 'temperature', value: options.temperature ?? 0
|
|
187
|
+
logger.vars({ name: 'modelId', value: modelId }, { name: 'originalMessages', value: options.messages?.length || 0 }, { name: 'processedMessages', value: processedMessages.length }, { name: 'temperature', value: options.temperature ?? 0 });
|
|
187
188
|
const requestBody = {
|
|
188
189
|
model: modelId,
|
|
189
190
|
messages: processedMessages,
|
|
190
|
-
temperature: options.temperature ?? 0
|
|
191
|
+
temperature: options.temperature ?? 0,
|
|
191
192
|
max_tokens: options.max_tokens,
|
|
192
193
|
stream: false,
|
|
193
194
|
...(options.tools && {
|
|
@@ -210,13 +211,112 @@ export class LLMClient {
|
|
|
210
211
|
}
|
|
211
212
|
logger.startTimer('llm-api-call');
|
|
212
213
|
this.currentAbortController = new AbortController();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
let response;
|
|
215
|
+
if (this.onStreamingContent) {
|
|
216
|
+
const streamRequestBody = { ...requestBody, stream: true };
|
|
217
|
+
const streamResp = await this.axiosInstance.post(url, streamRequestBody, {
|
|
218
|
+
responseType: 'stream',
|
|
219
|
+
signal: this.currentAbortController.signal,
|
|
220
|
+
headers: buildPerRequestHeaders(),
|
|
221
|
+
});
|
|
222
|
+
captureBatutaHeaders(streamResp.headers);
|
|
223
|
+
const stream = streamResp.data;
|
|
224
|
+
let buffer = '';
|
|
225
|
+
let contentAccum = '';
|
|
226
|
+
let reasoningAccum = '';
|
|
227
|
+
let role = 'assistant';
|
|
228
|
+
let finishReason = null;
|
|
229
|
+
const toolCallsMap = new Map();
|
|
230
|
+
let responseId = '';
|
|
231
|
+
let responseModel = '';
|
|
232
|
+
for await (const chunk of stream) {
|
|
233
|
+
if (this.isInterrupted) {
|
|
234
|
+
throw new Error('INTERRUPTED');
|
|
235
|
+
}
|
|
236
|
+
buffer += chunk.toString();
|
|
237
|
+
const lines = buffer.split('\n');
|
|
238
|
+
buffer = lines.pop() || '';
|
|
239
|
+
for (const line of lines) {
|
|
240
|
+
const trimmed = line.trim();
|
|
241
|
+
if (!trimmed || trimmed === 'data: [DONE]')
|
|
242
|
+
continue;
|
|
243
|
+
if (!trimmed.startsWith('data: '))
|
|
244
|
+
continue;
|
|
245
|
+
try {
|
|
246
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
247
|
+
if (data.id)
|
|
248
|
+
responseId = data.id;
|
|
249
|
+
if (data.model)
|
|
250
|
+
responseModel = data.model;
|
|
251
|
+
const choice = data.choices?.[0];
|
|
252
|
+
if (!choice)
|
|
253
|
+
continue;
|
|
254
|
+
if (choice.finish_reason)
|
|
255
|
+
finishReason = choice.finish_reason;
|
|
256
|
+
const delta = choice.delta;
|
|
257
|
+
if (!delta)
|
|
258
|
+
continue;
|
|
259
|
+
if (delta.role)
|
|
260
|
+
role = delta.role;
|
|
261
|
+
if (delta.content) {
|
|
262
|
+
contentAccum += delta.content;
|
|
263
|
+
this.onStreamingContent(delta.content);
|
|
264
|
+
}
|
|
265
|
+
if (delta.reasoning) {
|
|
266
|
+
reasoningAccum += delta.reasoning;
|
|
267
|
+
}
|
|
268
|
+
if (delta.tool_calls) {
|
|
269
|
+
for (const tc of delta.tool_calls) {
|
|
270
|
+
const idx = tc.index ?? 0;
|
|
271
|
+
if (!toolCallsMap.has(idx)) {
|
|
272
|
+
toolCallsMap.set(idx, { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } });
|
|
273
|
+
}
|
|
274
|
+
const existing = toolCallsMap.get(idx);
|
|
275
|
+
if (tc.id)
|
|
276
|
+
existing.id = tc.id;
|
|
277
|
+
if (tc.function?.name)
|
|
278
|
+
existing.function.name += tc.function.name;
|
|
279
|
+
if (tc.function?.arguments)
|
|
280
|
+
existing.function.arguments += tc.function.arguments;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch { }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
this.currentAbortController = null;
|
|
288
|
+
const toolCalls = Array.from(toolCallsMap.values())
|
|
289
|
+
.filter(tc => tc.id && tc.function.name)
|
|
290
|
+
.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } }));
|
|
291
|
+
const reassembledMessage = {
|
|
292
|
+
role: role,
|
|
293
|
+
content: contentAccum,
|
|
294
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
295
|
+
...(reasoningAccum ? { reasoning: reasoningAccum } : {}),
|
|
296
|
+
};
|
|
297
|
+
response = {
|
|
298
|
+
data: {
|
|
299
|
+
id: responseId,
|
|
300
|
+
object: 'chat.completion',
|
|
301
|
+
created: Math.floor(Date.now() / 1000),
|
|
302
|
+
model: responseModel || modelId,
|
|
303
|
+
choices: [{ index: 0, message: reassembledMessage, finish_reason: finishReason }],
|
|
304
|
+
},
|
|
305
|
+
status: streamResp.status,
|
|
306
|
+
statusText: streamResp.statusText,
|
|
307
|
+
headers: streamResp.headers,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
const httpResp = await this.axiosInstance.post(url, requestBody, {
|
|
312
|
+
signal: this.currentAbortController.signal,
|
|
313
|
+
headers: buildPerRequestHeaders(),
|
|
314
|
+
});
|
|
315
|
+
this.currentAbortController = null;
|
|
316
|
+
response = { data: httpResp.data, status: httpResp.status, statusText: httpResp.statusText, headers: httpResp.headers };
|
|
317
|
+
captureBatutaHeaders(response.headers);
|
|
318
|
+
}
|
|
218
319
|
const elapsed = logger.endTimer('llm-api-call');
|
|
219
|
-
captureBatutaHeaders(response.headers);
|
|
220
320
|
logger.flow('API response received');
|
|
221
321
|
if (!response.data.choices || !Array.isArray(response.data.choices)) {
|
|
222
322
|
logger.error('Invalid response structure - missing choices array', response.data);
|
|
@@ -354,7 +454,7 @@ export class LLMClient {
|
|
|
354
454
|
const requestBody = {
|
|
355
455
|
model: modelId,
|
|
356
456
|
messages: processedMessages,
|
|
357
|
-
temperature: options.temperature ?? 0
|
|
457
|
+
temperature: options.temperature ?? 0,
|
|
358
458
|
max_tokens: options.max_tokens,
|
|
359
459
|
stream: true,
|
|
360
460
|
...(options.tools && {
|
|
@@ -495,9 +595,7 @@ export class LLMClient {
|
|
|
495
595
|
const toolCallHistory = [];
|
|
496
596
|
let iterations = 0;
|
|
497
597
|
let contextLengthRecoveryAttempted = false;
|
|
498
|
-
let noToolCallRetries = 0;
|
|
499
598
|
let finalResponseFailures = 0;
|
|
500
|
-
const MAX_NO_TOOL_CALL_RETRIES = 3;
|
|
501
599
|
const MAX_FINAL_RESPONSE_FAILURES = 3;
|
|
502
600
|
const recentToolSignatures = [];
|
|
503
601
|
const recentNormalizedSignatures = [];
|
|
@@ -524,7 +622,7 @@ export class LLMClient {
|
|
|
524
622
|
response = await this.chatCompletion({
|
|
525
623
|
messages: workingMessages,
|
|
526
624
|
tools,
|
|
527
|
-
tool_choice: '
|
|
625
|
+
tool_choice: 'auto',
|
|
528
626
|
...(roleModel ? { model: roleModel } : {}),
|
|
529
627
|
});
|
|
530
628
|
}
|
|
@@ -733,34 +831,14 @@ export class LLMClient {
|
|
|
733
831
|
continue;
|
|
734
832
|
}
|
|
735
833
|
else {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
message: { role: 'assistant', content: fallbackContent },
|
|
745
|
-
toolCalls: toolCallHistory,
|
|
746
|
-
allMessages: workingMessages,
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
const hasMalformedToolCall = assistantMessage.content &&
|
|
750
|
-
(/<tool_call>/i.test(assistantMessage.content) ||
|
|
751
|
-
/<arg_key>/i.test(assistantMessage.content) ||
|
|
752
|
-
/<arg_value>/i.test(assistantMessage.content) ||
|
|
753
|
-
/<\/tool_call>/i.test(assistantMessage.content) ||
|
|
754
|
-
/bash<arg_key>/i.test(assistantMessage.content));
|
|
755
|
-
const retryMessage = hasMalformedToolCall
|
|
756
|
-
? 'Your previous response contained a malformed tool call (XML tags in content). You MUST use the proper tool_calls API format. Use final_response tool to deliver your message to the user.'
|
|
757
|
-
: 'You must use tools for all actions. Use final_response tool to deliver your final message to the user after completing all tasks.';
|
|
758
|
-
workingMessages.push({
|
|
759
|
-
role: 'user',
|
|
760
|
-
content: retryMessage,
|
|
761
|
-
});
|
|
762
|
-
logger.debug('Enforcing tool call - added retry message');
|
|
763
|
-
continue;
|
|
834
|
+
const finalContent = assistantMessage.content || 'Task completed.';
|
|
835
|
+
const { emitAssistantResponse } = await import('../../tools/llm/simple/file-tools.js');
|
|
836
|
+
emitAssistantResponse(finalContent);
|
|
837
|
+
return {
|
|
838
|
+
message: { role: 'assistant', content: finalContent },
|
|
839
|
+
toolCalls: toolCallHistory,
|
|
840
|
+
allMessages: workingMessages,
|
|
841
|
+
};
|
|
764
842
|
}
|
|
765
843
|
}
|
|
766
844
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { configManager } from './config/config-manager.js';
|
|
3
|
+
import { scanProviders, toEndpointConfig } from './config/auto-detect.js';
|
|
4
|
+
import { CONFIG_FILE_PATH } from '../constants.js';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
export function shouldShowOnboarding() {
|
|
7
|
+
if (!fs.existsSync(CONFIG_FILE_PATH))
|
|
8
|
+
return true;
|
|
9
|
+
return !configManager.hasEndpoints();
|
|
10
|
+
}
|
|
11
|
+
export async function runOnboarding() {
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(chalk.cyan(' ╔══════════════════════════════════════════╗'));
|
|
14
|
+
console.log(chalk.cyan(' ║') + chalk.bold(' Welcome to Orquesta CLI! 🎵 ') + chalk.cyan('║'));
|
|
15
|
+
console.log(chalk.cyan(' ╚══════════════════════════════════════════╝'));
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(chalk.dim(' Scanning for LLM providers...'));
|
|
18
|
+
console.log();
|
|
19
|
+
const result = await scanProviders();
|
|
20
|
+
if (result.detected.length === 0) {
|
|
21
|
+
console.log(chalk.yellow(' No LLM providers detected.'));
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(chalk.dim(' To get started, do one of the following:'));
|
|
24
|
+
console.log(chalk.dim(' • Set an env var: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.'));
|
|
25
|
+
console.log(chalk.dim(' • Start a local provider (Ollama on port 11434)'));
|
|
26
|
+
console.log(chalk.dim(' • Run: orquesta --add-provider <provider-id>'));
|
|
27
|
+
console.log();
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
let addedCount = 0;
|
|
31
|
+
for (const detected of result.detected) {
|
|
32
|
+
const endpoint = toEndpointConfig(detected);
|
|
33
|
+
await configManager.addEndpoint(endpoint);
|
|
34
|
+
addedCount++;
|
|
35
|
+
if (addedCount === 1 && endpoint.models.length > 0) {
|
|
36
|
+
await configManager.setCurrentEndpoint(endpoint.id);
|
|
37
|
+
await configManager.setCurrentModel(endpoint.models[0].id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log(chalk.green(` ✓ Auto-configured ${addedCount} provider(s):`));
|
|
41
|
+
for (const d of result.detected) {
|
|
42
|
+
const modelCount = d.discoveredModels.length;
|
|
43
|
+
console.log(chalk.white(` • ${d.provider.name}`) + chalk.dim(` (${modelCount} model${modelCount !== 1 ? 's' : ''})`));
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=onboarding.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandRegistry } from './commands/index.js';
|
|
1
2
|
import { sessionManager } from './session/session-manager.js';
|
|
2
3
|
import { usageTracker } from './usage-tracker.js';
|
|
3
4
|
import { logger } from '../utils/logger.js';
|
|
@@ -6,72 +7,24 @@ import { readHookConfig, writeHookFiles, disableHooks } from '../orquesta/hook-i
|
|
|
6
7
|
import { checkForCliUpdate, runCliUpdate, setSkippedVersion } from '../utils/update-checker.js';
|
|
7
8
|
import { createRequire } from 'module';
|
|
8
9
|
import { configManager } from './config/config-manager.js';
|
|
9
|
-
import { getForcedTier, setForcedTier
|
|
10
|
+
import { getForcedTier, setForcedTier } from './routing-state.js';
|
|
10
11
|
import { auditLog } from '../orchestration/audit-log.js';
|
|
11
12
|
import { remotePhone } from '../orquesta/remote-phone.js';
|
|
12
13
|
export async function executeSlashCommand(command, context) {
|
|
13
14
|
const trimmedCommand = command.trim();
|
|
14
15
|
logger.enter('executeSlashCommand', { command: trimmedCommand });
|
|
16
|
+
const commandName = trimmedCommand.split(/\s/)[0];
|
|
17
|
+
const registryResult = await commandRegistry.execute(commandName, context, trimmedCommand);
|
|
18
|
+
if (registryResult) {
|
|
19
|
+
logger.exit('executeSlashCommand', { handled: true, command: commandName, source: 'registry' });
|
|
20
|
+
return registryResult;
|
|
21
|
+
}
|
|
15
22
|
if (trimmedCommand === '/exit' || trimmedCommand === '/quit') {
|
|
16
23
|
logger.flow('Exit command received');
|
|
17
24
|
context.exit();
|
|
18
25
|
logger.exit('executeSlashCommand', { handled: true, command: 'exit' });
|
|
19
26
|
return { handled: true, shouldContinue: false };
|
|
20
27
|
}
|
|
21
|
-
if (trimmedCommand === '/clear') {
|
|
22
|
-
logger.flow('Clear command - resetting messages and todos');
|
|
23
|
-
context.setMessages([]);
|
|
24
|
-
context.setTodos([]);
|
|
25
|
-
resetBatutaSession();
|
|
26
|
-
logger.exit('executeSlashCommand', { handled: true, command: 'clear' });
|
|
27
|
-
return {
|
|
28
|
-
handled: true,
|
|
29
|
-
shouldContinue: false,
|
|
30
|
-
updatedContext: {
|
|
31
|
-
messages: [],
|
|
32
|
-
todos: [],
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
if (trimmedCommand === '/compact') {
|
|
37
|
-
logger.flow('Compact command received');
|
|
38
|
-
if (context.onCompact) {
|
|
39
|
-
logger.flow('Executing compact callback');
|
|
40
|
-
const result = await context.onCompact();
|
|
41
|
-
logger.vars({ name: 'compactSuccess', value: result.success }, { name: 'originalCount', value: result.originalMessageCount }, { name: 'newCount', value: result.newMessageCount });
|
|
42
|
-
const compactMessage = result.success
|
|
43
|
-
? `✅ Conversation compacted successfully. (${result.originalMessageCount} → ${result.newMessageCount} messages)`
|
|
44
|
-
: `❌ Compact failed: ${result.error}`;
|
|
45
|
-
const baseMessages = (result.success && result.compactedMessages)
|
|
46
|
-
? result.compactedMessages
|
|
47
|
-
: context.messages;
|
|
48
|
-
const updatedMessages = [
|
|
49
|
-
...baseMessages,
|
|
50
|
-
{ role: 'assistant', content: compactMessage },
|
|
51
|
-
];
|
|
52
|
-
context.setMessages(updatedMessages);
|
|
53
|
-
return {
|
|
54
|
-
handled: true,
|
|
55
|
-
shouldContinue: false,
|
|
56
|
-
updatedContext: {
|
|
57
|
-
messages: updatedMessages,
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
const fallbackMessage = '/compact is only available in interactive mode.';
|
|
62
|
-
const updatedMessages = [
|
|
63
|
-
...context.messages,
|
|
64
|
-
{ role: 'assistant', content: fallbackMessage },
|
|
65
|
-
];
|
|
66
|
-
context.setMessages(updatedMessages);
|
|
67
|
-
return {
|
|
68
|
-
handled: true,
|
|
69
|
-
shouldContinue: false,
|
|
70
|
-
updatedContext: {
|
|
71
|
-
messages: updatedMessages,
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
28
|
if (trimmedCommand === '/settings') {
|
|
76
29
|
if (context.onShowSettings) {
|
|
77
30
|
context.onShowSettings();
|
|
@@ -524,41 +477,6 @@ ${executorLines}
|
|
|
524
477
|
context.setMessages(updatedMessages);
|
|
525
478
|
return { handled: true, shouldContinue: false, updatedContext: { messages: updatedMessages } };
|
|
526
479
|
}
|
|
527
|
-
if (trimmedCommand.startsWith('/memory')) {
|
|
528
|
-
const sub = trimmedCommand.slice(7).trim();
|
|
529
|
-
const { addMemory, removeMemory, clearMemory, listMemory } = await import('./memory.js');
|
|
530
|
-
const reply = (content) => {
|
|
531
|
-
const updatedMessages = [...context.messages, { role: 'assistant', content }];
|
|
532
|
-
context.setMessages(updatedMessages);
|
|
533
|
-
return { handled: true, shouldContinue: false, updatedContext: { messages: updatedMessages } };
|
|
534
|
-
};
|
|
535
|
-
if (sub.startsWith('add ')) {
|
|
536
|
-
const note = sub.slice(4).trim();
|
|
537
|
-
if (!note)
|
|
538
|
-
return reply('Usage: /memory add <note>');
|
|
539
|
-
addMemory(note);
|
|
540
|
-
return reply(`✓ Saved to memory: "${note}"`);
|
|
541
|
-
}
|
|
542
|
-
if (sub === 'list' || sub === '') {
|
|
543
|
-
const entries = listMemory();
|
|
544
|
-
if (entries.length === 0)
|
|
545
|
-
return reply('Memory is empty. Use `/memory add <note>` to save preferences.');
|
|
546
|
-
const list = entries.map((e, i) => ` ${i + 1}. ${e}`).join('\n');
|
|
547
|
-
return reply(`📝 User memory (${entries.length} entries):\n${list}\n\nCommands: /memory add <note> | remove <n> | clear`);
|
|
548
|
-
}
|
|
549
|
-
if (sub.startsWith('remove ')) {
|
|
550
|
-
const idx = parseInt(sub.slice(7).trim(), 10);
|
|
551
|
-
if (isNaN(idx))
|
|
552
|
-
return reply('Usage: /memory remove <number>');
|
|
553
|
-
const ok = removeMemory(idx);
|
|
554
|
-
return reply(ok ? `✓ Removed entry #${idx}` : `Entry #${idx} not found`);
|
|
555
|
-
}
|
|
556
|
-
if (sub === 'clear') {
|
|
557
|
-
clearMemory();
|
|
558
|
-
return reply('✓ Memory cleared');
|
|
559
|
-
}
|
|
560
|
-
return reply('Usage: /memory add <note> | list | remove <n> | clear');
|
|
561
|
-
}
|
|
562
480
|
if (trimmedCommand === '/update') {
|
|
563
481
|
logger.flow('Update command received');
|
|
564
482
|
const reply = (content) => {
|
|
@@ -661,51 +579,6 @@ ${executorLines}
|
|
|
661
579
|
return reply(`❌ Could not open the remote phone channel: ${e.message}`);
|
|
662
580
|
}
|
|
663
581
|
}
|
|
664
|
-
if (trimmedCommand === '/help') {
|
|
665
|
-
const helpMessage = `
|
|
666
|
-
Available commands:
|
|
667
|
-
/exit, /quit - Exit the application
|
|
668
|
-
/clear - Clear conversation and TODOs
|
|
669
|
-
/compact - Compact conversation to free up context
|
|
670
|
-
/memory - Persistent memory: /memory add <note> | list | remove <n> | clear
|
|
671
|
-
/settings - Open settings menu
|
|
672
|
-
/model - Switch between LLM models
|
|
673
|
-
/project - Switch between Orquesta projects
|
|
674
|
-
/tool - Enable/disable optional tools (Browser, Background)
|
|
675
|
-
/load - Load a saved session
|
|
676
|
-
/usage - Show token usage statistics
|
|
677
|
-
/cost - Estimated USD spend this process (by model)
|
|
678
|
-
/route - Pin Batuta Auto tier (fast/balanced/premium/auto)
|
|
679
|
-
/sync - Bidirectional sync with Orquesta dashboard (pull & push LLM configs)
|
|
680
|
-
/login - Sign in to Orquesta via browser (opens getorquesta.com)
|
|
681
|
-
/logout - Sign out of Orquesta (clears token, keeps local LLM configs)
|
|
682
|
-
/whoami - Show current Orquesta connection
|
|
683
|
-
/hook - Claude Code hook here: /hook status | enable | disable
|
|
684
|
-
/remote-phone - Drive this session from your phone: on | off | status
|
|
685
|
-
/update - Update orquesta-cli to the latest version
|
|
686
|
-
|
|
687
|
-
Keyboard shortcuts:
|
|
688
|
-
Ctrl+C - Exit
|
|
689
|
-
Ctrl+T - Toggle TODO details
|
|
690
|
-
ESC - Interrupt current execution
|
|
691
|
-
@ - File browser
|
|
692
|
-
/ - Command autocomplete
|
|
693
|
-
|
|
694
|
-
Note: All conversations are automatically saved.
|
|
695
|
-
`;
|
|
696
|
-
const updatedMessages = [
|
|
697
|
-
...context.messages,
|
|
698
|
-
{ role: 'assistant', content: helpMessage },
|
|
699
|
-
];
|
|
700
|
-
context.setMessages(updatedMessages);
|
|
701
|
-
return {
|
|
702
|
-
handled: true,
|
|
703
|
-
shouldContinue: false,
|
|
704
|
-
updatedContext: {
|
|
705
|
-
messages: updatedMessages,
|
|
706
|
-
},
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
582
|
if (trimmedCommand.startsWith('/load')) {
|
|
710
583
|
logger.flow('Load command received');
|
|
711
584
|
const parts = trimmedCommand.split(' ');
|
|
@@ -4,12 +4,14 @@ import { CompactManager, contextTracker, buildCompactedMessages, } from '../core
|
|
|
4
4
|
import { configManager } from '../core/config/config-manager.js';
|
|
5
5
|
import { setTodoWriteCallback, clearTodoCallbacks, } from '../tools/llm/simple/todo-tools.js';
|
|
6
6
|
import { setGetTodosCallback, setFinalResponseCallback, setMarkTodosCompletedCallback, clearFinalResponseCallbacks, } from '../tools/llm/simple/final-response-tool.js';
|
|
7
|
+
import { eventBus, Events } from '../core/event-bus.js';
|
|
7
8
|
import { setDocsSearchLLMClientGetter, clearDocsSearchLLMClientGetter, } from '../tools/llm/simple/docs-search-agent-tool.js';
|
|
8
9
|
import { emitPlanCreated, emitTodoStart, emitTodoComplete, emitTodoFail, emitCompact, emitAssistantResponse, } from '../tools/llm/simple/file-tools.js';
|
|
9
10
|
import { toolRegistry } from '../tools/registry.js';
|
|
10
11
|
import { PLAN_EXECUTE_SYSTEM_PROMPT as PLAN_PROMPT } from '../prompts/system/plan-execute.js';
|
|
11
12
|
import { getProjectContext } from '../core/project-context.js';
|
|
12
13
|
import { getMemoryPrompt } from '../core/memory.js';
|
|
14
|
+
import { getGitContextPrompt } from '../core/git-context.js';
|
|
13
15
|
import { GIT_COMMIT_RULES } from '../prompts/shared/git-rules.js';
|
|
14
16
|
import { logger } from '../utils/logger.js';
|
|
15
17
|
import { getStreamLogger } from '../utils/json-stream-logger.js';
|
|
@@ -34,7 +36,7 @@ function buildSystemPrompt() {
|
|
|
34
36
|
const projectContext = getProjectContext();
|
|
35
37
|
const base = isGitRepo ? `${PLAN_PROMPT}\n\n${GIT_COMMIT_RULES}` : PLAN_PROMPT;
|
|
36
38
|
const appended = appendedSystemPrompt ? `\n\n${appendedSystemPrompt}` : '';
|
|
37
|
-
return base + buildEnvironmentContext() + projectContext + getMemoryPrompt() + appended;
|
|
39
|
+
return base + buildEnvironmentContext() + projectContext + getMemoryPrompt() + getGitContextPrompt() + appended;
|
|
38
40
|
}
|
|
39
41
|
export class PlanExecutor {
|
|
40
42
|
currentLLMClient = null;
|
|
@@ -80,74 +82,82 @@ export class PlanExecutor {
|
|
|
80
82
|
throw new Error('INTERRUPTED');
|
|
81
83
|
}
|
|
82
84
|
let currentMessages = messages;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
planningLLM.setAskUserCallback(callbacks.askUser);
|
|
85
|
+
const isSimpleTask = userMessage.length < 500 &&
|
|
86
|
+
!/\b(and then|after that|first.*then|step \d|multiple|several|refactor.*entire|migrate|rewrite.*all)\b/i.test(userMessage);
|
|
87
|
+
if (isSimpleTask) {
|
|
88
|
+
logger.flow('Simple task detected — skipping planner, executor will handle directly');
|
|
89
|
+
streamLogger?.logPlanningEnd(0, [], false, 0);
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
else {
|
|
92
|
+
callbacks.setCurrentActivity('Thinking');
|
|
93
|
+
const plannerModel = configManager.getRoleModel('planner');
|
|
94
|
+
const planningLLM = new PlanningLLM(llmClient, plannerModel ?? undefined);
|
|
95
|
+
const plannerStartedAt = Date.now();
|
|
96
|
+
if (callbacks.askUser) {
|
|
97
|
+
planningLLM.setAskUserCallback(callbacks.askUser);
|
|
98
|
+
}
|
|
99
|
+
const planResult = await planningLLM.generateTODOListWithDocsDecision(userMessage, currentMessages);
|
|
100
|
+
auditLog.emit(auditSid, 'planner.complete', {
|
|
101
|
+
runId,
|
|
102
|
+
model: plannerModel,
|
|
103
|
+
durationMs: Date.now() - plannerStartedAt,
|
|
104
|
+
todoCount: planResult.todos.length,
|
|
105
|
+
directResponse: !!planResult.directResponse,
|
|
103
106
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
if (planResult.clarificationMessages?.length) {
|
|
108
|
+
currentMessages = [...currentMessages, ...planResult.clarificationMessages];
|
|
109
|
+
callbacks.setMessages([...currentMessages]);
|
|
110
|
+
logger.flow('Added planning clarification messages to history', {
|
|
111
|
+
count: planResult.clarificationMessages.length,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (planResult.directResponse) {
|
|
115
|
+
logger.flow('Direct response - no execution needed');
|
|
116
|
+
streamLogger?.logPlanningEnd(0, [], true, Date.now() - planningStartTime);
|
|
117
|
+
const lastMsg = currentMessages[currentMessages.length - 1];
|
|
118
|
+
const needsUserMessage = !(lastMsg?.role === 'user' && lastMsg?.content === userMessage);
|
|
119
|
+
const updatedMessages = needsUserMessage
|
|
120
|
+
? [
|
|
121
|
+
...currentMessages,
|
|
122
|
+
{ role: 'user', content: userMessage },
|
|
123
|
+
{ role: 'assistant', content: planResult.directResponse }
|
|
124
|
+
]
|
|
125
|
+
: [
|
|
126
|
+
...currentMessages,
|
|
127
|
+
{ role: 'assistant', content: planResult.directResponse }
|
|
128
|
+
];
|
|
129
|
+
emitAssistantResponse(planResult.directResponse);
|
|
130
|
+
callbacks.setMessages([...updatedMessages]);
|
|
131
|
+
sessionManager.autoSaveCurrentSession(updatedMessages);
|
|
132
|
+
callbacks.setExecutionPhase('idle');
|
|
133
|
+
logger.exit('PlanExecutor.executePlanMode', { directResponse: true });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
currentTodos = planResult.todos;
|
|
137
|
+
streamLogger?.logPlanningEnd(currentTodos.length, currentTodos.map(t => ({ id: t.id, title: t.title, status: t.status })), false, Date.now() - planningStartTime);
|
|
138
|
+
logger.vars({ name: 'todoCount', value: currentTodos.length }, { name: 'docsSearchNeeded', value: planResult.docsSearchNeeded });
|
|
139
|
+
callbacks.setTodos(currentTodos);
|
|
140
|
+
emitPlanCreated(currentTodos.map(t => t.title));
|
|
141
|
+
const planMessage = planResult.docsSearchNeeded
|
|
142
|
+
? `📋 Created ${currentTodos.length} tasks (including docs search). Starting execution...`
|
|
143
|
+
: `📋 Created ${currentTodos.length} tasks. Starting execution...`;
|
|
144
|
+
const lastMsgForPlan = currentMessages[currentMessages.length - 1];
|
|
145
|
+
const needsUserMessageForPlan = !(lastMsgForPlan?.role === 'user' && lastMsgForPlan?.content === userMessage);
|
|
146
|
+
currentMessages = needsUserMessageForPlan
|
|
111
147
|
? [
|
|
112
148
|
...currentMessages,
|
|
113
149
|
{ role: 'user', content: userMessage },
|
|
114
|
-
{ role: 'assistant', content:
|
|
150
|
+
{ role: 'assistant', content: planMessage }
|
|
115
151
|
]
|
|
116
152
|
: [
|
|
117
153
|
...currentMessages,
|
|
118
|
-
{ role: 'assistant', content:
|
|
154
|
+
{ role: 'assistant', content: planMessage }
|
|
119
155
|
];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
logger.exit('PlanExecutor.executePlanMode', { directResponse: true });
|
|
125
|
-
return;
|
|
156
|
+
callbacks.setMessages(currentMessages);
|
|
157
|
+
this.setupTodoCallbacks(currentTodos, callbacks, (updated) => {
|
|
158
|
+
currentTodos = updated;
|
|
159
|
+
});
|
|
126
160
|
}
|
|
127
|
-
currentTodos = planResult.todos;
|
|
128
|
-
streamLogger?.logPlanningEnd(currentTodos.length, currentTodos.map(t => ({ id: t.id, title: t.title, status: t.status })), false, Date.now() - planningStartTime);
|
|
129
|
-
logger.vars({ name: 'todoCount', value: currentTodos.length }, { name: 'docsSearchNeeded', value: planResult.docsSearchNeeded });
|
|
130
|
-
callbacks.setTodos(currentTodos);
|
|
131
|
-
emitPlanCreated(currentTodos.map(t => t.title));
|
|
132
|
-
const planMessage = planResult.docsSearchNeeded
|
|
133
|
-
? `📋 Created ${currentTodos.length} tasks (including docs search). Starting execution...`
|
|
134
|
-
: `📋 Created ${currentTodos.length} tasks. Starting execution...`;
|
|
135
|
-
const lastMsgForPlan = currentMessages[currentMessages.length - 1];
|
|
136
|
-
const needsUserMessageForPlan = !(lastMsgForPlan?.role === 'user' && lastMsgForPlan?.content === userMessage);
|
|
137
|
-
currentMessages = needsUserMessageForPlan
|
|
138
|
-
? [
|
|
139
|
-
...currentMessages,
|
|
140
|
-
{ role: 'user', content: userMessage },
|
|
141
|
-
{ role: 'assistant', content: planMessage }
|
|
142
|
-
]
|
|
143
|
-
: [
|
|
144
|
-
...currentMessages,
|
|
145
|
-
{ role: 'assistant', content: planMessage }
|
|
146
|
-
];
|
|
147
|
-
callbacks.setMessages(currentMessages);
|
|
148
|
-
this.setupTodoCallbacks(currentTodos, callbacks, (updated) => {
|
|
149
|
-
currentTodos = updated;
|
|
150
|
-
});
|
|
151
161
|
callbacks.setExecutionPhase('executing');
|
|
152
162
|
const tools = toolRegistry.getLLMToolDefinitions();
|
|
153
163
|
const hasSystemMessage = currentMessages.some(m => m.role === 'system');
|
|
@@ -198,12 +208,9 @@ export class PlanExecutor {
|
|
|
198
208
|
}
|
|
199
209
|
else {
|
|
200
210
|
const todoContext = buildTodoContext(currentTodos);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
? { ...m, content: m.content + todoContext }
|
|
205
|
-
: m)
|
|
206
|
-
: [...currentMessages, { role: 'user', content: `Execute the TODO list.${todoContext}` }];
|
|
211
|
+
const messagesForLLM = todoContext
|
|
212
|
+
? [...currentMessages, { role: 'user', content: `[Current task status]${todoContext}` }]
|
|
213
|
+
: currentMessages;
|
|
207
214
|
const executorModel = configManager.getRoleModel('executor');
|
|
208
215
|
const result = await llmClient.chatCompletionWithTools(messagesForLLM, tools, {
|
|
209
216
|
getPendingMessage: callbacks.getPendingMessage,
|
|
@@ -314,12 +321,9 @@ export class PlanExecutor {
|
|
|
314
321
|
const activeTodo = findActiveTodo(currentTodos);
|
|
315
322
|
callbacks.setCurrentActivity(activeTodo?.title || 'Working on tasks');
|
|
316
323
|
const todoContext = buildTodoContext(currentTodos);
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
? { ...m, content: m.content + todoContext }
|
|
321
|
-
: m)
|
|
322
|
-
: [...currentMessages, { role: 'user', content: `Resume the TODO list.${todoContext}` }];
|
|
324
|
+
const messagesForLLM = todoContext
|
|
325
|
+
? [...currentMessages, { role: 'user', content: `[Current task status]${todoContext}` }]
|
|
326
|
+
: currentMessages;
|
|
323
327
|
const executorModel = configManager.getRoleModel('executor');
|
|
324
328
|
const result = await llmClient.chatCompletionWithTools(messagesForLLM, tools, {
|
|
325
329
|
getPendingMessage: callbacks.getPendingMessage,
|
|
@@ -460,6 +464,8 @@ export class PlanExecutor {
|
|
|
460
464
|
setFinalResponseCallback((message) => {
|
|
461
465
|
emitAssistantResponse(message);
|
|
462
466
|
});
|
|
467
|
+
void eventBus.on(Events.FINAL_RESPONSE, (_message) => {
|
|
468
|
+
});
|
|
463
469
|
setMarkTodosCompletedCallback(() => {
|
|
464
470
|
const completed = todosRef.map(t => t.status === 'completed' || t.status === 'failed'
|
|
465
471
|
? t
|
|
@@ -22,7 +22,6 @@ export const AVAILABLE_TOOLS_WITH_TODO = `
|
|
|
22
22
|
- **tell_to_user**: Send status updates to the user
|
|
23
23
|
- **ask_to_user**: Ask user a question with multiple choice options
|
|
24
24
|
- **write_todos**: Update entire TODO list (replaces current list)
|
|
25
|
-
- **call_docs_search_agent**: Search local documentation (~/.local-cli/docs)
|
|
26
25
|
`.trim();
|
|
27
26
|
export const TOOL_REASON_GUIDE = `
|
|
28
27
|
## CRITICAL - Tool "reason" Parameter
|