ft-scout 8.0.0 → 8.0.2

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.
@@ -9,7 +9,7 @@ import { saveAgentSession, getRecentSessionsSummary } from '../utils/session.js'
9
9
  import { safeNote, renderMarkdown } from '../utils/markdown.js';
10
10
  import { openai, callOpenAIWithRetry, isQuotaExceededError, sanitizeMessage, sanitizeMessages } from './llm.js';
11
11
  import { checkFileSyntax, verifyAndSelfHealFiles, executeSmartCommand, extractErrorDiagnostics } from './verifier.js';
12
- import { openApp, executeInApp } from './appControl.js';
12
+ import { openApp, executeInApp, cleanupScreenshots } from './appControl.js';
13
13
  import { speakText, listenSpeechToText } from './voiceEngine.js';
14
14
  const execAsync = promisify(exec);
15
15
  export function robustSnippetReplace(origContent, target, replacement) {
@@ -284,13 +284,13 @@ export const AGENT_TOOLS = [
284
284
  },
285
285
  {
286
286
  name: 'app_action',
287
- description: 'Execute interactive desktop/browser scratchpad automation actions, screen takeover, mouse cursor positioning & click takeover, keyboard keystrokes, hotkeys, screen seeing/screenshot capture, visual screen analysis, focus locks, or command sequences without shell commands (e.g. action: "takeover", "capture_screen", "see_screen", "analyze_screen", "get_screen_info", "click_app", "move_mouse", "type_text", "send_keys", "key_combo", "lock_app", "fetch_page", "navigate", "search", "send_dm", "exec_command", "open_file").',
287
+ description: 'Execute interactive desktop/browser scratchpad automation actions, screen seeing & analysis, UI element clicking, screen takeover, mouse cursor positioning & click takeover, mouse scrolling, window management, keyboard keystrokes, hotkeys, focus locks, or command sequences without shell commands (e.g. action: "see_screen", "analyze_screen", "capture_screen", "click_element", "click_app", "scroll", "list_windows", "focus_window", "takeover", "move_mouse", "type_text", "send_keys", "key_combo", "lock_app", "fetch_page", "navigate", "search", "send_dm", "exec_command", "open_file"). All temporary screenshot files are automatically deleted upon task completion.',
288
288
  parameters: {
289
289
  type: 'object',
290
290
  properties: {
291
291
  app: { type: 'string', description: 'Target application name, window title, screen, or category ("browser", "terminal", "editor", "notepad", "chrome", "desktop")' },
292
- action: { type: 'string', description: 'Action type ("takeover", "capture_screen", "see_screen", "analyze_screen", "get_screen_info", "type_text", "click_app", "move_mouse", "send_keys", "key_combo", "lock_app", "fetch_page", "search", "send_dm", "exec_command", "open_file")' },
293
- payload: { description: 'Action details/payload object or string (e.g. text string, { text: "...", enter: true }, { x: 100, y: 200, button: "left" }, { keyCombo: "ctrl+v" }, { duration: 3000 }, URL, search query, command, or file path)' },
292
+ action: { type: 'string', description: 'Action type ("see_screen", "analyze_screen", "capture_screen", "click_element", "click_app", "scroll", "list_windows", "focus_window", "takeover", "send_mail", "type_text", "move_mouse", "send_keys", "key_combo", "lock_app", "fetch_page", "search", "send_dm", "exec_command", "open_file")' },
293
+ payload: { description: 'Action details/payload object or string (e.g. element name/description to click { element: "Search Google" }, coordinates { x: 100, y: 200 }, scroll { direction: "down", amount: 4 }, text string, { text: "...", enter: true }, { keyCombo: "ctrl+v" }, { duration: 3000 }, URL, search query, command, or file path)' },
294
294
  },
295
295
  required: ['app', 'action'],
296
296
  },
@@ -405,6 +405,18 @@ export class AgentExecutionLoop {
405
405
  this.autoApprove = Boolean(options?.autoApprove);
406
406
  this.maxSteps = options?.maxSteps || 30;
407
407
  this.projectName = options?.projectName || path.basename(this.cwd);
408
+ // Ensure temporary screenshot cleanup on early exit or interrupt
409
+ const onExitOrInterrupt = () => {
410
+ try {
411
+ cleanupScreenshots();
412
+ }
413
+ catch { }
414
+ };
415
+ process.once('exit', onExitOrInterrupt);
416
+ process.once('SIGINT', () => {
417
+ onExitOrInterrupt();
418
+ process.exit(0);
419
+ });
408
420
  }
409
421
  getModifiedFiles() {
410
422
  return Array.from(this.modifiedFiles);
@@ -472,6 +484,51 @@ export class AgentExecutionLoop {
472
484
  }
473
485
  return summary;
474
486
  }
487
+ autoSyncScratchpadFromText(text) {
488
+ try {
489
+ const planLines = text.match(/[-*]\s*\[([ xX✓/])\]\s*(.*)/g);
490
+ if (planLines && planLines.length > 0) {
491
+ const plan = [];
492
+ const completed = [];
493
+ planLines.forEach((l, idx) => {
494
+ const isDone = l.includes('[x]') || l.includes('[X]') || l.includes('[✓]');
495
+ const cleanText = l.replace(/^[-*]\s*\[[ xX✓/]\]\s*/, '').trim();
496
+ plan.push(cleanText);
497
+ if (isDone)
498
+ completed.push(idx);
499
+ });
500
+ this.scratchpadState.plan = plan;
501
+ this.scratchpadState.completedSteps = completed;
502
+ }
503
+ const obsMatch = text.match(/Observations?:\s*([^\n]+(?:\n[^\n]+)*)/i);
504
+ const thoughtMatch = text.match(/Thought:\s*([^\n]+(?:\n[^\n]+)*)/i);
505
+ const notes = [
506
+ thoughtMatch ? `Thought: ${thoughtMatch[1]?.trim()}` : '',
507
+ obsMatch ? `Observations: ${obsMatch[1]?.trim()}` : '',
508
+ ].filter(Boolean).join('\n\n');
509
+ if (notes) {
510
+ this.scratchpadState.notes = notes;
511
+ }
512
+ const ftDir = path.join(this.cwd, '.ft');
513
+ if (!fs.existsSync(ftDir))
514
+ fs.mkdirSync(ftDir, { recursive: true });
515
+ const scratchpadPath = path.join(ftDir, 'scratchpad.md');
516
+ let mdContent = `# Scout Agent Scratchpad & Working Memory\n\n`;
517
+ if (this.scratchpadState.plan.length > 0) {
518
+ mdContent += `## Plan Checklist\n`;
519
+ this.scratchpadState.plan.forEach((item, idx) => {
520
+ const isDone = this.scratchpadState.completedSteps.includes(idx);
521
+ mdContent += `- [${isDone ? 'x' : ' '}] Step ${idx + 1}: ${item}\n`;
522
+ });
523
+ mdContent += `\n`;
524
+ }
525
+ if (this.scratchpadState.notes) {
526
+ mdContent += `## Working Memory & Observations\n${this.scratchpadState.notes}\n`;
527
+ }
528
+ fs.writeFileSync(scratchpadPath, mdContent, 'utf-8');
529
+ }
530
+ catch { }
531
+ }
475
532
  handleToolConsecutiveTracking(fnName, step) {
476
533
  if (['read_file', 'list_dir', 'glob_search', 'grep_search', 'tree_view', 'file_info'].includes(fnName)) {
477
534
  this.consecutiveReads++;
@@ -541,17 +598,36 @@ Core Directives & Behavioral Guidelines:
541
598
  19. EXTERNAL APP TAKEOVER & CONTROL PROTOCOL: When requested by user prompt to open, take over, or work inside external applications (browser, terminal, VS Code, Notepad, social apps like Instagram, WhatsApp, Twitter/X, Telegram, or custom apps):
542
599
  a. Launch App: Use \`open_app\` to launch or focus the target application with optional URL, file path, or initial script.
543
600
  b. Social DM & Messaging Automation: For Instagram, WhatsApp, Twitter/X, or Telegram messaging requests (e.g. "open instagram and send message to @user"), immediately invoke \`app_action\` with action "send_dm" or "open_dm" (or \`open_app\`) specifying the target username/phone and message text so the agent automatically opens the direct messaging link in the browser!
544
- c. Work Inside App: Use \`app_action\` or \`run_command\` to execute actions inside the app context (e.g. fetching browser page content, running commands inside terminal, searching web, opening files in editor).
545
- 20. RECKLESS SCRATCHPAD DESKTOP CONTROL, SCREEN TAKEOVER & VISUAL ANALYSIS (ZERO SHELL COMMANDS):
546
- a. Mouse & Keyboard Screen Takeover: During scratchpad execution, you act as a full desktop automation agent that takes over the mouse cursor, screen focus, and keyboard typing on the specified screen/app using \`app_action\` (\`action: "takeover"\`, \`action: "click_app"\`, \`action: "move_mouse"\`, \`action: "type_text"\`, \`action: "send_keys"\`, \`action: "key_combo"\`).
547
- b. Native Screen Seeing & Visual Analysis (No Shell Commands): You can view, capture, and visually analyze the specified screen/window directly using \`app_action\` (\`action: "capture_screen"\`, \`action: "see_screen"\`, \`action: "analyze_screen"\`, \`action: "get_screen_info"\`). DO NOT run shell commands (\`run_command\`) to capture or inspect screens—\`app_action\` handles native screen capture, visual analysis, mouse takeover, and key input internally!
548
- c. CLI Credential Input Prompt: If an application requires login credentials, passwords, 2FA codes, or secret tokens to proceed, call \`ask_user\` tool with a clear prompt. This presents a secure, interactive input bar directly in the user's running terminal CLI. Once the user enters the secret, take the received input, inject it into the target application window via \`app_action\` (\`action: "type_text"\`).
549
- 21. MANDATORY TAKEOVER TOOL INVOCATION MANDATE: Whenever the user goal requests to open, launch, take over, click, type, or interact with an external app or desktop screen (e.g. 'take over browser and open website', 'open notepad and type', 'take over desktop'), YOU MUST CALL \`open_app\` AND \`app_action\` TOOLS (with \`action: "takeover"\`, \`action: "click_app"\`, \`action: "type_text"\`, \`action: "send_keys"\`, \`action: "capture_screen"\`) IN YOUR VERY FIRST STEP! You MUST NOT return a plain text answer or claim you did it in text without calling the tool! The tool calls ARE MANDATORY for executing the physical takeover.
601
+ c. Email & Gmail Automation: When requested to send or compose an email (e.g. "send mail to frontterrain@gmail.com saying...", "takeover mail.google.com and send mail"):
602
+ - Immediately invoke \`app_action\` with \`action: "send_mail"\` (or \`action: "send_dm"\`) specifying the target recipient email and body text. The agent automatically constructs the direct Gmail compose URL (\`https://mail.google.com/mail/?view=cm&fs=1&to=<recipient>&su=<subject>&body=<body>\`) which pre-populates the compose window and dispatches the email via Ctrl+Enter!
603
+ - Alternatively, navigate directly to \`https://mail.google.com/mail/?view=cm&fs=1&to=<recipient>&su=...&body=...\` and trigger hotkey \`app_action(action: "key_combo", payload: { keyCombo: "ctrl+enter" })\`. NEVER click blind coordinates like (20, 20) in a web browser!
604
+ d. Work Inside App: Use \`app_action\` or \`run_command\` to execute actions inside the app context (e.g. fetching browser page content, running commands inside terminal, searching web, opening files in editor).
605
+ 20. DESKTOP APP TAKEOVER & AUTOMATION PROTOCOL:
606
+ a. STEP 1 SCREEN TAKEOVER: Whenever the user goal asks to take over the screen, control the desktop, or automate an external app (e.g. 'take over screen', 'open maps and click', 'take over desktop', 'open notepad'):
607
+ - Your VERY FIRST tool call in Step 1 MUST be \`app_action\` with \`action: "takeover"\` (e.g. \`app: "desktop"\` or the target app).
608
+ - Calling \`app_action\` with \`action: "takeover"\` immediately activates the full-screen sky-blue aura HUD, displays the warning banner "⚡ Scout is on the screen.", blocks external input interruptions, and takes over the mouse cursor!
609
+ - Then immediately proceed to launch/navigate with \`open_app\` or \`app_action\` (\`action: "click_element"\` / \`action: "click_app"\` / \`action: "type_text"\` / \`action: "send_keys"\`).
610
+ b. SCREEN CAPTURE & VISION DIRECTIVE: You CAN take screenshots and visually inspect or analyze the screen using \`app_action\` with \`action: "see_screen"\`, \`"analyze_screen"\`, or \`"capture_screen"\` whenever needed to verify UI state, check screen contents, or inspect open windows. All temporary screenshot files are automatically and securely deleted upon task completion for privacy and storage cleanliness.
611
+ c. VISUAL ELEMENT GROUNDING (click_element): Instead of guessing blind coordinates (x, y), click buttons, inputs, or menus by descriptive label using \`app_action(action: "click_element", payload: { element: "Search" })\`. Vision AI and native OS UI automation will locate the element and click it accurately.
612
+ d. WINDOW & SCROLL CONTROLS: Use \`app_action(action: "scroll", payload: { direction: "down", amount: 4 })\` to scroll pages. Use \`app_action(action: "list_windows")\` to see all open windows, and \`app_action(action: "focus_window", payload: { app: "chrome" })\` to bring a window front-and-center.
613
+ e. CLI Credential Input Prompt: If an application requires login credentials, passwords, 2FA codes, or secret tokens to proceed, call \`ask_user\` tool with a clear prompt. This presents a secure, interactive input bar directly in the user's running terminal CLI. Once the user enters the secret, take the received input, inject it into the target application window via \`app_action\` (\`action: "type_text"\`).
614
+ 21. MANDATORY TAKEOVER TOOL INVOCATION MANDATE: Whenever the user goal requests to open, launch, take over, click, type, or interact with an external app or desktop screen (e.g. 'take over browser and open website', 'open notepad and type', 'take over desktop', 'take over screen'):
615
+ YOU MUST CALL \`app_action(action: "takeover")\` IN STEP 1. Then call \`open_app\` and \`app_action\` (\`click_element\`, \`click_app\`, \`type_text\`, \`send_keys\`). DO NOT call \`read_file\` or \`write_file\` for workspace code files when asked to take over external desktop apps! The takeover tool call is MANDATORY for executing the physical takeover.
550
616
  22. BROWSER DIRECT URL NAVIGATION MANDATE: When asked to open or navigate to a specific website or web app (e.g. Apple Maps, GitHub, YouTube, etc.), NEVER call Google Search or issue repeated \`app_action: search\` calls with text queries! IMMEDIATELY pass the exact URL (e.g. "https://maps.apple.com") to \`open_app(app: "browser", target: "https://maps.apple.com")\` or \`app_action(app: "browser", action: "navigate", payload: { url: "https://maps.apple.com" })\`. Direct URL navigation must always target the exact site URL directly without putting queries into Google Search!
551
- 23. WORKING MEMORY & SCRATCHPAD PROTOCOL: For multi-step complex goals, maintain an active working memory scratchpad using the \`agent_scratchpad\` tool.
552
- a. Plan Checklist: Call \`agent_scratchpad(action: "update", plan: ["step 1", "step 2", ...])\` at the start of complex multi-step tasks.
553
- b. Reasoning Block: In every step, write a clear 1-2 sentence \`<scratchpad>\` reasoning block in your response before invoking tool calls to explain current state, working notes, and step selection.
554
- c. Real-Time Tracking: Update completed steps using \`agent_scratchpad(action: "update", completedSteps: [...])\` as you finish tasks so state is saved in \`.ft/scratchpad.md\`.
617
+ 23. WORKING MEMORY & STRUCTURED SCRATCHPAD PROTOCOL:
618
+ Like Antigravity and leading autonomous agents, maintain disciplined working memory. In EVERY step, start your response with a structured <scratchpad> reasoning block before returning tool calls:
619
+ \`\`\`markdown
620
+ <scratchpad>
621
+ Thought: [1-2 sentences on what you are doing on this turn and why]
622
+ Plan:
623
+ [x] 1. [Completed step]
624
+ [/] 2. [In-progress step]
625
+ [ ] 3. [Next upcoming step]
626
+ Observations: [What you learned from the last tool result or screen capture]
627
+ Next Action: [The exact tool you are invoking now]
628
+ </scratchpad>
629
+ \`\`\`
630
+ This keeps your reasoning crystal-clear, ensures plan progression, and syncs automatically with .ft/scratchpad.md.
555
631
 
556
632
  ${this.getScratchpadPromptContext()}
557
633
 
@@ -695,192 +771,225 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
695
771
  let step = 0;
696
772
  let finalSummary = '';
697
773
  const stepDurations = [];
698
- while (step < this.maxSteps) {
699
- step++;
700
- this.currentStep = step;
701
- const stepStartTime = Date.now();
702
- const avgStepMs = stepDurations.length > 0
703
- ? stepDurations.reduce((a, b) => a + b, 0) / stepDurations.length
704
- : 12000;
705
- const remainingSteps = (this.maxSteps - step + 1);
706
- const estSecsLeft = Math.max(5, Math.round((remainingSteps * avgStepMs) / 1000));
707
- const estLeftStr = estSecsLeft >= 60
708
- ? `${Math.floor(estSecsLeft / 60)}m ${estSecsLeft % 60}s`
709
- : `${estSecsLeft}s`;
710
- const stepSpinner = spinner();
711
- stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step}/${this.maxSteps} • Est. completion: ~${estLeftStr} left)`));
712
- try {
713
- let response;
774
+ try {
775
+ while (step < this.maxSteps) {
776
+ step++;
777
+ this.currentStep = step;
778
+ const stepStartTime = Date.now();
779
+ const avgStepMs = stepDurations.length > 0
780
+ ? stepDurations.reduce((a, b) => a + b, 0) / stepDurations.length
781
+ : 12000;
782
+ const remainingSteps = (this.maxSteps - step + 1);
783
+ const estSecsLeft = Math.max(5, Math.round((remainingSteps * avgStepMs) / 1000));
784
+ const estLeftStr = estSecsLeft >= 60
785
+ ? `${Math.floor(estSecsLeft / 60)}m ${estSecsLeft % 60}s`
786
+ : `${estSecsLeft}s`;
787
+ const stepSpinner = spinner();
788
+ stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step}/${this.maxSteps} • Est. completion: ~${estLeftStr} left)`));
714
789
  try {
715
- response = await callOpenAIWithRetry(async (model) => {
716
- return await openai.chat.completions.create({
717
- model,
718
- messages: sanitizeMessages(this.historyMessages),
719
- tools: AGENT_TOOLS.map((t) => ({ type: 'function', function: t })),
720
- tool_choice: 'auto',
721
- temperature: 0.1,
722
- });
723
- });
724
- }
725
- catch (err) {
726
- const errStr = String(err?.message || err?.error || err || '').toLowerCase();
727
- if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported') || errStr.includes('reasoning')) {
790
+ let response;
791
+ try {
728
792
  response = await callOpenAIWithRetry(async (model) => {
729
793
  return await openai.chat.completions.create({
730
794
  model,
731
795
  messages: sanitizeMessages(this.historyMessages),
796
+ tools: AGENT_TOOLS.map((t) => ({ type: 'function', function: t })),
797
+ tool_choice: 'auto',
732
798
  temperature: 0.1,
733
799
  });
734
800
  });
735
801
  }
736
- else {
737
- throw err;
738
- }
739
- }
740
- const choice = response.choices[0];
741
- if (!choice) {
742
- stepSpinner.stop(chalk.yellow('No response from AI model. Retrying step...'));
743
- continue;
744
- }
745
- const msg = sanitizeMessage(choice.message);
746
- this.historyMessages.push(msg);
747
- // Render assistant's thought/reasoning text if provided on this turn
748
- const textContent = msg.content || '';
749
- if (textContent.trim()) {
750
- safeNote(renderMarkdown(textContent), `Scout Thought (Step ${step})`);
751
- }
752
- // Check native tool calls
753
- if (msg.tool_calls && msg.tool_calls.length > 0) {
754
- stepSpinner.stop(chalk.green(`Step ${step}: Scout issued ${msg.tool_calls.length} tool call(s).`));
755
- for (const tc of msg.tool_calls) {
756
- if (tc.type === 'function' && tc.function) {
757
- const fnName = tc.function.name;
758
- let args = {};
759
- try {
760
- args = JSON.parse(tc.function.arguments || '{}');
761
- }
762
- catch { }
763
- const toolResult = await this.dispatchToolCall(fnName, args);
764
- this.historyMessages.push({
802
+ catch (err) {
803
+ const errStr = String(err?.message || err?.error || err || '').toLowerCase();
804
+ if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported') || errStr.includes('reasoning')) {
805
+ // Model doesn't support native function calling — inject tool-call formatting hint
806
+ // so extractJsonToolCall can parse the response as a structured tool invocation
807
+ const toolHintMsg = {
765
808
  role: 'user',
766
- tool_call_id: tc.id,
767
- content: `Tool Execution Result (${fnName}):\n${toolResult.result}`,
809
+ content: `IMPORTANT: This model does not support native function/tool calling. You MUST format your tool invocations as a JSON code block in your response like this:
810
+ \`\`\`json
811
+ { "tool": "tool_name", "args": { ... } }
812
+ \`\`\`
813
+ Available tools: read_file, write_file, edit_file, run_command, list_dir, grep_search, glob_search, tree_view, file_info, multi_edit_file, fetch_url, git_diff, open_app, app_action, agent_scratchpad, ask_user, speak_text, task_completed.
814
+ For app takeover/control use: { "tool": "app_action", "args": { "app": "browser", "action": "takeover" } }
815
+ For opening apps use: { "tool": "open_app", "args": { "app": "chrome", "target": "https://..." } }
816
+ For clicking use: { "tool": "app_action", "args": { "app": "desktop", "action": "click_app", "payload": { "x": 500, "y": 300 } } }
817
+ For typing text use: { "tool": "app_action", "args": { "app": "browser", "action": "type_text", "payload": { "text": "...", "enter": true } } }
818
+ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what you would do in plain text—output the JSON tool call directly!`,
819
+ };
820
+ const fallbackMessages = [...this.historyMessages, toolHintMsg];
821
+ response = await callOpenAIWithRetry(async (model) => {
822
+ return await openai.chat.completions.create({
823
+ model,
824
+ messages: sanitizeMessages(fallbackMessages),
825
+ temperature: 0.1,
826
+ });
768
827
  });
769
- this.handleToolConsecutiveTracking(fnName, step);
770
- if (fnName === 'task_completed') {
771
- finalSummary = args.summary || toolResult.result;
772
- if (this.modifiedFiles.size > 0) {
773
- const filesList = getDirectoryFiles(this.cwd);
774
- const healRes = await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, this.projectName, filesList, { maxRetries: 3 });
775
- if (healRes.verifiedFiles.length > 0) {
776
- safeNote(chalk.green(` Self-Healing Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build clean!`), ' Code Verification Clean');
777
- }
778
- if (healRes.remainingErrors.length > 0) {
779
- safeNote(chalk.yellow(`️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '️ Verification Warning');
780
- }
828
+ }
829
+ else {
830
+ throw err;
831
+ }
832
+ }
833
+ const choice = response.choices[0];
834
+ if (!choice) {
835
+ stepSpinner.stop(chalk.yellow('No response from Scout. Retrying step...'));
836
+ continue;
837
+ }
838
+ const msg = sanitizeMessage(choice.message);
839
+ this.historyMessages.push(msg);
840
+ // Render assistant's thought/scratchpad reasoning text if provided on this turn
841
+ const textContent = msg.content || '';
842
+ if (textContent.trim()) {
843
+ const scratchMatch = textContent.match(/<scratchpad>([\s\S]*?)<\/scratchpad>/i);
844
+ if (scratchMatch && scratchMatch[1]) {
845
+ const scratchText = scratchMatch[1].trim();
846
+ safeNote(renderMarkdown(scratchText), chalk.cyan.bold(`Scout Scratchpad & Working Memory (Step ${step})`));
847
+ this.autoSyncScratchpadFromText(scratchText);
848
+ const remainingText = textContent.replace(/<scratchpad>[\s\S]*?<\/scratchpad>/i, '').trim();
849
+ if (remainingText) {
850
+ safeNote(renderMarkdown(remainingText), `Scout Thought (Step ${step})`);
851
+ }
852
+ }
853
+ else {
854
+ safeNote(renderMarkdown(textContent), `Scout Thought (Step ${step})`);
855
+ }
856
+ }
857
+ // Check native tool calls
858
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
859
+ stepSpinner.stop(chalk.green(`Step ${step}: Scout issued ${msg.tool_calls.length} tool call(s).`));
860
+ for (const tc of msg.tool_calls) {
861
+ if (tc.type === 'function' && tc.function) {
862
+ const fnName = tc.function.name;
863
+ let args = {};
864
+ try {
865
+ args = JSON.parse(tc.function.arguments || '{}');
781
866
  }
782
- saveAgentSession({
783
- goal: userGoal,
784
- summary: finalSummary,
785
- modifiedFiles: Array.from(this.modifiedFiles),
867
+ catch { }
868
+ const toolResult = await this.dispatchToolCall(fnName, args);
869
+ this.historyMessages.push({
870
+ role: 'user',
871
+ tool_call_id: tc.id,
872
+ content: `Tool Execution Result (${fnName}):\n${toolResult.result}`,
786
873
  });
787
- return { success: true, summary: finalSummary };
874
+ this.handleToolConsecutiveTracking(fnName, step);
875
+ if (fnName === 'task_completed') {
876
+ finalSummary = args.summary || toolResult.result;
877
+ if (this.modifiedFiles.size > 0) {
878
+ const filesList = getDirectoryFiles(this.cwd);
879
+ const healRes = await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, this.projectName, filesList, { maxRetries: 3 });
880
+ if (healRes.verifiedFiles.length > 0) {
881
+ safeNote(chalk.green(` Self-Healing Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build clean!`), ' Code Verification Clean');
882
+ }
883
+ if (healRes.remainingErrors.length > 0) {
884
+ safeNote(chalk.yellow(`️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '️ Verification Warning');
885
+ }
886
+ }
887
+ saveAgentSession({
888
+ goal: userGoal,
889
+ summary: finalSummary,
890
+ modifiedFiles: Array.from(this.modifiedFiles),
891
+ });
892
+ return { success: true, summary: finalSummary };
893
+ }
788
894
  }
789
895
  }
896
+ continue;
790
897
  }
791
- continue;
792
- }
793
- // Check text response or fallback JSON tool call
794
- stepSpinner.stop(chalk.blue(`Step ${step} thinking complete.`));
795
- const parsedJsonTool = this.extractJsonToolCall(textContent);
796
- if (parsedJsonTool) {
797
- const toolResult = await this.dispatchToolCall(parsedJsonTool.tool, parsedJsonTool.args);
898
+ // Check text response or fallback JSON tool call
899
+ stepSpinner.stop(chalk.blue(`Step ${step} thinking complete.`));
900
+ const parsedJsonTool = this.extractJsonToolCall(textContent);
901
+ if (parsedJsonTool) {
902
+ const toolResult = await this.dispatchToolCall(parsedJsonTool.tool, parsedJsonTool.args);
903
+ this.historyMessages.push({
904
+ role: 'user',
905
+ content: `Tool Execution Result (${parsedJsonTool.tool}):\n${toolResult.result}`,
906
+ });
907
+ this.handleToolConsecutiveTracking(parsedJsonTool.tool, step);
908
+ if (parsedJsonTool.tool === 'task_completed') {
909
+ finalSummary = parsedJsonTool.args?.summary || toolResult.result;
910
+ saveAgentSession({
911
+ goal: userGoal,
912
+ summary: finalSummary,
913
+ modifiedFiles: Array.from(this.modifiedFiles),
914
+ });
915
+ return { success: true, summary: finalSummary };
916
+ }
917
+ continue;
918
+ }
919
+ if (textContent.trim()) {
920
+ let cleanedThought = textContent
921
+ .replace(/[\u0600-\u06FF\u0750-\u077F\uAC00-\uD7AF\u3040-\u30FF\u4E00-\u9FFF\u0D80-\u0DFF]+/g, '')
922
+ .trim();
923
+ if (!cleanedThought || cleanedThought.length < 5) {
924
+ cleanedThought = 'Analyzing codebase files and executing next tool operation...';
925
+ }
926
+ safeNote(renderMarkdown(cleanedThought), ` Scout Agent Thought (Step ${step})`);
927
+ const lowerText = textContent.toLowerCase();
928
+ const isExplicitCompletion = lowerText.includes('task is complete') ||
929
+ lowerText.includes('task complete') ||
930
+ lowerText.includes('goal completed') ||
931
+ lowerText.includes('goal is completed') ||
932
+ lowerText.includes('all tasks completed') ||
933
+ lowerText.includes('i have completed') ||
934
+ lowerText.includes('no further changes needed') ||
935
+ lowerText.includes('the fix is complete') ||
936
+ lowerText.includes('has been created') ||
937
+ lowerText.includes('successfully created') ||
938
+ lowerText.includes('created the file') ||
939
+ lowerText.includes('file created') ||
940
+ lowerText.includes('implementation complete') ||
941
+ lowerText.includes('work is complete');
942
+ // If explicit completion phrase found, OR files have already been modified and assistant returned a final summary without calling tools
943
+ if (isExplicitCompletion || (this.modifiedFiles.size > 0 && !lowerText.includes('?') && textContent.length > 50)) {
944
+ saveAgentSession({
945
+ goal: userGoal,
946
+ summary: textContent,
947
+ modifiedFiles: Array.from(this.modifiedFiles),
948
+ });
949
+ return { success: true, summary: textContent };
950
+ }
951
+ }
952
+ // If assistant responded with text without calling tools, prompt it to execute tools to complete the goal
798
953
  this.historyMessages.push({
799
954
  role: 'user',
800
- content: `Tool Execution Result (${parsedJsonTool.tool}):\n${toolResult.result}`,
955
+ content: 'You provided a text response but have not called any tools (write_file, edit_file, run_command, task_completed). Please execute necessary tool calls to complete the user goal, or invoke task_completed if finished.',
801
956
  });
802
- this.handleToolConsecutiveTracking(parsedJsonTool.tool, step);
803
- if (parsedJsonTool.tool === 'task_completed') {
804
- finalSummary = parsedJsonTool.args?.summary || toolResult.result;
805
- saveAgentSession({
806
- goal: userGoal,
807
- summary: finalSummary,
808
- modifiedFiles: Array.from(this.modifiedFiles),
809
- });
810
- return { success: true, summary: finalSummary };
811
- }
812
- continue;
957
+ stepDurations.push(Date.now() - stepStartTime);
813
958
  }
814
- if (textContent.trim()) {
815
- let cleanedThought = textContent
816
- .replace(/[\u0600-\u06FF\u0750-\u077F\uAC00-\uD7AF\u3040-\u30FF\u4E00-\u9FFF\u0D80-\u0DFF]+/g, '')
817
- .trim();
818
- if (!cleanedThought || cleanedThought.length < 5) {
819
- cleanedThought = 'Analyzing codebase files and executing next tool operation...';
820
- }
821
- safeNote(renderMarkdown(cleanedThought), ` Scout Agent Thought (Step ${step})`);
822
- const lowerText = textContent.toLowerCase();
823
- const isExplicitCompletion = lowerText.includes('task is complete') ||
824
- lowerText.includes('task complete') ||
825
- lowerText.includes('goal completed') ||
826
- lowerText.includes('goal is completed') ||
827
- lowerText.includes('all tasks completed') ||
828
- lowerText.includes('i have completed') ||
829
- lowerText.includes('no further changes needed') ||
830
- lowerText.includes('the fix is complete') ||
831
- lowerText.includes('has been created') ||
832
- lowerText.includes('successfully created') ||
833
- lowerText.includes('created the file') ||
834
- lowerText.includes('file created') ||
835
- lowerText.includes('implementation complete') ||
836
- lowerText.includes('work is complete');
837
- // If explicit completion phrase found, OR files have already been modified and assistant returned a final summary without calling tools
838
- if (isExplicitCompletion || (this.modifiedFiles.size > 0 && !lowerText.includes('?') && textContent.length > 50)) {
839
- saveAgentSession({
840
- goal: userGoal,
841
- summary: textContent,
842
- modifiedFiles: Array.from(this.modifiedFiles),
843
- });
844
- return { success: true, summary: textContent };
959
+ catch (err) {
960
+ stepDurations.push(Date.now() - stepStartTime);
961
+ if (isQuotaExceededError(err)) {
962
+ stepSpinner.stop(chalk.red(`Oops! it\'s not you, it\'s us`));
963
+ safeNote(`${chalk.bold.red(`Something went wrong in (Step ${step}), please try again in a moment`)}\n\n` +
964
+ `${chalk.yellow('The configured AI provider has reached its API usage limit or rate cap.')}\n` +
965
+ `${chalk.dim('This is separate from your Scout credit balance shown by `scout quota`.')}\n\n` +
966
+ `${chalk.bold.cyan(' Please come back and try again in a few hours (or check back later today).')}\n\n` +
967
+ `${chalk.dim('Scout Agent session has ended gracefully to protect remaining workflow.')}`, '️ API Quota Limit Reached');
968
+ return {
969
+ success: false,
970
+ summary: 'Something went wrong, please try again in a moment.',
971
+ };
845
972
  }
973
+ stepSpinner.stop(chalk.red(`Step ${step} execution error: ${err?.message || String(err)}`));
974
+ this.historyMessages.push({
975
+ role: 'user',
976
+ content: `Error in previous turn: ${err?.message || String(err)}. Please try alternative steps or call tools.`,
977
+ });
846
978
  }
847
- // If assistant responded with text without calling tools, prompt it to execute tools to complete the goal
848
- this.historyMessages.push({
849
- role: 'user',
850
- content: 'You provided a text response but have not called any tools (write_file, edit_file, run_command, task_completed). Please execute necessary tool calls to complete the user goal, or invoke task_completed if finished.',
851
- });
852
- stepDurations.push(Date.now() - stepStartTime);
853
- }
854
- catch (err) {
855
- stepDurations.push(Date.now() - stepStartTime);
856
- if (isQuotaExceededError(err)) {
857
- stepSpinner.stop(chalk.red(`Oops! it\'s not you, it\'s us`));
858
- safeNote(`${chalk.bold.red(`Something went wrong in (Step ${step}), please try again in a moment`)}\n\n` +
859
- `${chalk.yellow('The configured AI provider has reached its API usage limit or rate cap.')}\n` +
860
- `${chalk.dim('This is separate from your Scout credit balance shown by `scout quota`.')}\n\n` +
861
- `${chalk.bold.cyan(' Please come back and try again in a few hours (or check back later today).')}\n\n` +
862
- `${chalk.dim('Scout Agent session has ended gracefully to protect remaining workflow.')}`, '️ API Quota Limit Reached');
863
- return {
864
- success: false,
865
- summary: 'Something went wrong, please try again in a moment.',
866
- };
979
+ // If current max steps limit is reached while task is still in progress, auto-extend by +15 extra steps (up to 60 max threshold)
980
+ if (step >= this.maxSteps && this.maxSteps < 60) {
981
+ this.maxSteps += 15;
982
+ safeNote(chalk.bold.yellow(`⚡ Step limit reached while task is in progress. Automatically extending execution by +15 extra steps (New Max Limit: ${this.maxSteps})...`), ' Auto Extra Steps Extension');
867
983
  }
868
- stepSpinner.stop(chalk.red(`Step ${step} execution error: ${err?.message || String(err)}`));
869
- this.historyMessages.push({
870
- role: 'user',
871
- content: `Error in previous turn: ${err?.message || String(err)}. Please try alternative steps or call tools.`,
872
- });
873
- }
874
- // If current max steps limit is reached while task is still in progress, auto-extend by +15 extra steps (up to 60 max threshold)
875
- if (step >= this.maxSteps && this.maxSteps < 60) {
876
- this.maxSteps += 15;
877
- safeNote(chalk.bold.yellow(`⚡ Step limit reached while task is in progress. Automatically extending execution by +15 extra steps (New Max Limit: ${this.maxSteps})...`), ' Auto Extra Steps Extension');
878
984
  }
985
+ return {
986
+ success: false,
987
+ summary: `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
988
+ };
989
+ }
990
+ finally {
991
+ cleanupScreenshots();
879
992
  }
880
- return {
881
- success: false,
882
- summary: `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
883
- };
884
993
  }
885
994
  extractJsonToolCall(content) {
886
995
  if (!content)
@@ -900,7 +1009,10 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
900
1009
  const knownTools = [
901
1010
  'read_file', 'write_file', 'edit_file', 'run_command', 'list_dir',
902
1011
  'grep_search', 'glob_search', 'tree_view', 'file_info', 'multi_edit_file',
903
- 'fetch_url', 'git_diff', 'open_app', 'app_action', 'ask_user', 'task_completed'
1012
+ 'fetch_url', 'git_diff', 'open_app', 'app_action', 'agent_scratchpad',
1013
+ 'ask_user', 'speak_text', 'task_completed',
1014
+ 'desktop_action', 'type_text', 'click_app', 'send_keys', 'takeover',
1015
+ 'capture_screen', 'see_screen', 'analyze_screen', 'scratchpad',
904
1016
  ];
905
1017
  for (const toolName of knownTools) {
906
1018
  const tagRegex = new RegExp(`<${toolName}>([\\s\\S]*?)<\\/${toolName}>`, 'i');
@@ -1374,7 +1486,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
1374
1486
  case 'app_action': {
1375
1487
  const appName = String(args.app || args.appName || 'desktop').trim();
1376
1488
  const action = String(args.action || (name !== 'app_action' ? name : 'type_text')).trim();
1377
- const payload = args.payload !== undefined ? args.payload : (args.text || args.content || args.target || args.url || args.command || args);
1489
+ const payload = args.payload !== undefined ? args.payload : (args.text || args.content || args.target || args.query || args.element || args.url || args.command || args);
1378
1490
  const actionRes = await executeInApp(appName, action, payload);
1379
1491
  return { result: actionRes.output };
1380
1492
  }
@@ -1407,6 +1519,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
1407
1519
  case 'task_completed': {
1408
1520
  const summaryStr = String(args.summary || 'Task completed successfully.');
1409
1521
  speakText(summaryStr, { async: true });
1522
+ cleanupScreenshots();
1410
1523
  return { result: summaryStr };
1411
1524
  }
1412
1525
  default: