ft-scout 9.0.6 → 9.0.8
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/.firebase/hosting.d2Vi.cache +9 -9
- package/AUTONOMOUS_FEATURES.md +1 -1
- package/README.md +128 -21
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +45 -4
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/audit.js +1 -1
- package/bin/src/commands/audit.js.map +1 -1
- package/bin/src/commands/dashboard.js +1 -1
- package/bin/src/commands/dashboard.js.map +1 -1
- package/bin/src/commands/mcp.d.ts.map +1 -0
- package/bin/src/commands/mcp.js +393 -0
- package/bin/src/commands/mcp.js.map +1 -0
- package/bin/src/commands/owners.js +1 -1
- package/bin/src/commands/owners.js.map +1 -1
- package/bin/src/discovery/messages.js +2 -2
- package/bin/src/discovery/messages.js.map +1 -1
- package/bin/src/discovery/signals.js +3 -3
- package/bin/src/discovery/signals.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +214 -93
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/planner.d.ts.map +1 -1
- package/bin/src/engine/planner.js +81 -2
- package/bin/src/engine/planner.js.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.js +79 -5
- package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
- package/bin/src/engine/scoutAudioRecorder.cs +215 -0
- package/bin/src/engine/scout_audio_recorder.exe +0 -0
- package/bin/src/engine/toolRegistry.d.ts.map +1 -1
- package/bin/src/engine/toolRegistry.js +13 -0
- package/bin/src/engine/toolRegistry.js.map +1 -1
- package/bin/src/engine/verifier.js +2 -2
- package/bin/src/engine/verifier.js.map +1 -1
- package/bin/src/engine/voiceEngine.d.ts.map +1 -1
- package/bin/src/engine/voiceEngine.js +333 -13
- package/bin/src/engine/voiceEngine.js.map +1 -1
- package/bin/src/index.js +85 -7
- package/bin/src/index.js.map +1 -1
- package/bin/src/mcp/client.d.ts.map +1 -0
- package/bin/src/mcp/client.js +127 -0
- package/bin/src/mcp/client.js.map +1 -0
- package/bin/src/mcp/discovery.d.ts.map +1 -0
- package/bin/src/mcp/discovery.js +91 -0
- package/bin/src/mcp/discovery.js.map +1 -0
- package/bin/src/mcp/health.d.ts.map +1 -0
- package/bin/src/mcp/health.js +44 -0
- package/bin/src/mcp/health.js.map +1 -0
- package/bin/src/mcp/index.d.ts.map +1 -0
- package/bin/src/mcp/index.js +15 -0
- package/bin/src/mcp/index.js.map +1 -0
- package/bin/src/mcp/manager.d.ts.map +1 -0
- package/bin/src/mcp/manager.js +535 -0
- package/bin/src/mcp/manager.js.map +1 -0
- package/bin/src/mcp/permissions.d.ts.map +1 -0
- package/bin/src/mcp/permissions.js +50 -0
- package/bin/src/mcp/permissions.js.map +1 -0
- package/bin/src/mcp/persistence.d.ts.map +1 -0
- package/bin/src/mcp/persistence.js +156 -0
- package/bin/src/mcp/persistence.js.map +1 -0
- package/bin/src/mcp/recovery.d.ts.map +1 -0
- package/bin/src/mcp/recovery.js +75 -0
- package/bin/src/mcp/recovery.js.map +1 -0
- package/bin/src/mcp/telemetry.d.ts.map +1 -0
- package/bin/src/mcp/telemetry.js +82 -0
- package/bin/src/mcp/telemetry.js.map +1 -0
- package/bin/src/mcp/toolRouter.d.ts.map +1 -0
- package/bin/src/mcp/toolRouter.js +127 -0
- package/bin/src/mcp/toolRouter.js.map +1 -0
- package/bin/src/mcp/transport.d.ts.map +1 -0
- package/bin/src/mcp/transport.js +328 -0
- package/bin/src/mcp/transport.js.map +1 -0
- package/bin/src/mcp/types.d.ts.map +1 -0
- package/bin/src/mcp/types.js +6 -0
- package/bin/src/mcp/types.js.map +1 -0
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +207 -0
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/email.js +4 -4
- package/package.json +1 -1
- package/web/app.js +1255 -59
- package/web/styles.css +1474 -15
|
@@ -14,12 +14,13 @@ import { TaskStateManager } from './agentState.js';
|
|
|
14
14
|
import { SemanticBrowserController } from './browser/index.js';
|
|
15
15
|
import { ActionVerifier } from './actionVerifier.js';
|
|
16
16
|
import { ApprovalManager, RiskClassifier } from './safety.js';
|
|
17
|
-
import { TaskPlanner } from './planner.js';
|
|
17
|
+
import { TaskPlanner, checkSimpleAppAction } from './planner.js';
|
|
18
18
|
import { UnifiedToolRegistry } from './toolRegistry.js';
|
|
19
19
|
import { SystemComputerController, MockComputerController } from './computer/index.js';
|
|
20
20
|
import { AgentWorkspace, inferExecutionIntent, resolveDesktopPath } from './workspace/index.js';
|
|
21
21
|
import { AgentRuntime } from './runtime/index.js';
|
|
22
22
|
import { loadAuthConfig, isProOrHigher } from '../utils/auth.js';
|
|
23
|
+
import { getMCPManager } from '../mcp/index.js';
|
|
23
24
|
export { AgentRuntime, AgentWorkspace, inferExecutionIntent, resolveDesktopPath, SystemComputerController, MockComputerController };
|
|
24
25
|
const execAsync = promisify(exec);
|
|
25
26
|
export function robustSnippetReplace(origContent, target, replacement) {
|
|
@@ -157,7 +158,7 @@ export const AGENT_TOOLS = [
|
|
|
157
158
|
},
|
|
158
159
|
{
|
|
159
160
|
name: 'run_command',
|
|
160
|
-
description: 'Execute a terminal/shell command in the workspace (e.g. npm test, npm run build, git status). WARNING: Do NOT use run_command to create files or folders (e.g. mkdir, touch, echo >)
|
|
161
|
+
description: 'Execute a terminal/shell command in the workspace (e.g. npm test, npm run build, git status). WARNING: Do NOT use run_command to create files or folders (e.g. mkdir, touch, echo >)call write_file directly instead!',
|
|
161
162
|
parameters: {
|
|
162
163
|
type: 'object',
|
|
163
164
|
properties: {
|
|
@@ -270,7 +271,7 @@ export const AGENT_TOOLS = [
|
|
|
270
271
|
},
|
|
271
272
|
{
|
|
272
273
|
name: 'ask_user',
|
|
273
|
-
description: 'ONLY use for missing API keys or secret credentials. DO NOT use to ask the user what code to write or to re-confirm their
|
|
274
|
+
description: 'ONLY use for missing API keys or secret credentials. DO NOT use to ask the user what code to write or to re-confirm their prompttake immediate autonomous tool action instead.',
|
|
274
275
|
parameters: {
|
|
275
276
|
type: 'object',
|
|
276
277
|
properties: {
|
|
@@ -374,6 +375,7 @@ export class AgentExecutionLoop {
|
|
|
374
375
|
cancelReason;
|
|
375
376
|
executionIntent;
|
|
376
377
|
userTier;
|
|
378
|
+
currentGoal = '';
|
|
377
379
|
constructor(options) {
|
|
378
380
|
this.cwd = process.cwd();
|
|
379
381
|
this.autoApprove = Boolean(options?.autoApprove);
|
|
@@ -582,6 +584,8 @@ PLAN TIER & CAPABILITIES: DEVELOPER FREE (BASIC DEVELOPER OPS ONLY)
|
|
|
582
584
|
4. Reading & exploring: use read_file, list_dir, grep_search to inspect files.
|
|
583
585
|
• Do NOT attempt to use scratchpad, computer, or browser tools. Execute the requested file edit, file creation, or command directly and call task_completed immediately.
|
|
584
586
|
`;
|
|
587
|
+
const mcpRouting = getMCPManager().routeTools(this.currentGoal);
|
|
588
|
+
const mcpCapabilitiesSection = mcpRouting.capabilitiesSummary ? `\n${mcpRouting.capabilitiesSummary}\n` : '';
|
|
585
589
|
return `You are Scout Agent, an elite AI Principal Software Engineer & Autonomous Computer-Use Operator (developed by FrontTerrain).
|
|
586
590
|
|
|
587
591
|
Working Directory: "${this.cwd}"
|
|
@@ -589,6 +593,7 @@ Host OS Platform: "${process.platform}"
|
|
|
589
593
|
Current Date & Time: "${new Date().toISOString()}" (Current Year: ${new Date().getFullYear()})
|
|
590
594
|
${tierDirective}
|
|
591
595
|
${intentInfo}
|
|
596
|
+
${mcpCapabilitiesSection}
|
|
592
597
|
Workspace Overview (first 100 files):
|
|
593
598
|
${filesList.join('\n')}
|
|
594
599
|
|
|
@@ -615,8 +620,10 @@ CORE ARCHITECTURAL PRINCIPLES:
|
|
|
615
620
|
6. MULTILINGUAL & INTENT COMPREHENSION: Understand user intent in any language (English, Hinglish like "likho", "bnao", "code karo", "fix karo", "samjha do", Hindi, Spanish, etc.).
|
|
616
621
|
7. FOR CODE & REPO TASKS: When the task is a coding, project refactoring, or repository maintenance task, use direct code modification tools (write_file, edit_file, run_command, read_file, grep_search).
|
|
617
622
|
8. RUN & VERIFY COMMANDS: Execute shell commands, tests, builds, and formatters using run_command.
|
|
618
|
-
9. CRITICAL TERMINATION RULE: As soon as you have finished creating/modifying the necessary files, running verification checks, or completing the desktop application workflow, YOU MUST CALL task_completed tool immediately
|
|
623
|
+
9. CRITICAL TERMINATION & CONVERSATIONAL VOICE RULE: As soon as you have finished creating/modifying the necessary files, running verification checks, or completing the desktop application workflow, YOU MUST CALL task_completed tool immediately.
|
|
624
|
+
- CONVERSATIONAL CO-PILOT TONE: In your 'summary' parameter for task_completed, speak directly and warmly to the developer like an elite human co-pilot (e.g. "I've launched Chrome for you and it's active on your screen. What should we tackle next?" or "I've created the authentication controller and all unit tests pass with zero regressions!"). Never output dry mechanical robot logs or third-person phrases like "Objective achieved with 1 tool call". Talk directly to the user!
|
|
619
625
|
10. THOUGHT TRANSPARENCY PROTOCOL: In EVERY step before calling any tools or ending a turn, you MUST provide a clear 1-2 sentence explanation in your text response describing your current reasoning, what file/action you are taking, and why you are taking it.
|
|
626
|
+
11. APPLICATION LAUNCH & SIMPLE ACTION TERMINATION: When the user's objective is simply to open, launch, close, or focus an application (e.g. "Open Chrome", "Launch Notepad", "Open Calculator"): Immediately after calling computer_open_app or computer_close_app, your goal is 100% complete! You MUST call task_completed on the very next step with your friendly conversational summary. NEVER explore the repository or call list_dir, read_file, grep_search, or edit_file for app launching tasks!
|
|
620
627
|
|
|
621
628
|
When returning tool calls, use standard OpenAI function calling format or JSON tool call payload format:
|
|
622
629
|
\`\`\`json
|
|
@@ -629,6 +636,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
|
|
|
629
636
|
}
|
|
630
637
|
async executeGoal(userGoal) {
|
|
631
638
|
this.resetContext();
|
|
639
|
+
this.currentGoal = userGoal;
|
|
632
640
|
this.executionIntent = inferExecutionIntent(userGoal);
|
|
633
641
|
this.taskStateManager.setObjective(userGoal);
|
|
634
642
|
if (this.computerController.setExpectedApplication && this.executionIntent.requiredApplications.length > 0) {
|
|
@@ -636,7 +644,9 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
|
|
|
636
644
|
}
|
|
637
645
|
const structuredPlan = this.taskPlanner.createPlan(userGoal);
|
|
638
646
|
this.taskStateManager.setPlan(structuredPlan.steps);
|
|
639
|
-
|
|
647
|
+
const mcpRouted = getMCPManager().routeTools(userGoal);
|
|
648
|
+
const mcpNames = mcpRouted.tools.map((t) => t.originalName);
|
|
649
|
+
this.taskStateManager.setAvailableTools([...this.toolRegistry.getToolsForTier(this.userTier).map((t) => t.name), ...mcpNames]);
|
|
640
650
|
if (this.mockMode) {
|
|
641
651
|
for (let i = 1; i <= structuredPlan.steps.length; i++) {
|
|
642
652
|
this.taskStateManager.startStep(i);
|
|
@@ -657,97 +667,100 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
|
|
|
657
667
|
content: recentSessionsMemory,
|
|
658
668
|
});
|
|
659
669
|
}
|
|
660
|
-
// Robust Target File Discovery:
|
|
670
|
+
// Robust Target File Discovery: Only run file discovery if the goal is NOT a simple desktop application launch/action
|
|
671
|
+
const simpleAppCheck = checkSimpleAppAction(userGoal);
|
|
661
672
|
const filesList = getDirectoryFiles(this.cwd);
|
|
662
673
|
const matchedFiles = [];
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
norm.includes('
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
matchedFiles.
|
|
674
|
+
if (!simpleAppCheck.isSimple) {
|
|
675
|
+
const isIgnoredFile = (filePath) => {
|
|
676
|
+
const norm = filePath.replace(/\\/g, '/').toLowerCase();
|
|
677
|
+
const ext = path.extname(norm);
|
|
678
|
+
const ignoredExts = ['.pyc', '.pyo', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.exe', '.bin', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz'];
|
|
679
|
+
return (norm.includes('__pycache__/') ||
|
|
680
|
+
norm.includes('.git/') ||
|
|
681
|
+
norm.includes('node_modules/') ||
|
|
682
|
+
norm.includes('.ft/') ||
|
|
683
|
+
ignoredExts.includes(ext));
|
|
684
|
+
};
|
|
685
|
+
// Match potential paths
|
|
686
|
+
const pathMatches = userGoal.match(/(?:[a-zA-Z]:[\\/][^\s"':;,]+|\.?[\\/][^\s"':;,]+\.[a-zA-Z0-9]+|[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)/g) || [];
|
|
687
|
+
for (const rawPath of pathMatches) {
|
|
688
|
+
const cleanP = rawPath.replace(/^["'()]+|["'()]+$/g, '').trim();
|
|
689
|
+
if (!cleanP || cleanP.length < 3)
|
|
690
|
+
continue;
|
|
691
|
+
const fullP = path.isAbsolute(cleanP) ? cleanP : path.resolve(this.cwd, cleanP);
|
|
692
|
+
if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
|
|
693
|
+
const relP = path.relative(this.cwd, fullP).replace(/\\/g, '/');
|
|
694
|
+
if (!isIgnoredFile(relP) && !matchedFiles.includes(relP))
|
|
695
|
+
matchedFiles.push(relP);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
// Check if any file in filesList matches or ends with the extracted relative path or filename
|
|
699
|
+
const normToken = cleanP.replace(/\\/g, '/').toLowerCase();
|
|
700
|
+
const baseName = path.basename(normToken);
|
|
701
|
+
for (const f of filesList) {
|
|
702
|
+
if (isIgnoredFile(f))
|
|
703
|
+
continue;
|
|
704
|
+
const normF = f.replace(/\\/g, '/').toLowerCase();
|
|
705
|
+
if (normF === normToken || normF.endsWith('/' + normToken) || (baseName.includes('.') && normF.endsWith('/' + baseName))) {
|
|
706
|
+
if (!matchedFiles.includes(f))
|
|
707
|
+
matchedFiles.push(f);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
684
711
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
712
|
+
// Keyword fallback if no direct path matched
|
|
713
|
+
if (matchedFiles.length === 0) {
|
|
714
|
+
const stopWords = new Set([
|
|
715
|
+
'the', 'a', 'an', 'in', 'is', 'for', 'to', 'of', 'and', 'or', 'on', 'at', 'by', 'from',
|
|
716
|
+
'with', 'this', 'that', 'it', 'code', 'file', 'files', 'fix', 'add', 'make', 'create',
|
|
717
|
+
'update', 'change', 'modify', 'edit', 'write', 'implement', 'bro', 'please', 'scout',
|
|
718
|
+
'agent', 'bug', 'error', 'issue', 'help', 'app', 'project', 'script', 'function',
|
|
719
|
+
'folder', 'dir', 'directory', 'testing', 'inside', 'open', 'vs', 'vscode', 'py', 'python',
|
|
720
|
+
'new', 'build', 'run', 'test', 'suite'
|
|
721
|
+
]);
|
|
722
|
+
const words = userGoal.split(/[\s,;:\/\\]+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
|
|
723
|
+
const keywords = words.filter((w) => w.length >= 3 && !stopWords.has(w.toLowerCase()));
|
|
689
724
|
for (const f of filesList) {
|
|
690
725
|
if (isIgnoredFile(f))
|
|
691
726
|
continue;
|
|
727
|
+
if (matchedFiles.length >= 3)
|
|
728
|
+
break;
|
|
692
729
|
const normF = f.replace(/\\/g, '/').toLowerCase();
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
730
|
+
const baseName = path.basename(normF, path.extname(normF));
|
|
731
|
+
for (const kw of keywords) {
|
|
732
|
+
const kwLower = kw.toLowerCase();
|
|
733
|
+
if (baseName === kwLower || normF.endsWith('/' + kwLower + path.extname(normF))) {
|
|
734
|
+
if (!matchedFiles.includes(f))
|
|
735
|
+
matchedFiles.push(f);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
696
738
|
}
|
|
697
739
|
}
|
|
698
740
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (baseName === kwLower || normF.endsWith('/' + kwLower + path.extname(normF))) {
|
|
722
|
-
if (!matchedFiles.includes(f))
|
|
723
|
-
matchedFiles.push(f);
|
|
724
|
-
break;
|
|
741
|
+
// Safely inject high-confidence existing file previews without prompt-hijacking
|
|
742
|
+
for (const file of matchedFiles.slice(0, 3)) {
|
|
743
|
+
const fullP = path.resolve(this.cwd, file);
|
|
744
|
+
if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
|
|
745
|
+
const analyzeSpin = spinner();
|
|
746
|
+
analyzeSpin.start(chalk.cyan(` Auto-discovered target file: ${file}. Analyzing...`));
|
|
747
|
+
try {
|
|
748
|
+
const content = fs.readFileSync(fullP, 'utf-8');
|
|
749
|
+
const lines = content.split('\n');
|
|
750
|
+
analyzeSpin.stop(chalk.green(` Auto-discovered & analyzed ${file} (${lines.length} lines)`));
|
|
751
|
+
const previewSnippet = lines.length <= 250
|
|
752
|
+
? lines.map((l, i) => `${i + 1} | ${l}`).join('\n')
|
|
753
|
+
: lines.slice(0, 100).map((l, i) => `${i + 1} | ${l}`).join('\n') + `\n... [truncated, file has ${lines.length} lines. Call read_file to view remaining lines]`;
|
|
754
|
+
this.readFilesHistory.add(file);
|
|
755
|
+
this.readFilesHistory.add(file.replace(/\\/g, '/').toLowerCase());
|
|
756
|
+
this.historyMessages.push({
|
|
757
|
+
role: 'user',
|
|
758
|
+
content: `[Reference Workspace Context - Pre-loaded target file "${file}" (${lines.length} lines)]:\n\`\`\`\n${previewSnippet}\n\`\`\`\nCRITICAL DIRECTIVE: The target file "${file}" is ALREADY fully pre-loaded into your reference context above. DO NOT call read_file or list_dir for this file again! Immediately begin calling write_file, edit_file, or run_command to satisfy the user goal.`,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
analyzeSpin.stop(chalk.yellow(`Could not read file ${file}`));
|
|
725
763
|
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
// Safely inject high-confidence existing file previews without prompt-hijacking
|
|
730
|
-
for (const file of matchedFiles.slice(0, 3)) {
|
|
731
|
-
const fullP = path.resolve(this.cwd, file);
|
|
732
|
-
if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
|
|
733
|
-
const analyzeSpin = spinner();
|
|
734
|
-
analyzeSpin.start(chalk.cyan(` Auto-discovered target file: ${file}. Analyzing...`));
|
|
735
|
-
try {
|
|
736
|
-
const content = fs.readFileSync(fullP, 'utf-8');
|
|
737
|
-
const lines = content.split('\n');
|
|
738
|
-
analyzeSpin.stop(chalk.green(` Auto-discovered & analyzed ${file} (${lines.length} lines)`));
|
|
739
|
-
const previewSnippet = lines.length <= 250
|
|
740
|
-
? lines.map((l, i) => `${i + 1} | ${l}`).join('\n')
|
|
741
|
-
: lines.slice(0, 100).map((l, i) => `${i + 1} | ${l}`).join('\n') + `\n... [truncated, file has ${lines.length} lines. Call read_file to view remaining lines]`;
|
|
742
|
-
this.readFilesHistory.add(file);
|
|
743
|
-
this.readFilesHistory.add(file.replace(/\\/g, '/').toLowerCase());
|
|
744
|
-
this.historyMessages.push({
|
|
745
|
-
role: 'user',
|
|
746
|
-
content: `[Reference Workspace Context - Pre-loaded target file "${file}" (${lines.length} lines)]:\n\`\`\`\n${previewSnippet}\n\`\`\`\nCRITICAL DIRECTIVE: The target file "${file}" is ALREADY fully pre-loaded into your reference context above. DO NOT call read_file or list_dir for this file again! Immediately begin calling write_file, edit_file, or run_command to satisfy the user goal.`,
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
catch {
|
|
750
|
-
analyzeSpin.stop(chalk.yellow(`Could not read file ${file}`));
|
|
751
764
|
}
|
|
752
765
|
}
|
|
753
766
|
}
|
|
@@ -786,12 +799,26 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
|
|
|
786
799
|
stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step}/${this.maxSteps} • Est. completion: ~${estLeftStr} left)`));
|
|
787
800
|
try {
|
|
788
801
|
let response;
|
|
802
|
+
const mcpRouting = getMCPManager().routeTools(userGoal);
|
|
803
|
+
const mcpToolDefs = mcpRouting.tools.map((mt) => ({
|
|
804
|
+
name: mt.originalName,
|
|
805
|
+
description: `[MCP: ${mt.serverName}] ${mt.description}`,
|
|
806
|
+
parameters: {
|
|
807
|
+
type: 'object',
|
|
808
|
+
properties: mt.inputSchema.properties || {},
|
|
809
|
+
required: mt.inputSchema.required || [],
|
|
810
|
+
},
|
|
811
|
+
source: 'mcp',
|
|
812
|
+
mcpServerId: mt.serverId,
|
|
813
|
+
mcpServerName: mt.serverName,
|
|
814
|
+
}));
|
|
815
|
+
const activeLoopTools = [...this.toolRegistry.getToolsForTier(this.userTier), ...mcpToolDefs];
|
|
789
816
|
try {
|
|
790
817
|
response = await callOpenAIWithRetry(async (model) => {
|
|
791
818
|
return await openai.chat.completions.create({
|
|
792
819
|
model,
|
|
793
820
|
messages: sanitizeMessages(this.historyMessages),
|
|
794
|
-
tools:
|
|
821
|
+
tools: activeLoopTools.map((t) => ({ type: 'function', function: t })),
|
|
795
822
|
tool_choice: 'auto',
|
|
796
823
|
temperature: 0.1,
|
|
797
824
|
presence_penalty: 0.1,
|
|
@@ -802,7 +829,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
|
|
|
802
829
|
catch (err) {
|
|
803
830
|
const errStr = String(err?.message || err?.error || err || '').toLowerCase();
|
|
804
831
|
if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported') || errStr.includes('reasoning')) {
|
|
805
|
-
// Model doesn't support native function calling
|
|
832
|
+
// Model doesn't support native function calling inject tool-call formatting hint
|
|
806
833
|
// so extractJsonToolCall can parse the response as a structured tool invocation
|
|
807
834
|
const toolHintMsg = {
|
|
808
835
|
role: 'user',
|
|
@@ -811,7 +838,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
|
|
|
811
838
|
{ "tool": "tool_name", "args": { ... } }
|
|
812
839
|
\`\`\`
|
|
813
840
|
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, ask_user, speak_text, task_completed.
|
|
814
|
-
You MUST output exactly ONE JSON code block per tool call. Do NOT describe what you would do in plain
|
|
841
|
+
You MUST output exactly ONE JSON code block per tool call. Do NOT describe what you would do in plain textoutput the JSON tool call directly!`,
|
|
815
842
|
};
|
|
816
843
|
const fallbackMessages = [...this.historyMessages, toolHintMsg];
|
|
817
844
|
response = await callOpenAIWithRetry(async (model) => {
|
|
@@ -858,6 +885,29 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
858
885
|
content: `Tool Execution Result (${fnName}):\n${toolResult.result}`,
|
|
859
886
|
});
|
|
860
887
|
this.handleToolConsecutiveTracking(fnName, args, step);
|
|
888
|
+
// Automatic immediate completion for simple app actions (open/close/switch window)
|
|
889
|
+
if (simpleAppCheck.isSimple && (fnName === 'computer_open_app' || fnName === 'computer_close_app' || fnName === 'computer_switch_window')) {
|
|
890
|
+
let callSucceeded = false;
|
|
891
|
+
try {
|
|
892
|
+
const parsed = JSON.parse(toolResult.result);
|
|
893
|
+
callSucceeded = parsed.success !== false;
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
|
|
897
|
+
}
|
|
898
|
+
if (callSucceeded) {
|
|
899
|
+
const appName = simpleAppCheck.appName || 'application';
|
|
900
|
+
const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
|
|
901
|
+
finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
|
|
902
|
+
safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
|
|
903
|
+
saveAgentSession({
|
|
904
|
+
goal: userGoal,
|
|
905
|
+
summary: finalSummary,
|
|
906
|
+
modifiedFiles: [],
|
|
907
|
+
});
|
|
908
|
+
return { success: true, summary: finalSummary };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
861
911
|
if (fnName === 'task_completed') {
|
|
862
912
|
finalSummary = args.summary || toolResult.result;
|
|
863
913
|
if (this.modifiedFiles.size > 0) {
|
|
@@ -891,6 +941,29 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
891
941
|
content: `Tool Execution Result (${parsedJsonTool.tool}):\n${toolResult.result}`,
|
|
892
942
|
});
|
|
893
943
|
this.handleToolConsecutiveTracking(parsedJsonTool.tool, parsedJsonTool.args, step);
|
|
944
|
+
// Automatic immediate completion for simple app actions (open/close/switch window)
|
|
945
|
+
if (simpleAppCheck.isSimple && (parsedJsonTool.tool === 'computer_open_app' || parsedJsonTool.tool === 'computer_close_app' || parsedJsonTool.tool === 'computer_switch_window')) {
|
|
946
|
+
let callSucceeded = false;
|
|
947
|
+
try {
|
|
948
|
+
const parsed = JSON.parse(toolResult.result);
|
|
949
|
+
callSucceeded = parsed.success !== false;
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
|
|
953
|
+
}
|
|
954
|
+
if (callSucceeded) {
|
|
955
|
+
const appName = simpleAppCheck.appName || 'application';
|
|
956
|
+
const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
|
|
957
|
+
finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
|
|
958
|
+
safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
|
|
959
|
+
saveAgentSession({
|
|
960
|
+
goal: userGoal,
|
|
961
|
+
summary: finalSummary,
|
|
962
|
+
modifiedFiles: [],
|
|
963
|
+
});
|
|
964
|
+
return { success: true, summary: finalSummary };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
894
967
|
if (parsedJsonTool.tool === 'task_completed') {
|
|
895
968
|
finalSummary = parsedJsonTool.args?.summary || toolResult.result;
|
|
896
969
|
saveAgentSession({
|
|
@@ -1178,6 +1251,22 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
1178
1251
|
}
|
|
1179
1252
|
}
|
|
1180
1253
|
// Validate against execution intent (USER INTENT > TOOL CONVENIENCE)
|
|
1254
|
+
const simpleCheckInDispatch = checkSimpleAppAction(this.taskStateManager.getState()?.objective || '');
|
|
1255
|
+
if (simpleCheckInDispatch.isSimple) {
|
|
1256
|
+
if (['list_dir', 'read_file', 'grep_search', 'glob_search', 'file_info', 'tree_view', 'write_file', 'edit_file', 'multi_edit_file'].includes(name)) {
|
|
1257
|
+
const blockedExplorationMsg = `[UNNECESSARY WORKSPACE EXPLORATION BLOCKED]: The user only requested to ${simpleCheckInDispatch.action || 'launch'} ${simpleCheckInDispatch.appName || 'the application'}. Reading repository code or listing files (${name}) is forbidden for this task. You must call task_completed immediately.`;
|
|
1258
|
+
this.taskStateManager.addObservation(name, blockedExplorationMsg, false);
|
|
1259
|
+
return {
|
|
1260
|
+
result: JSON.stringify({
|
|
1261
|
+
success: false,
|
|
1262
|
+
action: name,
|
|
1263
|
+
error: 'UnnecessaryWorkspaceExplorationBlocked',
|
|
1264
|
+
message: blockedExplorationMsg,
|
|
1265
|
+
directive: "Call 'task_completed' NOW with your completion summary."
|
|
1266
|
+
}, null, 2)
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1181
1270
|
if (this.executionIntent && !this.executionIntent.allowAlternativeTools) {
|
|
1182
1271
|
if (this.executionIntent.requiredInteraction === 'computer') {
|
|
1183
1272
|
if (['write_file', 'edit_file', 'multi_edit_file'].includes(name)) {
|
|
@@ -1356,13 +1445,13 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
1356
1445
|
checkSpin.start(chalk.cyan(` Checking for error: edited ${targetPath}...`));
|
|
1357
1446
|
const syntaxCheck = await checkFileSyntax(targetPath, this.cwd);
|
|
1358
1447
|
if (!syntaxCheck.valid) {
|
|
1359
|
-
checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath}
|
|
1448
|
+
checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} Compiler/Syntax Error!`));
|
|
1360
1449
|
this.taskStateManager.recordError(`Syntax error in "${targetPath}": ${syntaxCheck.errors.join('; ')}`, 'write_file');
|
|
1361
1450
|
return {
|
|
1362
1451
|
result: `Successfully wrote ${content.length} characters (${numberOfLinesChanged} lines changed) to "${targetPath}".\n\n️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
|
|
1363
1452
|
};
|
|
1364
1453
|
}
|
|
1365
|
-
checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath}
|
|
1454
|
+
checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} Syntax & build clean.`));
|
|
1366
1455
|
this.taskStateManager.recordArtifact({
|
|
1367
1456
|
filename: path.basename(targetPath),
|
|
1368
1457
|
path: targetPath,
|
|
@@ -1422,12 +1511,12 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
1422
1511
|
checkSpin.start(chalk.cyan(` Checking for error: edited ${targetPath}...`));
|
|
1423
1512
|
const syntaxCheck = await checkFileSyntax(targetPath, this.cwd);
|
|
1424
1513
|
if (!syntaxCheck.valid) {
|
|
1425
|
-
checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath}
|
|
1514
|
+
checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} Compiler/Syntax Error!`));
|
|
1426
1515
|
return {
|
|
1427
1516
|
result: `Successfully replaced snippet (${numberOfLinesChanged} lines changed) in "${targetPath}".\n\n️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
|
|
1428
1517
|
};
|
|
1429
1518
|
}
|
|
1430
|
-
checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath}
|
|
1519
|
+
checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} Syntax & build clean.`));
|
|
1431
1520
|
return { result: `Successfully replaced target snippet (${numberOfLinesChanged} lines changed) in "${targetPath}". Rationale: ${reason}. Syntax check passed cleanly.` };
|
|
1432
1521
|
}
|
|
1433
1522
|
case 'run_command': {
|
|
@@ -1983,6 +2072,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
1983
2072
|
const res = await this.computerController.openApplication(appName, args.args);
|
|
1984
2073
|
this.taskStateManager.setActiveApplication(appName);
|
|
1985
2074
|
this.taskStateManager.addObservation('computer_open_app', res.output, res.success);
|
|
2075
|
+
const currentObj = this.taskStateManager.getState()?.objective || '';
|
|
2076
|
+
const simpleCheck = checkSimpleAppAction(currentObj);
|
|
2077
|
+
const isOnlyOpening = simpleCheck.isSimple && simpleCheck.action !== 'close';
|
|
1986
2078
|
const structuredRes = {
|
|
1987
2079
|
success: res.success,
|
|
1988
2080
|
action: 'open_app',
|
|
@@ -1991,6 +2083,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
1991
2083
|
recoverable: res.recoverable ?? true,
|
|
1992
2084
|
activeWindow: res.activeWindow || appName,
|
|
1993
2085
|
expectedWindow: res.expectedWindow || appName,
|
|
2086
|
+
...(isOnlyOpening && res.success ? {
|
|
2087
|
+
directive: "CRITICAL: The requested application is now opened. Your objective is 100% satisfied. You MUST call 'task_completed' immediately on your next turn. DO NOT call list_dir, read_file, or explore the repository."
|
|
2088
|
+
} : {}),
|
|
1994
2089
|
};
|
|
1995
2090
|
return { result: JSON.stringify(structuredRes, null, 2) };
|
|
1996
2091
|
}
|
|
@@ -1998,6 +2093,12 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
1998
2093
|
const appName = String(args.appName || args.app || '');
|
|
1999
2094
|
const res = await this.computerController.closeApplication(appName);
|
|
2000
2095
|
this.taskStateManager.addObservation('computer_close_app', res.output, res.success);
|
|
2096
|
+
const currentObj = this.taskStateManager.getState()?.objective || '';
|
|
2097
|
+
const simpleCheck = checkSimpleAppAction(currentObj);
|
|
2098
|
+
const isOnlyClosing = simpleCheck.isSimple && simpleCheck.action === 'close';
|
|
2099
|
+
if (isOnlyClosing && res.success) {
|
|
2100
|
+
return { result: `${res.output}\nCRITICAL: Application closed. Objective complete. Call task_completed now.` };
|
|
2101
|
+
}
|
|
2001
2102
|
return { result: res.output };
|
|
2002
2103
|
}
|
|
2003
2104
|
case 'computer_switch_window': {
|
|
@@ -2053,8 +2154,28 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
|
|
|
2053
2154
|
this.taskStateManager.completeStep(this.currentStep, summaryStr);
|
|
2054
2155
|
return { result: summaryStr };
|
|
2055
2156
|
}
|
|
2056
|
-
default:
|
|
2157
|
+
default: {
|
|
2158
|
+
// Check if this tool is provided by a connected MCP server
|
|
2159
|
+
const mcpManager = getMCPManager();
|
|
2160
|
+
const mcpTarget = mcpManager.findTool(name);
|
|
2161
|
+
if (mcpTarget) {
|
|
2162
|
+
const mcpRes = await mcpManager.executeTool(name, args);
|
|
2163
|
+
const isSuccess = mcpRes.status === 'success';
|
|
2164
|
+
const outputStr = typeof mcpRes.result === 'string'
|
|
2165
|
+
? mcpRes.result
|
|
2166
|
+
: (mcpRes.result?.text ? String(mcpRes.result.text) : JSON.stringify(mcpRes.result ?? { status: mcpRes.status, error: mcpRes.error }));
|
|
2167
|
+
this.taskStateManager.addObservation(name, mcpRes.outputSummary || outputStr, isSuccess);
|
|
2168
|
+
// Check if MCP result generated or referenced local files/artifacts
|
|
2169
|
+
if (isSuccess && typeof mcpRes.result === 'object' && mcpRes.result !== null) {
|
|
2170
|
+
const possiblePath = mcpRes.result.path || mcpRes.result.filePath || mcpRes.result.file || args.destPath || args.path || args.outputPath;
|
|
2171
|
+
if (typeof possiblePath === 'string' && fs.existsSync(path.resolve(this.cwd, possiblePath))) {
|
|
2172
|
+
this.modifiedFiles.add(path.relative(this.cwd, path.resolve(this.cwd, possiblePath)).replace(/\\/g, '/'));
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return { result: outputStr };
|
|
2176
|
+
}
|
|
2057
2177
|
return { result: `Unknown tool call name: "${name}"` };
|
|
2178
|
+
}
|
|
2058
2179
|
}
|
|
2059
2180
|
}
|
|
2060
2181
|
}
|