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.
- package/README.md +40 -5
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +12 -7
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +290 -177
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/appControl.d.ts.map +1 -1
- package/bin/src/engine/appControl.js +693 -62
- package/bin/src/engine/appControl.js.map +1 -1
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +4 -3
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/voiceEngine.d.ts.map +1 -1
- package/bin/src/engine/voiceEngine.js +67 -199
- package/bin/src/engine/voiceEngine.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/utils/branding.js +1 -1
- package/package.json +2 -2
- package/web/app.js +2 -2
- package/web/styles.css +1 -1
|
@@ -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,
|
|
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 ("
|
|
293
|
-
payload: { description: 'Action details/payload object or string (e.g.
|
|
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.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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:
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
699
|
-
step
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
|
716
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
767
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
.
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
848
|
-
this.
|
|
849
|
-
|
|
850
|
-
|
|
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', '
|
|
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:
|