ft-scout 9.4.6 → 9.4.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.
Files changed (98) hide show
  1. package/.firebase/hosting.d2Vi.cache +8 -8
  2. package/.scoutrules +13 -0
  3. package/AUTONOMOUS_FEATURES.md +15 -10
  4. package/README.md +588 -482
  5. package/bin/src/commands/agent.d.ts.map +1 -1
  6. package/bin/src/commands/agent.js +73 -25
  7. package/bin/src/commands/agent.js.map +1 -1
  8. package/bin/src/commands/authCommand.d.ts.map +1 -1
  9. package/bin/src/commands/authCommand.js +4 -10
  10. package/bin/src/commands/authCommand.js.map +1 -1
  11. package/bin/src/commands/benchmark.d.ts.map +1 -1
  12. package/bin/src/commands/benchmark.js +0 -5
  13. package/bin/src/commands/benchmark.js.map +1 -1
  14. package/bin/src/commands/checkpoint.d.ts.map +1 -0
  15. package/bin/src/commands/checkpoint.js +49 -0
  16. package/bin/src/commands/checkpoint.js.map +1 -0
  17. package/bin/src/commands/init.d.ts.map +1 -1
  18. package/bin/src/commands/init.js +14 -17
  19. package/bin/src/commands/init.js.map +1 -1
  20. package/bin/src/commands/login.d.ts.map +1 -1
  21. package/bin/src/commands/login.js +2 -17
  22. package/bin/src/commands/login.js.map +1 -1
  23. package/bin/src/commands/plan.d.ts.map +1 -1
  24. package/bin/src/commands/plan.js +36 -6
  25. package/bin/src/commands/plan.js.map +1 -1
  26. package/bin/src/commands/pr.d.ts.map +1 -0
  27. package/bin/src/commands/pr.js +191 -0
  28. package/bin/src/commands/pr.js.map +1 -0
  29. package/bin/src/commands/watch.d.ts.map +1 -0
  30. package/bin/src/commands/watch.js +149 -0
  31. package/bin/src/commands/watch.js.map +1 -0
  32. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  33. package/bin/src/engine/agentEngine.js +492 -375
  34. package/bin/src/engine/agentEngine.js.map +1 -1
  35. package/bin/src/engine/checkpoints/checkpointManager.d.ts.map +1 -0
  36. package/bin/src/engine/checkpoints/checkpointManager.js +171 -0
  37. package/bin/src/engine/checkpoints/checkpointManager.js.map +1 -0
  38. package/bin/src/engine/computer/computerController.d.ts.map +1 -1
  39. package/bin/src/engine/computer/computerController.js +49 -0
  40. package/bin/src/engine/computer/computerController.js.map +1 -1
  41. package/bin/src/engine/computer/operatorHud.d.ts.map +1 -0
  42. package/bin/src/engine/computer/operatorHud.js +126 -0
  43. package/bin/src/engine/computer/operatorHud.js.map +1 -0
  44. package/bin/src/engine/llm.d.ts.map +1 -1
  45. package/bin/src/engine/llm.js +21 -51
  46. package/bin/src/engine/llm.js.map +1 -1
  47. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
  48. package/bin/src/engine/runtime/agentRuntime.js +308 -206
  49. package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
  50. package/bin/src/engine/subagents/subagentManager.d.ts.map +1 -0
  51. package/bin/src/engine/subagents/subagentManager.js +96 -0
  52. package/bin/src/engine/subagents/subagentManager.js.map +1 -0
  53. package/bin/src/engine/toolRegistry.js +1 -1
  54. package/bin/src/engine/toolRegistry.js.map +1 -1
  55. package/bin/src/engine/voiceEngine.d.ts.map +1 -1
  56. package/bin/src/engine/voiceEngine.js +27 -238
  57. package/bin/src/engine/voiceEngine.js.map +1 -1
  58. package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
  59. package/bin/src/engine/workspace/agentWorkspace.js +22 -9
  60. package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
  61. package/bin/src/index.js +76 -9
  62. package/bin/src/index.js.map +1 -1
  63. package/bin/src/mcp/client.js +1 -1
  64. package/bin/src/server/server.d.ts.map +1 -1
  65. package/bin/src/server/server.js +37 -3
  66. package/bin/src/server/server.js.map +1 -1
  67. package/bin/src/utils/admin.d.ts.map +1 -1
  68. package/bin/src/utils/admin.js +63 -0
  69. package/bin/src/utils/admin.js.map +1 -1
  70. package/bin/src/utils/auth.d.ts.map +1 -1
  71. package/bin/src/utils/auth.js +10 -5
  72. package/bin/src/utils/auth.js.map +1 -1
  73. package/bin/src/utils/branding.js +1 -1
  74. package/bin/src/utils/claudeUI.d.ts.map +1 -0
  75. package/bin/src/utils/claudeUI.js +404 -0
  76. package/bin/src/utils/claudeUI.js.map +1 -0
  77. package/bin/src/utils/interruptHandler.d.ts.map +1 -0
  78. package/bin/src/utils/interruptHandler.js +121 -0
  79. package/bin/src/utils/interruptHandler.js.map +1 -0
  80. package/bin/src/utils/payment.d.ts.map +1 -1
  81. package/bin/src/utils/payment.js +1 -1
  82. package/bin/src/utils/payment.js.map +1 -1
  83. package/bin/src/utils/pricing.d.ts.map +1 -1
  84. package/bin/src/utils/pricing.js +106 -45
  85. package/bin/src/utils/pricing.js.map +1 -1
  86. package/bin/src/utils/qrBridge.d.ts.map +1 -1
  87. package/bin/src/utils/qrBridge.js +17 -17
  88. package/bin/src/utils/qrBridge.js.map +1 -1
  89. package/bin/src/utils/rules.d.ts.map +1 -0
  90. package/bin/src/utils/rules.js +105 -0
  91. package/bin/src/utils/rules.js.map +1 -0
  92. package/bin/src/utils/updateCheck.d.ts.map +1 -1
  93. package/bin/src/utils/updateCheck.js +12 -1
  94. package/bin/src/utils/updateCheck.js.map +1 -1
  95. package/firebase.json +1 -1
  96. package/package.json +1 -1
  97. package/web/app.js +110 -52
  98. package/web/styles.css +3 -3
@@ -23,6 +23,10 @@ import { loadAuthConfig, isProOrHigher } from '../utils/auth.js';
23
23
  import { getMCPManager } from '../mcp/index.js';
24
24
  import { writeTelemetry } from '../utils/qrBridge.js';
25
25
  import { compress, LEVEL_FAST } from 'codrop';
26
+ import { CheckpointManager } from './checkpoints/checkpointManager.js';
27
+ import { renderStepHeader, renderThoughtBox, renderToolCall, renderToolResult, renderTaskCompletedCard, } from '../utils/claudeUI.js';
28
+ import { loadWorkspaceRules } from '../utils/rules.js';
29
+ import { InterruptManager } from '../utils/interruptHandler.js';
26
30
  export { AgentRuntime, AgentWorkspace, inferExecutionIntent, resolveDesktopPath, SystemComputerController, MockComputerController };
27
31
  const execAsync = promisify(exec);
28
32
  export function robustSnippetReplace(origContent, target, replacement) {
@@ -293,6 +297,29 @@ export const AGENT_TOOLS = [
293
297
  required: ['text'],
294
298
  },
295
299
  },
300
+ {
301
+ name: 'delegate_subtask',
302
+ description: 'Delegate a specialized subtask to an autonomous specialist subagent (architect, coder, tester, reviewer, general). Useful for delegating code reviews, test generation, architectural breakdowns, or isolated module implementations to an autonomous worker agent.',
303
+ parameters: {
304
+ type: 'object',
305
+ properties: {
306
+ role: {
307
+ type: 'string',
308
+ enum: ['architect', 'coder', 'tester', 'reviewer', 'general'],
309
+ description: 'Specialist role for the subagent (architect, coder, tester, reviewer, general)',
310
+ },
311
+ task: {
312
+ type: 'string',
313
+ description: 'The specific subtask instruction or goal for the subagent to complete',
314
+ },
315
+ context: {
316
+ type: 'string',
317
+ description: 'Relevant file paths, error messages, or architectural context to provide to the subagent',
318
+ },
319
+ },
320
+ required: ['role', 'task'],
321
+ },
322
+ },
296
323
  {
297
324
  name: 'task_completed',
298
325
  description: 'Signal that the user goal has been fully completed with the full answer, results, and summary.',
@@ -383,6 +410,7 @@ export class AgentExecutionLoop {
383
410
  userTier;
384
411
  currentGoal = '';
385
412
  isAudioMode = false;
413
+ agentWorkspace;
386
414
  syncTelemetry(status, lastLog) {
387
415
  try {
388
416
  writeTelemetry({
@@ -403,11 +431,12 @@ export class AgentExecutionLoop {
403
431
  this.mockMode = Boolean(options?.mockMode);
404
432
  this.isAudioMode = Boolean(options?.isAudioMode);
405
433
  this.userTier = (options?.tier || loadAuthConfig().currentTier || 'free').toLowerCase();
406
- // Free tier is bounded to basic direct actions (max 5 steps). Full autonomous multi-step loops require Pro/Enterprise.
407
- const isPaid = this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
408
- const requestedSteps = options?.maxSteps || 30;
409
- this.maxSteps = isPaid ? requestedSteps : Math.min(requestedSteps, 5);
410
- this.taskStateManager = new TaskStateManager(options?.taskId, '', this.cwd);
434
+ // Free tier is bounded to basic direct actions (max 5 steps). Full autonomous multi-step loops require Pro/Enterprise (unlimited steps).
435
+ const isPaid = this.mockMode || isProOrHigher(this.userTier);
436
+ const requestedSteps = options?.maxSteps;
437
+ this.maxSteps = isPaid ? (requestedSteps || Infinity) : Math.min(requestedSteps || 5, 5);
438
+ this.agentWorkspace = new AgentWorkspace(options?.taskId, '', this.cwd);
439
+ this.taskStateManager = this.agentWorkspace.getStateManager();
411
440
  this.computerController = options?.computerController || (this.mockMode ? new MockComputerController() : new SystemComputerController(this.cwd));
412
441
  this.browserController = options?.browserController || new SemanticBrowserController(this.cwd);
413
442
  this.actionVerifier = new ActionVerifier(this.cwd);
@@ -418,6 +447,9 @@ export class AgentExecutionLoop {
418
447
  getUserTier() {
419
448
  return this.userTier;
420
449
  }
450
+ isPaidTier() {
451
+ return this.mockMode || isProOrHigher(this.userTier);
452
+ }
421
453
  getExecutionIntent() {
422
454
  return this.executionIntent;
423
455
  }
@@ -591,9 +623,10 @@ Task Execution Intent & Constraints:
591
623
  • Target Destination: ${this.executionIntent.targetDestination || 'Default'}
592
624
  • Target Artifact: ${this.executionIntent.targetArtifactName || 'None'}
593
625
  ` : '';
594
- const isPaid = this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
626
+ const isPaid = this.isPaidTier();
595
627
  const tierDirective = isPaid ? `
596
628
  PLAN TIER & CAPABILITIES: ${this.userTier.toUpperCase()} (FULL ACCESS UNLOCKED)
629
+ • Unlimited autonomous multi-step loops with no step limits.
597
630
  • Autonomous Computer-Use, Browser Automation, and AI Scratchpad Reasoning are fully ENABLED.
598
631
  • Use the 'scratchpad' tool to organize multi-step reasoning, break down complex goals, draft hypotheses, and track working memory across turns.
599
632
  ` : `
@@ -610,6 +643,8 @@ PLAN TIER & CAPABILITIES: DEVELOPER FREE (BASIC DEVELOPER OPS ONLY)
610
643
  const mcpCapabilitiesSection = mcpRouting.capabilitiesSummary ? `\n${mcpRouting.capabilitiesSummary}\n` : '';
611
644
  const recentSessionsMemory = getRecentSessionsSummary(1);
612
645
  const sessionHistorySection = recentSessionsMemory ? `\n${recentSessionsMemory}\n` : '';
646
+ const workspaceRules = loadWorkspaceRules(this.cwd);
647
+ const rulesSection = workspaceRules.found ? `\n${workspaceRules.formattedPrompt}\n` : '';
613
648
  return `You are Scout Agent, an elite AI Principal Software Engineer & Autonomous Computer-Use Operator (developed by FrontTerrain).
614
649
 
615
650
  Working Directory: "${this.cwd}"
@@ -619,6 +654,7 @@ ${tierDirective}
619
654
  ${intentInfo}
620
655
  ${mcpCapabilitiesSection}
621
656
  ${sessionHistorySection}
657
+ ${rulesSection}
622
658
  Workspace Overview (first 100 files):
623
659
  ${filesList.join('\n')}
624
660
 
@@ -710,6 +746,10 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
710
746
  };
711
747
  }
712
748
  this.historyMessages.push({ role: 'system', content: this.systemPrompt() });
749
+ const workspaceRules = loadWorkspaceRules(this.cwd);
750
+ if (workspaceRules.found) {
751
+ console.log(chalk.cyan(`📋 Loaded workspace rules from ${workspaceRules.fileName}`));
752
+ }
713
753
  // Robust Target File Discovery: Only run file discovery if the goal has explicit code editing/modification intent
714
754
  const simpleAppCheck = checkSimpleAppAction(userGoal);
715
755
  const lowerGoal = userGoal.toLowerCase().trim();
@@ -821,371 +861,448 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
821
861
  content: promptContent,
822
862
  });
823
863
  }
824
- const estTotalSecs = Math.round(this.maxSteps * 12);
825
- const estTimeText = estTotalSecs >= 60 ? `~${(estTotalSecs / 60).toFixed(1)}m` : `~${estTotalSecs}s`;
826
- console.log(chalk.gray(`Estimated completion time: ${estTimeText} (Max ${this.maxSteps} steps)`));
864
+ const isPaid = this.mockMode || isProOrHigher(this.userTier);
865
+ if (!isPaid && Number.isFinite(this.maxSteps)) {
866
+ const estTotalSecs = Math.round(this.maxSteps * 12);
867
+ const estTimeText = estTotalSecs >= 60 ? `~${(estTotalSecs / 60).toFixed(1)}m` : `~${estTotalSecs}s`;
868
+ console.log(chalk.gray(`Estimated completion time: ${estTimeText} (Free Tier Max ${this.maxSteps} steps)`));
869
+ }
870
+ else {
871
+ console.log(chalk.gray(`⚡ Autonomous execution loop: Unlimited steps active (${this.userTier.toUpperCase()} plan)`));
872
+ }
827
873
  let step = 0;
828
874
  let finalSummary = '';
829
875
  const stepDurations = [];
830
- while (step < this.maxSteps) {
831
- if (this.isCancelled) {
832
- return {
833
- success: false,
834
- summary: `Task cancelled: ${this.cancelReason || 'User abort'}`,
835
- };
836
- }
837
- step++;
838
- this.currentStep = step;
839
- this.taskStateManager.startStep(step);
840
- const stepStartTime = Date.now();
841
- const avgStepMs = stepDurations.length > 0
842
- ? stepDurations.reduce((a, b) => a + b, 0) / stepDurations.length
843
- : 12000;
844
- const remainingSteps = (this.maxSteps - step + 1);
845
- const estSecsLeft = Math.max(5, Math.round((remainingSteps * avgStepMs) / 1000));
846
- const estLeftStr = estSecsLeft >= 60
847
- ? `${Math.floor(estSecsLeft / 60)}m ${estSecsLeft % 60}s`
848
- : `${estSecsLeft}s`;
849
- const stepSpinner = spinner();
850
- stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step}/${this.maxSteps} • Est. completion: ~${estLeftStr} left)`));
851
- try {
852
- let response;
853
- const mcpRouting = getMCPManager().routeTools(userGoal);
854
- const mcpToolDefs = mcpRouting.tools.map((mt) => {
855
- let desc = mt.description;
856
- if (mt.originalName === 'execute_blender_code') {
857
- desc = 'Execute Python (bpy) code inside Blender. Use this to procedurally create, model, texture, color, and render 3D meshes, anime/game characters, clothing, hair, materials, cel-shading, lights, cameras, and scenes directly.';
858
- }
859
- else if (mt.originalName === 'generate_3d') {
860
- desc = 'Cloud AI text-to-3d generator (requires external API key). If keys are not set, use execute_blender_code to model directly.';
876
+ const interruptManager = new InterruptManager({
877
+ getStepInfo: () => ({
878
+ step: this.currentStep || 1,
879
+ maxSteps: this.maxSteps,
880
+ goal: this.currentGoal,
881
+ modifiedFilesCount: this.modifiedFiles.size,
882
+ }),
883
+ onPause: () => {
884
+ this.syncTelemetry('paused', `scout: execution paused by user at step ${this.currentStep}`);
885
+ },
886
+ onResume: () => {
887
+ this.syncTelemetry('running', `scout: execution resumed at step ${this.currentStep}`);
888
+ },
889
+ onFeedback: (feedback) => {
890
+ this.historyMessages.push({
891
+ role: 'user',
892
+ content: `[USER STEERING FEEDBACK (INTERRUPTED AT STEP ${this.currentStep})]:\n"${feedback}"\n\nPlease adapt your execution plan accordingly and follow these user steering instructions immediately.`,
893
+ });
894
+ },
895
+ onRevert: () => {
896
+ if (this.backups.length === 0)
897
+ return false;
898
+ for (const b of this.backups) {
899
+ try {
900
+ fs.writeFileSync(b.filePath, b.originalContent, 'utf-8');
861
901
  }
902
+ catch { }
903
+ }
904
+ this.modifiedFiles.clear();
905
+ return true;
906
+ },
907
+ onAbort: (reason) => {
908
+ this.isCancelled = true;
909
+ this.cancelReason = reason;
910
+ },
911
+ });
912
+ interruptManager.attach();
913
+ try {
914
+ while (step < this.maxSteps) {
915
+ if (this.isCancelled) {
862
916
  return {
863
- name: mt.originalName,
864
- description: `[MCP: ${mt.serverName}] ${desc.length > 160 ? desc.slice(0, 157) + '...' : desc}`,
865
- parameters: {
866
- type: 'object',
867
- properties: mt.inputSchema.properties || {},
868
- required: mt.inputSchema.required || [],
869
- },
870
- source: 'mcp',
871
- mcpServerId: mt.serverId,
872
- mcpServerName: mt.serverName,
917
+ success: false,
918
+ summary: `Task cancelled: ${this.cancelReason || 'User abort'}`,
873
919
  };
920
+ }
921
+ step++;
922
+ this.currentStep = step;
923
+ this.taskStateManager.startStep(step);
924
+ // Context Compaction check: If history is long, compact middle turns using workspace state (including scratchpad working memory)
925
+ if (this.historyMessages.length > 10) {
926
+ this.historyMessages = this.agentWorkspace.compactHistoryMessages(this.historyMessages, 6);
927
+ }
928
+ const stepStartTime = Date.now();
929
+ renderStepHeader({
930
+ step,
931
+ maxSteps: this.maxSteps,
932
+ startTimeMs: this.startTime,
933
+ tokensConsumed: this.totalTokensConsumed,
934
+ userTier: this.userTier,
874
935
  });
875
- const goalLower = (userGoal || '').toLowerCase();
876
- const needsBrowser = goalLower.includes('browser') || goalLower.includes('website') || goalLower.includes('webpage') || goalLower.includes('http') || goalLower.includes('url') || goalLower.includes('scrape') || goalLower.includes('chrome');
877
- const needsComputer = goalLower.includes('computer') || goalLower.includes('desktop') || goalLower.includes('mouse') || goalLower.includes('coordinate') || goalLower.includes('drag') || goalLower.includes('double click');
878
- let baseTools = this.toolRegistry.getToolsForTier(this.userTier);
879
- if (mcpToolDefs.length > 0 && !needsBrowser && !needsComputer) {
880
- const ESSENTIAL_BASE_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'run_command', 'list_dir', 'grep_search', 'task_completed', 'ask_user', 'speak_text', 'scratchpad', 'computer_open_app']);
881
- baseTools = baseTools.filter((t) => ESSENTIAL_BASE_TOOLS.has(t.name));
936
+ const stepSpinner = spinner();
937
+ if (isPaid && !Number.isFinite(this.maxSteps)) {
938
+ stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step})`));
882
939
  }
883
- else if (!needsBrowser) {
884
- baseTools = baseTools.filter((t) => !t.name.startsWith('browser_'));
940
+ else {
941
+ const avgStepMs = stepDurations.length > 0
942
+ ? stepDurations.reduce((a, b) => a + b, 0) / stepDurations.length
943
+ : 12000;
944
+ const remainingSteps = (this.maxSteps - step + 1);
945
+ const estSecsLeft = Math.max(5, Math.round((remainingSteps * avgStepMs) / 1000));
946
+ const estLeftStr = estSecsLeft >= 60
947
+ ? `${Math.floor(estSecsLeft / 60)}m ${estSecsLeft % 60}s`
948
+ : `${estSecsLeft}s`;
949
+ stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step}/${this.maxSteps} • Est. completion: ~${estLeftStr} left)`));
885
950
  }
886
- const activeLoopTools = [...baseTools, ...mcpToolDefs];
887
951
  try {
888
- response = await callOpenAIWithRetry(async (model) => {
889
- return await openai.chat.completions.create({
890
- model,
891
- messages: sanitizeMessages(this.historyMessages),
892
- tools: activeLoopTools.map((t) => ({ type: 'function', function: t })),
893
- tool_choice: 'auto',
894
- temperature: 0.1,
895
- presence_penalty: 0.1,
896
- frequency_penalty: 0.1,
897
- });
898
- });
899
- }
900
- catch (err) {
901
- const errStr = String(err?.message || err?.error || err || '').toLowerCase();
902
- if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported') || errStr.includes('reasoning')) {
903
- // Model doesn't support native function calling inject tool-call formatting hint
904
- // so extractJsonToolCall can parse the response as a structured tool invocation
905
- const availableToolNames = activeLoopTools.map((t) => t.name).join(', ');
906
- const toolHintMsg = {
907
- role: 'user',
908
- 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:
909
- \`\`\`json
910
- { "tool": "tool_name", "args": { ... } }
911
- \`\`\`
912
- Available tools: ${availableToolNames}
913
- 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!`,
952
+ let response;
953
+ const mcpRouting = getMCPManager().routeTools(userGoal);
954
+ const mcpToolDefs = mcpRouting.tools.map((mt) => {
955
+ let desc = mt.description;
956
+ if (mt.originalName === 'execute_blender_code') {
957
+ desc = 'Execute Python (bpy) code inside Blender. Use this to procedurally create, model, texture, color, and render 3D meshes, anime/game characters, clothing, hair, materials, cel-shading, lights, cameras, and scenes directly.';
958
+ }
959
+ else if (mt.originalName === 'generate_3d') {
960
+ desc = 'Cloud AI text-to-3d generator (requires external API key). If keys are not set, use execute_blender_code to model directly.';
961
+ }
962
+ return {
963
+ name: mt.originalName,
964
+ description: `[MCP: ${mt.serverName}] ${desc.length > 160 ? desc.slice(0, 157) + '...' : desc}`,
965
+ parameters: {
966
+ type: 'object',
967
+ properties: mt.inputSchema.properties || {},
968
+ required: mt.inputSchema.required || [],
969
+ },
970
+ source: 'mcp',
971
+ mcpServerId: mt.serverId,
972
+ mcpServerName: mt.serverName,
914
973
  };
915
- const fallbackMessages = [...this.historyMessages, toolHintMsg];
974
+ });
975
+ const goalLower = (userGoal || '').toLowerCase();
976
+ const needsBrowser = goalLower.includes('browser') || goalLower.includes('website') || goalLower.includes('webpage') || goalLower.includes('http') || goalLower.includes('url') || goalLower.includes('scrape') || goalLower.includes('chrome');
977
+ const needsComputer = goalLower.includes('computer') || goalLower.includes('desktop') || goalLower.includes('mouse') || goalLower.includes('coordinate') || goalLower.includes('drag') || goalLower.includes('double click');
978
+ let baseTools = this.toolRegistry.getToolsForTier(this.userTier);
979
+ if (mcpToolDefs.length > 0 && !needsBrowser && !needsComputer) {
980
+ const ESSENTIAL_BASE_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'run_command', 'list_dir', 'grep_search', 'task_completed', 'ask_user', 'speak_text', 'scratchpad', 'computer_open_app']);
981
+ baseTools = baseTools.filter((t) => ESSENTIAL_BASE_TOOLS.has(t.name));
982
+ }
983
+ else if (!needsBrowser) {
984
+ baseTools = baseTools.filter((t) => !t.name.startsWith('browser_'));
985
+ }
986
+ const activeLoopTools = [...baseTools, ...mcpToolDefs];
987
+ try {
916
988
  response = await callOpenAIWithRetry(async (model) => {
917
989
  return await openai.chat.completions.create({
918
990
  model,
919
- messages: sanitizeMessages(fallbackMessages),
991
+ messages: sanitizeMessages(this.historyMessages),
992
+ tools: activeLoopTools.map((t) => ({ type: 'function', function: t })),
993
+ tool_choice: 'auto',
920
994
  temperature: 0.1,
921
995
  presence_penalty: 0.1,
922
996
  frequency_penalty: 0.1,
923
997
  });
924
998
  });
925
999
  }
926
- else {
927
- throw err;
928
- }
929
- }
930
- const choice = response.choices[0];
931
- if (!choice) {
932
- stepSpinner.stop(chalk.yellow('No response from Scout. Retrying step...'));
933
- continue;
934
- }
935
- const msg = sanitizeMessage(choice.message);
936
- const tokensThisTurn = response?.usage?.total_tokens || 0;
937
- this.totalTokensConsumed += tokensThisTurn;
938
- this.syncTelemetry('running', `scout: [step ${step}] generated reasoning (${tokensThisTurn} tokens)`);
939
- this.historyMessages.push(msg);
940
- // Render assistant's thought reasoning text if provided on this turn
941
- const textContent = msg.content || '';
942
- if (textContent.trim()) {
943
- safeNote(renderMarkdown(textContent), `Scout Thought (Step ${step})`);
944
- if (this.isAudioMode) {
945
- const speechText = cleanTextForSpeech(textContent);
946
- if (speechText) {
947
- speakText(speechText.length > 200 ? speechText.slice(0, 200) + '...' : speechText, { async: true });
1000
+ catch (err) {
1001
+ const errStr = String(err?.message || err?.error || err || '').toLowerCase();
1002
+ if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported') || errStr.includes('reasoning')) {
1003
+ // Model doesn't support native function calling inject tool-call formatting hint
1004
+ // so extractJsonToolCall can parse the response as a structured tool invocation
1005
+ const availableToolNames = activeLoopTools.map((t) => t.name).join(', ');
1006
+ const toolHintMsg = {
1007
+ role: 'user',
1008
+ 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:
1009
+ \`\`\`json
1010
+ { "tool": "tool_name", "args": { ... } }
1011
+ \`\`\`
1012
+ Available tools: ${availableToolNames}
1013
+ 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!`,
1014
+ };
1015
+ const fallbackMessages = [...this.historyMessages, toolHintMsg];
1016
+ response = await callOpenAIWithRetry(async (model) => {
1017
+ return await openai.chat.completions.create({
1018
+ model,
1019
+ messages: sanitizeMessages(fallbackMessages),
1020
+ temperature: 0.1,
1021
+ presence_penalty: 0.1,
1022
+ frequency_penalty: 0.1,
1023
+ });
1024
+ });
1025
+ }
1026
+ else {
1027
+ throw err;
948
1028
  }
949
1029
  }
950
- }
951
- // Check native tool calls
952
- if (msg.tool_calls && msg.tool_calls.length > 0) {
953
- stepSpinner.stop(chalk.green(`Step ${step}: Scout issued ${msg.tool_calls.length} tool call(s).`));
954
- for (const tc of msg.tool_calls) {
955
- if (tc.type === 'function' && tc.function) {
956
- const fnName = tc.function.name;
957
- let args = {};
958
- try {
959
- args = JSON.parse(tc.function.arguments || '{}');
960
- }
961
- catch { }
962
- if (this.isAudioMode && fnName !== 'task_completed') {
963
- let actionPhrase = '';
964
- if (fnName === 'run_command')
965
- actionPhrase = `Running command ${args.command ? args.command.slice(0, 40) : ''}`;
966
- else if (fnName === 'read_file')
967
- actionPhrase = `Reading ${path.basename(args.filePath || '')}`;
968
- else if (fnName === 'write_file')
969
- actionPhrase = `Creating ${path.basename(args.filePath || '')}`;
970
- else if (fnName === 'edit_file')
971
- actionPhrase = `Editing ${path.basename(args.filePath || '')}`;
972
- else if (fnName.startsWith('mcp_') || fnName.includes('github'))
973
- actionPhrase = `Querying ${fnName}`;
974
- if (actionPhrase)
975
- speakText(actionPhrase, { async: true });
976
- }
977
- const toolResult = await this.dispatchToolCall(fnName, args);
978
- this.totalToolCalls++;
979
- this.syncTelemetry('running', `scout: [step ${step}] executed tool ${fnName}`);
980
- const toolResStr = String(toolResult.result || '');
981
- let cappedResult = toolResStr;
982
- if (toolResStr.length > 800) {
1030
+ const choice = response.choices[0];
1031
+ if (!choice) {
1032
+ stepSpinner.stop(chalk.yellow('No response from Scout. Retrying step...'));
1033
+ continue;
1034
+ }
1035
+ const msg = sanitizeMessage(choice.message);
1036
+ const tokensThisTurn = response?.usage?.total_tokens || 0;
1037
+ this.totalTokensConsumed += tokensThisTurn;
1038
+ this.syncTelemetry('running', `scout: [step ${step}] generated reasoning (${tokensThisTurn} tokens)`);
1039
+ this.historyMessages.push(msg);
1040
+ // Render assistant's thought reasoning text if provided on this turn
1041
+ const textContent = msg.content || '';
1042
+ if (textContent.trim()) {
1043
+ renderThoughtBox(textContent, step);
1044
+ }
1045
+ // Check native tool calls
1046
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
1047
+ stepSpinner.stop(chalk.green(`Step ${step}: Scout issued ${msg.tool_calls.length} tool call(s).`));
1048
+ for (const tc of msg.tool_calls) {
1049
+ if (tc.type === 'function' && tc.function) {
1050
+ const fnName = tc.function.name;
1051
+ let args = {};
983
1052
  try {
984
- const origBytes = Buffer.byteLength(toolResStr, 'utf-8');
985
- const cdp = compress(Buffer.from(toolResStr, 'utf-8'), LEVEL_FAST);
986
- const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
987
- cappedResult = toolResStr.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
988
- this.syncTelemetry('running', `scout: [codrop] compressed ${fnName} output (${ratio}x ratio)`);
1053
+ args = JSON.parse(tc.function.arguments || '{}');
989
1054
  }
990
- catch {
991
- cappedResult = toolResStr.slice(0, 800) + '\n... [output truncated for token efficiency]';
992
- }
993
- }
994
- this.historyMessages.push({
995
- role: 'user',
996
- tool_call_id: tc.id,
997
- content: `Tool Execution Result (${fnName}):\n${cappedResult}`,
998
- });
999
- this.handleToolConsecutiveTracking(fnName, args, step);
1000
- // Automatic immediate completion for simple app actions (open/close/switch window)
1001
- if (simpleAppCheck.isSimple && (fnName === 'computer_open_app' || fnName === 'computer_close_app' || fnName === 'computer_switch_window')) {
1002
- let callSucceeded = false;
1003
- try {
1004
- const parsed = JSON.parse(toolResult.result);
1005
- callSucceeded = parsed.success !== false;
1055
+ catch { }
1056
+ const toolStartTime = Date.now();
1057
+ const toolResult = await this.dispatchToolCall(fnName, args);
1058
+ const durationMs = Date.now() - toolStartTime;
1059
+ renderToolResult({
1060
+ name: fnName,
1061
+ args,
1062
+ result: toolResult.result,
1063
+ durationMs,
1064
+ success: !String(toolResult.result).startsWith('[PLAN RESTRICTION') && !String(toolResult.result).startsWith('[ACTION BLOCKED'),
1065
+ });
1066
+ this.totalToolCalls++;
1067
+ this.syncTelemetry('running', `scout: [step ${step}] executed tool ${fnName}`);
1068
+ const toolResStr = String(toolResult.result || '');
1069
+ let cappedResult = toolResStr;
1070
+ if (toolResStr.length > 800) {
1071
+ try {
1072
+ const origBytes = Buffer.byteLength(toolResStr, 'utf-8');
1073
+ const cdp = compress(Buffer.from(toolResStr, 'utf-8'), LEVEL_FAST);
1074
+ const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
1075
+ cappedResult = toolResStr.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
1076
+ this.syncTelemetry('running', `scout: [codrop] compressed ${fnName} output (${ratio}x ratio)`);
1077
+ }
1078
+ catch {
1079
+ cappedResult = toolResStr.slice(0, 800) + '\n... [output truncated for token efficiency]';
1080
+ }
1006
1081
  }
1007
- catch {
1008
- callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
1082
+ this.historyMessages.push({
1083
+ role: 'user',
1084
+ tool_call_id: tc.id,
1085
+ content: `Tool Execution Result (${fnName}):\n${cappedResult}`,
1086
+ });
1087
+ this.handleToolConsecutiveTracking(fnName, args, step);
1088
+ // Automatic immediate completion for simple app actions (open/close/switch window)
1089
+ if (simpleAppCheck.isSimple && (fnName === 'computer_open_app' || fnName === 'computer_close_app' || fnName === 'computer_switch_window')) {
1090
+ let callSucceeded = false;
1091
+ try {
1092
+ const parsed = JSON.parse(toolResult.result);
1093
+ callSucceeded = parsed.success !== false;
1094
+ }
1095
+ catch {
1096
+ callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
1097
+ }
1098
+ if (callSucceeded) {
1099
+ const appName = simpleAppCheck.appName || 'application';
1100
+ const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
1101
+ finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
1102
+ renderTaskCompletedCard({
1103
+ objective: userGoal,
1104
+ summary: finalSummary,
1105
+ modifiedFiles: [],
1106
+ steps: step,
1107
+ maxSteps: this.maxSteps,
1108
+ totalTokens: this.totalTokensConsumed,
1109
+ totalToolCalls: this.totalToolCalls,
1110
+ elapsedSecs: Math.round((Date.now() - this.startTime) / 1000),
1111
+ userTier: this.userTier,
1112
+ });
1113
+ saveAgentSession({
1114
+ goal: userGoal,
1115
+ summary: finalSummary,
1116
+ modifiedFiles: [],
1117
+ });
1118
+ this.syncTelemetry('idle', `scout: completed task successfully (${this.modifiedFiles.size} files modified)`);
1119
+ return { success: true, summary: finalSummary };
1120
+ }
1009
1121
  }
1010
- if (callSucceeded) {
1011
- const appName = simpleAppCheck.appName || 'application';
1012
- const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
1013
- finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
1014
- safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
1122
+ if (fnName === 'task_completed') {
1123
+ finalSummary = args.summary || toolResult.result;
1124
+ if (this.modifiedFiles.size > 0) {
1125
+ const filesList = getDirectoryFiles(this.cwd);
1126
+ const healRes = await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, path.basename(this.cwd), filesList, { maxRetries: 3 });
1127
+ if (healRes.verifiedFiles.length > 0) {
1128
+ safeNote(chalk.green(` Self-Healing Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build clean!`), ' Code Verification Clean');
1129
+ }
1130
+ if (healRes.remainingErrors.length > 0) {
1131
+ safeNote(chalk.yellow(`️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '️ Verification Warning');
1132
+ }
1133
+ }
1134
+ renderTaskCompletedCard({
1135
+ objective: userGoal,
1136
+ summary: finalSummary,
1137
+ modifiedFiles: Array.from(this.modifiedFiles),
1138
+ steps: step,
1139
+ maxSteps: this.maxSteps,
1140
+ totalTokens: this.totalTokensConsumed,
1141
+ totalToolCalls: this.totalToolCalls,
1142
+ elapsedSecs: Math.round((Date.now() - this.startTime) / 1000),
1143
+ userTier: this.userTier,
1144
+ });
1015
1145
  saveAgentSession({
1016
1146
  goal: userGoal,
1017
1147
  summary: finalSummary,
1018
- modifiedFiles: [],
1148
+ modifiedFiles: Array.from(this.modifiedFiles),
1019
1149
  });
1020
- this.syncTelemetry('idle', `scout: completed task successfully (${this.modifiedFiles.size} files modified)`);
1021
1150
  return { success: true, summary: finalSummary };
1022
1151
  }
1023
1152
  }
1024
- if (fnName === 'task_completed') {
1025
- finalSummary = args.summary || toolResult.result;
1026
- if (this.modifiedFiles.size > 0) {
1027
- const filesList = getDirectoryFiles(this.cwd);
1028
- const healRes = await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, path.basename(this.cwd), filesList, { maxRetries: 3 });
1029
- if (healRes.verifiedFiles.length > 0) {
1030
- safeNote(chalk.green(` Self-Healing Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build clean!`), ' Code Verification Clean');
1031
- }
1032
- if (healRes.remainingErrors.length > 0) {
1033
- safeNote(chalk.yellow(`️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '️ Verification Warning');
1034
- }
1035
- }
1153
+ }
1154
+ continue;
1155
+ }
1156
+ // Check text response or fallback JSON tool call
1157
+ stepSpinner.stop(chalk.blue(`Step ${step} thinking complete.`));
1158
+ const parsedJsonTool = this.extractJsonToolCall(textContent);
1159
+ if (parsedJsonTool) {
1160
+ const toolStartTime = Date.now();
1161
+ const toolResult = await this.dispatchToolCall(parsedJsonTool.tool, parsedJsonTool.args);
1162
+ const durationMs = Date.now() - toolStartTime;
1163
+ renderToolResult({
1164
+ name: parsedJsonTool.tool,
1165
+ args: parsedJsonTool.args,
1166
+ result: toolResult.result,
1167
+ durationMs,
1168
+ success: !String(toolResult.result).startsWith('[PLAN RESTRICTION') && !String(toolResult.result).startsWith('[ACTION BLOCKED'),
1169
+ });
1170
+ const jsonResStr = String(toolResult.result || '');
1171
+ let cappedJsonResult = jsonResStr;
1172
+ if (jsonResStr.length > 800) {
1173
+ try {
1174
+ const origBytes = Buffer.byteLength(jsonResStr, 'utf-8');
1175
+ const cdp = compress(Buffer.from(jsonResStr, 'utf-8'), LEVEL_FAST);
1176
+ const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
1177
+ cappedJsonResult = jsonResStr.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
1178
+ }
1179
+ catch {
1180
+ cappedJsonResult = jsonResStr.slice(0, 800) + '\n... [output truncated for token efficiency]';
1181
+ }
1182
+ }
1183
+ this.historyMessages.push({
1184
+ role: 'user',
1185
+ content: `Tool Execution Result (${parsedJsonTool.tool}):\n${cappedJsonResult}`,
1186
+ });
1187
+ this.handleToolConsecutiveTracking(parsedJsonTool.tool, parsedJsonTool.args, step);
1188
+ // Automatic immediate completion for simple app actions (open/close/switch window)
1189
+ if (simpleAppCheck.isSimple && (parsedJsonTool.tool === 'computer_open_app' || parsedJsonTool.tool === 'computer_close_app' || parsedJsonTool.tool === 'computer_switch_window')) {
1190
+ let callSucceeded = false;
1191
+ try {
1192
+ const parsed = JSON.parse(toolResult.result);
1193
+ callSucceeded = parsed.success !== false;
1194
+ }
1195
+ catch {
1196
+ callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
1197
+ }
1198
+ if (callSucceeded) {
1199
+ const appName = simpleAppCheck.appName || 'application';
1200
+ const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
1201
+ finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
1202
+ safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
1036
1203
  saveAgentSession({
1037
1204
  goal: userGoal,
1038
1205
  summary: finalSummary,
1039
- modifiedFiles: Array.from(this.modifiedFiles),
1206
+ modifiedFiles: [],
1040
1207
  });
1041
1208
  return { success: true, summary: finalSummary };
1042
1209
  }
1043
1210
  }
1044
- }
1045
- continue;
1046
- }
1047
- // Check text response or fallback JSON tool call
1048
- stepSpinner.stop(chalk.blue(`Step ${step} thinking complete.`));
1049
- const parsedJsonTool = this.extractJsonToolCall(textContent);
1050
- if (parsedJsonTool) {
1051
- const toolResult = await this.dispatchToolCall(parsedJsonTool.tool, parsedJsonTool.args);
1052
- const jsonResStr = String(toolResult.result || '');
1053
- let cappedJsonResult = jsonResStr;
1054
- if (jsonResStr.length > 800) {
1055
- try {
1056
- const origBytes = Buffer.byteLength(jsonResStr, 'utf-8');
1057
- const cdp = compress(Buffer.from(jsonResStr, 'utf-8'), LEVEL_FAST);
1058
- const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
1059
- cappedJsonResult = jsonResStr.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
1060
- }
1061
- catch {
1062
- cappedJsonResult = jsonResStr.slice(0, 800) + '\n... [output truncated for token efficiency]';
1063
- }
1064
- }
1065
- this.historyMessages.push({
1066
- role: 'user',
1067
- content: `Tool Execution Result (${parsedJsonTool.tool}):\n${cappedJsonResult}`,
1068
- });
1069
- this.handleToolConsecutiveTracking(parsedJsonTool.tool, parsedJsonTool.args, step);
1070
- // Automatic immediate completion for simple app actions (open/close/switch window)
1071
- if (simpleAppCheck.isSimple && (parsedJsonTool.tool === 'computer_open_app' || parsedJsonTool.tool === 'computer_close_app' || parsedJsonTool.tool === 'computer_switch_window')) {
1072
- let callSucceeded = false;
1073
- try {
1074
- const parsed = JSON.parse(toolResult.result);
1075
- callSucceeded = parsed.success !== false;
1076
- }
1077
- catch {
1078
- callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
1079
- }
1080
- if (callSucceeded) {
1081
- const appName = simpleAppCheck.appName || 'application';
1082
- const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
1083
- finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
1084
- safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
1211
+ if (parsedJsonTool.tool === 'task_completed') {
1212
+ finalSummary = parsedJsonTool.args?.summary || toolResult.result;
1085
1213
  saveAgentSession({
1086
1214
  goal: userGoal,
1087
1215
  summary: finalSummary,
1088
- modifiedFiles: [],
1216
+ modifiedFiles: Array.from(this.modifiedFiles),
1089
1217
  });
1090
1218
  return { success: true, summary: finalSummary };
1091
1219
  }
1220
+ continue;
1092
1221
  }
1093
- if (parsedJsonTool.tool === 'task_completed') {
1094
- finalSummary = parsedJsonTool.args?.summary || toolResult.result;
1095
- saveAgentSession({
1096
- goal: userGoal,
1097
- summary: finalSummary,
1098
- modifiedFiles: Array.from(this.modifiedFiles),
1099
- });
1100
- return { success: true, summary: finalSummary };
1222
+ if (textContent.trim()) {
1223
+ let cleanedThought = textContent
1224
+ .replace(/[\u0600-\u06FF\u0750-\u077F\uAC00-\uD7AF\u3040-\u30FF\u4E00-\u9FFF\u0D80-\u0DFF]+/g, '')
1225
+ .trim();
1226
+ if (!cleanedThought || cleanedThought.length < 5) {
1227
+ cleanedThought = 'Analyzing codebase files and executing next tool operation...';
1228
+ }
1229
+ safeNote(renderMarkdown(cleanedThought), ` Scout Agent Thought (Step ${step})`);
1230
+ const lowerText = textContent.toLowerCase().trim();
1231
+ const isExplicitCompletion = lowerText.includes('task is complete') ||
1232
+ lowerText.includes('task complete') ||
1233
+ lowerText.includes('goal completed') ||
1234
+ lowerText.includes('goal is completed') ||
1235
+ lowerText.includes('all tasks completed') ||
1236
+ lowerText.includes('i have completed') ||
1237
+ lowerText.includes('no further changes needed') ||
1238
+ lowerText.includes('the fix is complete') ||
1239
+ lowerText.includes('has been created') ||
1240
+ lowerText.includes('successfully created') ||
1241
+ lowerText.includes('created the file') ||
1242
+ lowerText.includes('file created') ||
1243
+ lowerText.includes('implementation complete') ||
1244
+ lowerText.includes('setup is complete') ||
1245
+ lowerText.includes('setup complete') ||
1246
+ lowerText.includes('instructions have been executed') ||
1247
+ lowerText.includes('work is complete');
1248
+ const isIntermediateThought = /^(?:the\s+user\s+wants|i\s+will|i'll|let\s+me|i\s+need\s+to|i\s+am\s+going\s+to|first|starting|next|looking\s+at|checking|we\s+need\s+to|we\s+will)\b/i.test(lowerText);
1249
+ const hasActionDirective = /\b(execute|run|setup|set\s+up|install|configure|create|make|write|edit|modify|fix|update|refactor|build|delete|remove|patch|implement|apply|download|save|deploy)\b/i.test(userGoal);
1250
+ const isInformationalGoal = !hasActionDirective && (/^(?:lookup|look\s*up|check|find|who|what|why|where|how|tell|show|explain|display|list|search|query|status|info|describe)\b/i.test(userGoal.trim()) ||
1251
+ !/\b(create|make|write|edit|modify|fix|update|refactor|build|delete|remove|patch|implement|execute|run|setup|set\s+up|install)\b/i.test(userGoal.trim()));
1252
+ const isAnsweringInformational = !isIntermediateThought && isInformationalGoal && textContent.trim().length > 20 && !lowerText.includes('error occurred') && !lowerText.includes('cannot complete');
1253
+ const isSummaryAfterModifications = !isIntermediateThought && this.modifiedFiles.size > 0 && !lowerText.includes('?') && textContent.trim().length > 30;
1254
+ if (isExplicitCompletion || isAnsweringInformational || isSummaryAfterModifications) {
1255
+ saveAgentSession({
1256
+ goal: userGoal,
1257
+ summary: textContent,
1258
+ modifiedFiles: Array.from(this.modifiedFiles),
1259
+ });
1260
+ return { success: true, summary: textContent };
1261
+ }
1101
1262
  }
1102
- continue;
1263
+ // If assistant responded with text without calling tools, prompt it to execute tools to complete the goal
1264
+ this.historyMessages.push({
1265
+ role: 'user',
1266
+ 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.',
1267
+ });
1268
+ stepDurations.push(Date.now() - stepStartTime);
1103
1269
  }
1104
- if (textContent.trim()) {
1105
- let cleanedThought = textContent
1106
- .replace(/[\u0600-\u06FF\u0750-\u077F\uAC00-\uD7AF\u3040-\u30FF\u4E00-\u9FFF\u0D80-\u0DFF]+/g, '')
1107
- .trim();
1108
- if (!cleanedThought || cleanedThought.length < 5) {
1109
- cleanedThought = 'Analyzing codebase files and executing next tool operation...';
1110
- }
1111
- safeNote(renderMarkdown(cleanedThought), ` Scout Agent Thought (Step ${step})`);
1112
- const lowerText = textContent.toLowerCase().trim();
1113
- const isExplicitCompletion = lowerText.includes('task is complete') ||
1114
- lowerText.includes('task complete') ||
1115
- lowerText.includes('goal completed') ||
1116
- lowerText.includes('goal is completed') ||
1117
- lowerText.includes('all tasks completed') ||
1118
- lowerText.includes('i have completed') ||
1119
- lowerText.includes('no further changes needed') ||
1120
- lowerText.includes('the fix is complete') ||
1121
- lowerText.includes('has been created') ||
1122
- lowerText.includes('successfully created') ||
1123
- lowerText.includes('created the file') ||
1124
- lowerText.includes('file created') ||
1125
- lowerText.includes('implementation complete') ||
1126
- lowerText.includes('setup is complete') ||
1127
- lowerText.includes('setup complete') ||
1128
- lowerText.includes('instructions have been executed') ||
1129
- lowerText.includes('work is complete');
1130
- const isIntermediateThought = /^(?:the\s+user\s+wants|i\s+will|i'll|let\s+me|i\s+need\s+to|i\s+am\s+going\s+to|first|starting|next|looking\s+at|checking|we\s+need\s+to|we\s+will)\b/i.test(lowerText);
1131
- const hasActionDirective = /\b(execute|run|setup|set\s+up|install|configure|create|make|write|edit|modify|fix|update|refactor|build|delete|remove|patch|implement|apply|download|save|deploy)\b/i.test(userGoal);
1132
- const isInformationalGoal = !hasActionDirective && (/^(?:lookup|look\s*up|check|find|who|what|why|where|how|tell|show|explain|display|list|search|query|status|info|describe)\b/i.test(userGoal.trim()) ||
1133
- !/\b(create|make|write|edit|modify|fix|update|refactor|build|delete|remove|patch|implement|execute|run|setup|set\s+up|install)\b/i.test(userGoal.trim()));
1134
- const isAnsweringInformational = !isIntermediateThought && isInformationalGoal && textContent.trim().length > 20 && !lowerText.includes('error occurred') && !lowerText.includes('cannot complete');
1135
- const isSummaryAfterModifications = !isIntermediateThought && this.modifiedFiles.size > 0 && !lowerText.includes('?') && textContent.trim().length > 30;
1136
- if (isExplicitCompletion || isAnsweringInformational || isSummaryAfterModifications) {
1137
- saveAgentSession({
1138
- goal: userGoal,
1139
- summary: textContent,
1140
- modifiedFiles: Array.from(this.modifiedFiles),
1141
- });
1142
- return { success: true, summary: textContent };
1270
+ catch (err) {
1271
+ stepDurations.push(Date.now() - stepStartTime);
1272
+ if (isQuotaExceededError(err)) {
1273
+ stepSpinner.stop(chalk.red(`Oops! it\'s not you, it\'s us`));
1274
+ safeNote(`${chalk.bold.red(`Something went wrong in (Step ${step}), please try again in a moment`)}\n\n` +
1275
+ `${chalk.yellow('The configured AI provider has reached its API usage limit or rate cap.')}\n` +
1276
+ `${chalk.dim('This is separate from your Scout credit balance shown by `scout quota`.')}\n\n` +
1277
+ `${chalk.bold.cyan(' Please come back and try again in a few hours (or check back later today).')}\n\n` +
1278
+ `${chalk.dim('Scout Agent session has ended gracefully to protect remaining workflow.')}`, '️ API Quota Limit Reached');
1279
+ return {
1280
+ success: false,
1281
+ summary: 'Something went wrong, please try again in a moment.',
1282
+ };
1143
1283
  }
1284
+ stepSpinner.stop(chalk.red(`Step ${step} execution error: ${err?.message || String(err)}`));
1285
+ this.historyMessages.push({
1286
+ role: 'user',
1287
+ content: `Error in previous turn: ${err?.message || String(err)}. Please try alternative steps or call tools.`,
1288
+ });
1144
1289
  }
1145
- // If assistant responded with text without calling tools, prompt it to execute tools to complete the goal
1146
- this.historyMessages.push({
1147
- role: 'user',
1148
- 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.',
1149
- });
1150
- stepDurations.push(Date.now() - stepStartTime);
1151
- }
1152
- catch (err) {
1153
- stepDurations.push(Date.now() - stepStartTime);
1154
- if (isQuotaExceededError(err)) {
1155
- stepSpinner.stop(chalk.red(`Oops! it\'s not you, it\'s us`));
1156
- safeNote(`${chalk.bold.red(`Something went wrong in (Step ${step}), please try again in a moment`)}\n\n` +
1157
- `${chalk.yellow('The configured AI provider has reached its API usage limit or rate cap.')}\n` +
1158
- `${chalk.dim('This is separate from your Scout credit balance shown by `scout quota`.')}\n\n` +
1159
- `${chalk.bold.cyan(' Please come back and try again in a few hours (or check back later today).')}\n\n` +
1160
- `${chalk.dim('Scout Agent session has ended gracefully to protect remaining workflow.')}`, '️ API Quota Limit Reached');
1161
- return {
1162
- success: false,
1163
- summary: 'Something went wrong, please try again in a moment.',
1164
- };
1165
- }
1166
- stepSpinner.stop(chalk.red(`Step ${step} execution error: ${err?.message || String(err)}`));
1167
- this.historyMessages.push({
1168
- role: 'user',
1169
- content: `Error in previous turn: ${err?.message || String(err)}. Please try alternative steps or call tools.`,
1170
- });
1171
- }
1172
- // Step limit check: Auto-extend by +15 steps ONLY for Pro & Enterprise (up to 60 max threshold)
1173
- const isPaid = this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
1174
- if (step >= this.maxSteps) {
1175
- if (isPaid && this.maxSteps < 60) {
1176
- this.maxSteps += 15;
1177
- 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');
1178
- }
1179
- else if (!isPaid) {
1290
+ // Step limit check: Free tier only. Pro and Enterprise have unlimited steps.
1291
+ if (!isPaid && step >= this.maxSteps) {
1180
1292
  safeNote(chalk.bold.yellow(`⚡ Free Tier limit reached (${this.maxSteps} steps).\nFree developers can perform basic code editing, creating files/folders, and running commands.\nTo unlock unlimited autonomous multi-step loops and scratchpad reasoning, upgrade to Pro at https://scout.frontterrain.com/plans or run 'scout dashboard'.`), ' Free Tier Step Limit');
1181
1293
  break;
1182
1294
  }
1183
1295
  }
1296
+ return {
1297
+ success: false,
1298
+ summary: isPaid
1299
+ ? `Task ended after ${step} iteration steps. Modified files: ${Array.from(this.modifiedFiles).join(', ')}`
1300
+ : `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
1301
+ };
1302
+ }
1303
+ finally {
1304
+ interruptManager.detach();
1184
1305
  }
1185
- return {
1186
- success: false,
1187
- summary: `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
1188
- };
1189
1306
  }
1190
1307
  extractJsonToolCall(content) {
1191
1308
  if (!content)
@@ -1206,7 +1323,7 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1206
1323
  'read_file', 'write_file', 'edit_file', 'run_command', 'list_dir',
1207
1324
  'grep_search', 'glob_search', 'tree_view', 'file_info', 'multi_edit_file',
1208
1325
  'fetch_url', 'git_diff',
1209
- 'ask_user', 'speak_text', 'task_completed', 'scratchpad',
1326
+ 'ask_user', 'speak_text', 'delegate_subtask', 'task_completed', 'scratchpad',
1210
1327
  ];
1211
1328
  for (const toolName of knownTools) {
1212
1329
  const tagRegex = new RegExp(`<${toolName}>([\\s\\S]*?)<\\/${toolName}>`, 'i');
@@ -1306,16 +1423,16 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1306
1423
  actionDesc = `Inspecting visible desktop UI`;
1307
1424
  else if (name === 'computer_diagnostic')
1308
1425
  actionDesc = `Running computer control diagnostics`;
1426
+ else if (name === 'delegate_subtask')
1427
+ actionDesc = `Delegating subtask to [${args.role || 'subagent'}]: ${String(args.task || '').slice(0, 40)}`;
1309
1428
  else if (name === 'computer')
1310
1429
  actionDesc = `Computer action: ${args.action || 'action'}`;
1311
1430
  else if (name === 'scratchpad')
1312
1431
  actionDesc = `Scratchpad (${args.action || 'read'})`;
1313
1432
  else if (name === 'task_completed')
1314
1433
  actionDesc = `Task finished`;
1315
- const badge = chalk.bgCyan.black.bold(` 🛠️ TOOL: ${name} `);
1316
- const descText = actionDesc ? chalk.yellow.bold(` → ${actionDesc}`) : '';
1317
- console.log(`\n${badge}${descText} ${chalk.dim(JSON.stringify(args))}`);
1318
- const isPaid = this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
1434
+ renderToolCall({ name, args, step: this.currentStep });
1435
+ const isPaid = this.mockMode || isProOrHigher(this.userTier);
1319
1436
  // 1. Scratchpad Feature Gating (Pro & Enterprise only)
1320
1437
  if (name === 'scratchpad') {
1321
1438
  if (!isPaid) {
@@ -1326,41 +1443,25 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1326
1443
  }
1327
1444
  const action = String(args.action || 'read').toLowerCase().trim();
1328
1445
  const content = String(args.content || '');
1329
- const ftDir = path.join(this.cwd, '.ft');
1330
- const scratchPath = path.join(ftDir, 'scratchpad.md');
1331
- fs.mkdirSync(ftDir, { recursive: true });
1332
- let currentScratch = '';
1333
- if (fs.existsSync(scratchPath)) {
1334
- try {
1335
- currentScratch = fs.readFileSync(scratchPath, 'utf-8');
1336
- }
1337
- catch { }
1338
- }
1339
1446
  if (action === 'write') {
1340
- fs.writeFileSync(scratchPath, content, 'utf-8');
1341
- try {
1342
- fs.writeFileSync(path.join(ftDir, 'scratchpad.cdp'), compress(Buffer.from(content, 'utf-8'), LEVEL_FAST));
1343
- }
1344
- catch { }
1447
+ this.agentWorkspace.setScratchpad(content);
1345
1448
  this.taskStateManager.addObservation(name, `Scratchpad updated (${content.length} chars, Codrop .cdp archived)`, true);
1346
1449
  return { result: `Scratchpad updated successfully:\n${content}` };
1347
1450
  }
1348
1451
  else if (action === 'append') {
1452
+ const currentScratch = this.agentWorkspace.getScratchpad();
1349
1453
  const updated = currentScratch ? `${currentScratch}\n\n---\n<!-- ${new Date().toISOString()} -->\n${content}` : content;
1350
- fs.writeFileSync(scratchPath, updated, 'utf-8');
1351
- try {
1352
- fs.writeFileSync(path.join(ftDir, 'scratchpad.cdp'), compress(Buffer.from(updated, 'utf-8'), LEVEL_FAST));
1353
- }
1354
- catch { }
1454
+ this.agentWorkspace.setScratchpad(updated);
1355
1455
  this.taskStateManager.addObservation(name, `Appended to scratchpad (${content.length} chars, Codrop .cdp archived)`, true);
1356
1456
  return { result: `Appended to scratchpad. Current contents:\n${updated}` };
1357
1457
  }
1358
1458
  else if (action === 'clear') {
1359
- fs.writeFileSync(scratchPath, '', 'utf-8');
1459
+ this.agentWorkspace.clearScratchpad();
1360
1460
  this.taskStateManager.addObservation(name, `Cleared scratchpad`, true);
1361
1461
  return { result: `Scratchpad cleared.` };
1362
1462
  }
1363
1463
  else {
1464
+ const currentScratch = this.agentWorkspace.getScratchpad();
1364
1465
  this.taskStateManager.addObservation(name, `Read scratchpad (${currentScratch.length} chars)`, true);
1365
1466
  return { result: currentScratch ? `Scratchpad Contents:\n${currentScratch}` : `Scratchpad is currently empty.` };
1366
1467
  }
@@ -1486,6 +1587,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1486
1587
  else if (action === 'diagnostic') {
1487
1588
  name = 'computer_diagnostic';
1488
1589
  }
1590
+ else if (action === 'wait') {
1591
+ name = 'computer_wait';
1592
+ }
1489
1593
  }
1490
1594
  // Tier Gating Enforcement
1491
1595
  if (!isPaid) {
@@ -1504,6 +1608,11 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1504
1608
  this.taskStateManager.addObservation(name, upgradeMsg, false);
1505
1609
  return { result: upgradeMsg };
1506
1610
  }
1611
+ if (name === 'delegate_subtask') {
1612
+ const upgradeMsg = `[PLAN RESTRICTION]: Autonomous Multi-Agent Swarm Delegation is exclusively available to Pro and Enterprise plan subscribers. Free tier developers can perform code editing, creating files/folders, and running commands. Upgrade to Pro at https://scout.frontterrain.com to unlock autonomous subagents.`;
1613
+ this.taskStateManager.addObservation(name, upgradeMsg, false);
1614
+ return { result: upgradeMsg };
1615
+ }
1507
1616
  }
1508
1617
  switch (name) {
1509
1618
  case 'read_file': {
@@ -1563,6 +1672,10 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1563
1672
  this.backups.push({ filePath: targetPath, originalContent: '__FT_WAS_NEW__' });
1564
1673
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
1565
1674
  }
1675
+ try {
1676
+ CheckpointManager.getInstance(this.cwd).createCheckpoint(`Step ${this.currentStep}: write_file ${targetPath}`, [targetPath], this.currentStep, this.taskStateManager.getCurrentState().taskId);
1677
+ }
1678
+ catch { }
1566
1679
  fs.writeFileSync(fullPath, content, 'utf-8');
1567
1680
  this.modifiedFiles.add(targetPath);
1568
1681
  const oldLineCount = origContent !== '__FT_WAS_NEW__' ? origContent.split('\n').length : 0;
@@ -1618,6 +1731,10 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1618
1731
  return { result: `Error editing "${targetPath}": ${replaceRes.error}` };
1619
1732
  }
1620
1733
  this.backups.push({ filePath: targetPath, originalContent: origContent });
1734
+ try {
1735
+ CheckpointManager.getInstance(this.cwd).createCheckpoint(`Step ${this.currentStep}: edit_file ${targetPath}`, [targetPath], this.currentStep, this.taskStateManager.getCurrentState().taskId);
1736
+ }
1737
+ catch { }
1621
1738
  fs.writeFileSync(fullPath, replaceRes.updatedContent, 'utf-8');
1622
1739
  this.modifiedFiles.add(targetPath);
1623
1740
  const targetLines = target.split('\n').length;
@@ -1877,11 +1994,7 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1877
1994
  }
1878
1995
  case 'speak_text': {
1879
1996
  const phrase = String(args.text || args.phrase || '').trim();
1880
- if (phrase) {
1881
- await speakText(phrase, { async: true });
1882
- return { result: `Spoke text out loud: "${phrase}"` };
1883
- }
1884
- return { result: 'No text provided to speak.' };
1997
+ return { result: `[TTS DISABLED]: Text-to-speech audio synthesis has been disabled. Text displayed: "${phrase}"` };
1885
1998
  }
1886
1999
  case 'browser_navigate': {
1887
2000
  const targetUrl = String(args.url || '').trim();
@@ -1953,35 +2066,6 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1953
2066
  this.taskStateManager.addObservation('browser_scroll', res.output, res.success);
1954
2067
  return { result: res.output };
1955
2068
  }
1956
- case 'scratchpad': {
1957
- const action = String(args.action || 'read').toLowerCase().trim();
1958
- const scratchpadPath = path.resolve(this.cwd, '.ft', 'scratchpad.md');
1959
- fs.mkdirSync(path.dirname(scratchpadPath), { recursive: true });
1960
- if (action === 'read') {
1961
- const content = fs.existsSync(scratchpadPath) ? fs.readFileSync(scratchpadPath, 'utf-8') : '(Scratchpad is empty)';
1962
- this.taskStateManager.addObservation('scratchpad', 'Read scratchpad content', true);
1963
- return { result: `Scratchpad Content:\n${content || '(empty)'}` };
1964
- }
1965
- else if (action === 'write') {
1966
- const newContent = String(args.content || '');
1967
- fs.writeFileSync(scratchpadPath, newContent, 'utf-8');
1968
- this.taskStateManager.addObservation('scratchpad', `Wrote scratchpad (${newContent.length} chars)`, true);
1969
- return { result: `Scratchpad updated (${newContent.length} chars).` };
1970
- }
1971
- else if (action === 'append') {
1972
- const appendContent = String(args.content || '');
1973
- fs.appendFileSync(scratchpadPath, `\n${appendContent}`, 'utf-8');
1974
- this.taskStateManager.addObservation('scratchpad', `Appended to scratchpad (${appendContent.length} chars)`, true);
1975
- return { result: `Appended to scratchpad (${appendContent.length} chars).` };
1976
- }
1977
- else if (action === 'clear') {
1978
- if (fs.existsSync(scratchpadPath))
1979
- fs.unlinkSync(scratchpadPath);
1980
- this.taskStateManager.addObservation('scratchpad', 'Cleared scratchpad', true);
1981
- return { result: 'Scratchpad cleared.' };
1982
- }
1983
- return { result: `Unknown scratchpad action: "${action}". Supported: read, write, append, clear.` };
1984
- }
1985
2069
  case 'computer_screenshot': {
1986
2070
  const res = await this.computerController.screenshot();
1987
2071
  if (res.path) {
@@ -2244,6 +2328,39 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
2244
2328
  };
2245
2329
  return { result: JSON.stringify(structuredRes, null, 2) };
2246
2330
  }
2331
+ case 'computer': {
2332
+ return {
2333
+ result: JSON.stringify({
2334
+ success: false,
2335
+ error: 'InvalidComputerAction',
2336
+ message: `Unknown computer action: "${args.action}". Supported actions: click, right_click, double_click, move, mouse_down, mouse_up, drag, scroll, screenshot, type, key_press, hotkey, open_app, close_app, switch_window, inspect_ui, diagnostic, wait.`
2337
+ }, null, 2)
2338
+ };
2339
+ }
2340
+ case 'delegate_subtask': {
2341
+ const role = (args.role || 'general');
2342
+ const task = String(args.task || '').trim();
2343
+ const context = args.context ? String(args.context) : undefined;
2344
+ if (!task) {
2345
+ return { result: JSON.stringify({ success: false, error: 'No task specified for subagent delegation.' }) };
2346
+ }
2347
+ const { SubagentManager } = await import('./subagents/subagentManager.js');
2348
+ const subagentMgr = SubagentManager.getInstance();
2349
+ const subRes = await subagentMgr.delegate({
2350
+ role,
2351
+ task,
2352
+ context,
2353
+ cwd: this.cwd,
2354
+ tier: this.userTier,
2355
+ });
2356
+ if (subRes.modifiedFiles && subRes.modifiedFiles.length > 0) {
2357
+ for (const f of subRes.modifiedFiles) {
2358
+ this.modifiedFiles.add(f);
2359
+ }
2360
+ }
2361
+ this.taskStateManager.addObservation('delegate_subtask', `[Subagent ${role.toUpperCase()} Result]: ${subRes.summary}`, subRes.success);
2362
+ return { result: JSON.stringify(subRes, null, 2) };
2363
+ }
2247
2364
  case 'task_completed': {
2248
2365
  const summaryStr = String(args.summary || 'Task completed successfully.');
2249
2366
  if (this.executionIntent?.targetDestination === 'Desktop' && this.executionIntent?.targetArtifactName) {