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.
Files changed (38) hide show
  1. package/dist/agents/planner/index.js +2 -1
  2. package/dist/cli.js +17 -16
  3. package/dist/constants.d.ts +1 -1
  4. package/dist/constants.js +1 -1
  5. package/dist/core/commands/clear.d.ts +3 -0
  6. package/dist/core/commands/clear.js +22 -0
  7. package/dist/core/commands/compact.d.ts +3 -0
  8. package/dist/core/commands/compact.js +45 -0
  9. package/dist/core/commands/help.d.ts +3 -0
  10. package/dist/core/commands/help.js +50 -0
  11. package/dist/core/commands/index.d.ts +3 -0
  12. package/dist/core/commands/index.js +11 -0
  13. package/dist/core/commands/memory.d.ts +3 -0
  14. package/dist/core/commands/memory.js +40 -0
  15. package/dist/core/commands/registry.d.ts +11 -0
  16. package/dist/core/commands/registry.js +25 -0
  17. package/dist/core/commands/types.d.ts +10 -0
  18. package/dist/core/commands/types.js +2 -0
  19. package/dist/core/event-bus.d.ts +20 -0
  20. package/dist/core/event-bus.js +35 -0
  21. package/dist/core/git-context.d.ts +11 -0
  22. package/dist/core/git-context.js +62 -0
  23. package/dist/core/ignore-filter.d.ts +4 -0
  24. package/dist/core/ignore-filter.js +50 -0
  25. package/dist/core/llm/llm-client.d.ts +1 -0
  26. package/dist/core/llm/llm-client.js +118 -40
  27. package/dist/core/onboarding.d.ts +3 -0
  28. package/dist/core/onboarding.js +48 -0
  29. package/dist/core/slash-command-handler.js +8 -135
  30. package/dist/orchestration/plan-executor.js +77 -71
  31. package/dist/prompts/shared/tool-usage.js +0 -1
  32. package/dist/prompts/system/plan-execute.js +50 -57
  33. package/dist/tools/llm/simple/file-tools.js +12 -1
  34. package/dist/tools/llm/simple/final-response-tool.js +7 -11
  35. package/dist/tools/registry.js +63 -10
  36. package/dist/ui/components/PlanExecuteApp.d.ts +1 -0
  37. package/dist/ui/components/PlanExecuteApp.js +59 -22
  38. 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.7 });
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.7,
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
- const response = await this.axiosInstance.post(url, requestBody, {
214
- signal: this.currentAbortController.signal,
215
- headers: buildPerRequestHeaders(),
216
- });
217
- this.currentAbortController = null;
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.7,
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: 'required',
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
- noToolCallRetries++;
737
- logger.flow(`No tool call - enforcing tool usage (attempt ${noToolCallRetries}/${MAX_NO_TOOL_CALL_RETRIES})`);
738
- if (noToolCallRetries > MAX_NO_TOOL_CALL_RETRIES) {
739
- logger.warn('Max no-tool-call retries exceeded - returning content as final response');
740
- const fallbackContent = assistantMessage.content || 'Task completed.';
741
- const { emitAssistantResponse } = await import('../../tools/llm/simple/file-tools.js');
742
- emitAssistantResponse(fallbackContent);
743
- return {
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,3 @@
1
+ export declare function shouldShowOnboarding(): boolean;
2
+ export declare function runOnboarding(): Promise<boolean>;
3
+ //# sourceMappingURL=onboarding.d.ts.map
@@ -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, resetBatutaSession } from './routing-state.js';
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
- callbacks.setCurrentActivity('Thinking');
84
- const plannerModel = configManager.getRoleModel('planner');
85
- const planningLLM = new PlanningLLM(llmClient, plannerModel ?? undefined);
86
- const plannerStartedAt = Date.now();
87
- if (callbacks.askUser) {
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
- const planResult = await planningLLM.generateTODOListWithDocsDecision(userMessage, currentMessages);
91
- auditLog.emit(auditSid, 'planner.complete', {
92
- runId,
93
- model: plannerModel,
94
- durationMs: Date.now() - plannerStartedAt,
95
- todoCount: planResult.todos.length,
96
- directResponse: !!planResult.directResponse,
97
- });
98
- if (planResult.clarificationMessages?.length) {
99
- currentMessages = [...currentMessages, ...planResult.clarificationMessages];
100
- callbacks.setMessages([...currentMessages]);
101
- logger.flow('Added planning clarification messages to history', {
102
- count: planResult.clarificationMessages.length,
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
- if (planResult.directResponse) {
106
- logger.flow('Direct response - no execution needed');
107
- streamLogger?.logPlanningEnd(0, [], true, Date.now() - planningStartTime);
108
- const lastMsg = currentMessages[currentMessages.length - 1];
109
- const needsUserMessage = !(lastMsg?.role === 'user' && lastMsg?.content === userMessage);
110
- const updatedMessages = needsUserMessage
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: planResult.directResponse }
150
+ { role: 'assistant', content: planMessage }
115
151
  ]
116
152
  : [
117
153
  ...currentMessages,
118
- { role: 'assistant', content: planResult.directResponse }
154
+ { role: 'assistant', content: planMessage }
119
155
  ];
120
- emitAssistantResponse(planResult.directResponse);
121
- callbacks.setMessages([...updatedMessages]);
122
- sessionManager.autoSaveCurrentSession(updatedMessages);
123
- callbacks.setExecutionPhase('idle');
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 lastUserMsgIndex = currentMessages.map(m => m.role).lastIndexOf('user');
202
- const messagesForLLM = lastUserMsgIndex >= 0
203
- ? currentMessages.map((m, i) => i === lastUserMsgIndex
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 lastUserMsgIndex = currentMessages.map(m => m.role).lastIndexOf('user');
318
- const messagesForLLM = lastUserMsgIndex >= 0
319
- ? currentMessages.map((m, i) => i === lastUserMsgIndex
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