ft-scout 9.4.7 → 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 (80) 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 +11 -24
  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/init.d.ts.map +1 -1
  15. package/bin/src/commands/init.js +14 -17
  16. package/bin/src/commands/init.js.map +1 -1
  17. package/bin/src/commands/login.d.ts.map +1 -1
  18. package/bin/src/commands/login.js +2 -17
  19. package/bin/src/commands/login.js.map +1 -1
  20. package/bin/src/commands/plan.d.ts.map +1 -1
  21. package/bin/src/commands/plan.js +36 -6
  22. package/bin/src/commands/plan.js.map +1 -1
  23. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  24. package/bin/src/engine/agentEngine.js +415 -371
  25. package/bin/src/engine/agentEngine.js.map +1 -1
  26. package/bin/src/engine/computer/computerController.d.ts.map +1 -1
  27. package/bin/src/engine/computer/computerController.js +46 -0
  28. package/bin/src/engine/computer/computerController.js.map +1 -1
  29. package/bin/src/engine/llm.d.ts.map +1 -1
  30. package/bin/src/engine/llm.js +21 -51
  31. package/bin/src/engine/llm.js.map +1 -1
  32. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
  33. package/bin/src/engine/runtime/agentRuntime.js +305 -210
  34. package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
  35. package/bin/src/engine/toolRegistry.js +1 -1
  36. package/bin/src/engine/toolRegistry.js.map +1 -1
  37. package/bin/src/engine/voiceEngine.d.ts.map +1 -1
  38. package/bin/src/engine/voiceEngine.js +27 -238
  39. package/bin/src/engine/voiceEngine.js.map +1 -1
  40. package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
  41. package/bin/src/engine/workspace/agentWorkspace.js +22 -9
  42. package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
  43. package/bin/src/index.js +30 -9
  44. package/bin/src/index.js.map +1 -1
  45. package/bin/src/mcp/client.js +1 -1
  46. package/bin/src/server/server.d.ts.map +1 -1
  47. package/bin/src/server/server.js +35 -1
  48. package/bin/src/server/server.js.map +1 -1
  49. package/bin/src/utils/admin.d.ts.map +1 -1
  50. package/bin/src/utils/admin.js +63 -0
  51. package/bin/src/utils/admin.js.map +1 -1
  52. package/bin/src/utils/auth.d.ts.map +1 -1
  53. package/bin/src/utils/auth.js +10 -5
  54. package/bin/src/utils/auth.js.map +1 -1
  55. package/bin/src/utils/branding.js +1 -1
  56. package/bin/src/utils/claudeUI.d.ts.map +1 -0
  57. package/bin/src/utils/claudeUI.js +404 -0
  58. package/bin/src/utils/claudeUI.js.map +1 -0
  59. package/bin/src/utils/interruptHandler.d.ts.map +1 -0
  60. package/bin/src/utils/interruptHandler.js +121 -0
  61. package/bin/src/utils/interruptHandler.js.map +1 -0
  62. package/bin/src/utils/payment.d.ts.map +1 -1
  63. package/bin/src/utils/payment.js +1 -1
  64. package/bin/src/utils/payment.js.map +1 -1
  65. package/bin/src/utils/pricing.d.ts.map +1 -1
  66. package/bin/src/utils/pricing.js +106 -45
  67. package/bin/src/utils/pricing.js.map +1 -1
  68. package/bin/src/utils/qrBridge.d.ts.map +1 -1
  69. package/bin/src/utils/qrBridge.js +17 -17
  70. package/bin/src/utils/qrBridge.js.map +1 -1
  71. package/bin/src/utils/rules.d.ts.map +1 -0
  72. package/bin/src/utils/rules.js +105 -0
  73. package/bin/src/utils/rules.js.map +1 -0
  74. package/bin/src/utils/updateCheck.d.ts.map +1 -1
  75. package/bin/src/utils/updateCheck.js +12 -1
  76. package/bin/src/utils/updateCheck.js.map +1 -1
  77. package/firebase.json +1 -1
  78. package/package.json +1 -1
  79. package/web/app.js +108 -51
  80. package/web/styles.css +3 -3
@@ -24,6 +24,9 @@ import { getMCPManager } from '../mcp/index.js';
24
24
  import { writeTelemetry } from '../utils/qrBridge.js';
25
25
  import { compress, LEVEL_FAST } from 'codrop';
26
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';
27
30
  export { AgentRuntime, AgentWorkspace, inferExecutionIntent, resolveDesktopPath, SystemComputerController, MockComputerController };
28
31
  const execAsync = promisify(exec);
29
32
  export function robustSnippetReplace(origContent, target, replacement) {
@@ -407,6 +410,7 @@ export class AgentExecutionLoop {
407
410
  userTier;
408
411
  currentGoal = '';
409
412
  isAudioMode = false;
413
+ agentWorkspace;
410
414
  syncTelemetry(status, lastLog) {
411
415
  try {
412
416
  writeTelemetry({
@@ -428,10 +432,11 @@ export class AgentExecutionLoop {
428
432
  this.isAudioMode = Boolean(options?.isAudioMode);
429
433
  this.userTier = (options?.tier || loadAuthConfig().currentTier || 'free').toLowerCase();
430
434
  // Free tier is bounded to basic direct actions (max 5 steps). Full autonomous multi-step loops require Pro/Enterprise (unlimited steps).
431
- const isPaid = this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
435
+ const isPaid = this.mockMode || isProOrHigher(this.userTier);
432
436
  const requestedSteps = options?.maxSteps;
433
437
  this.maxSteps = isPaid ? (requestedSteps || Infinity) : Math.min(requestedSteps || 5, 5);
434
- this.taskStateManager = new TaskStateManager(options?.taskId, '', this.cwd);
438
+ this.agentWorkspace = new AgentWorkspace(options?.taskId, '', this.cwd);
439
+ this.taskStateManager = this.agentWorkspace.getStateManager();
435
440
  this.computerController = options?.computerController || (this.mockMode ? new MockComputerController() : new SystemComputerController(this.cwd));
436
441
  this.browserController = options?.browserController || new SemanticBrowserController(this.cwd);
437
442
  this.actionVerifier = new ActionVerifier(this.cwd);
@@ -443,7 +448,7 @@ export class AgentExecutionLoop {
443
448
  return this.userTier;
444
449
  }
445
450
  isPaidTier() {
446
- return this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
451
+ return this.mockMode || isProOrHigher(this.userTier);
447
452
  }
448
453
  getExecutionIntent() {
449
454
  return this.executionIntent;
@@ -638,6 +643,8 @@ PLAN TIER & CAPABILITIES: DEVELOPER FREE (BASIC DEVELOPER OPS ONLY)
638
643
  const mcpCapabilitiesSection = mcpRouting.capabilitiesSummary ? `\n${mcpRouting.capabilitiesSummary}\n` : '';
639
644
  const recentSessionsMemory = getRecentSessionsSummary(1);
640
645
  const sessionHistorySection = recentSessionsMemory ? `\n${recentSessionsMemory}\n` : '';
646
+ const workspaceRules = loadWorkspaceRules(this.cwd);
647
+ const rulesSection = workspaceRules.found ? `\n${workspaceRules.formattedPrompt}\n` : '';
641
648
  return `You are Scout Agent, an elite AI Principal Software Engineer & Autonomous Computer-Use Operator (developed by FrontTerrain).
642
649
 
643
650
  Working Directory: "${this.cwd}"
@@ -647,6 +654,7 @@ ${tierDirective}
647
654
  ${intentInfo}
648
655
  ${mcpCapabilitiesSection}
649
656
  ${sessionHistorySection}
657
+ ${rulesSection}
650
658
  Workspace Overview (first 100 files):
651
659
  ${filesList.join('\n')}
652
660
 
@@ -738,6 +746,10 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
738
746
  };
739
747
  }
740
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
+ }
741
753
  // Robust Target File Discovery: Only run file discovery if the goal has explicit code editing/modification intent
742
754
  const simpleAppCheck = checkSimpleAppAction(userGoal);
743
755
  const lowerGoal = userGoal.toLowerCase().trim();
@@ -849,7 +861,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
849
861
  content: promptContent,
850
862
  });
851
863
  }
852
- const isPaid = this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
864
+ const isPaid = this.mockMode || isProOrHigher(this.userTier);
853
865
  if (!isPaid && Number.isFinite(this.maxSteps)) {
854
866
  const estTotalSecs = Math.round(this.maxSteps * 12);
855
867
  const estTimeText = estTotalSecs >= 60 ? `~${(estTotalSecs / 60).toFixed(1)}m` : `~${estTotalSecs}s`;
@@ -861,365 +873,436 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
861
873
  let step = 0;
862
874
  let finalSummary = '';
863
875
  const stepDurations = [];
864
- while (step < this.maxSteps) {
865
- if (this.isCancelled) {
866
- return {
867
- success: false,
868
- summary: `Task cancelled: ${this.cancelReason || 'User abort'}`,
869
- };
870
- }
871
- step++;
872
- this.currentStep = step;
873
- this.taskStateManager.startStep(step);
874
- const stepStartTime = Date.now();
875
- const stepSpinner = spinner();
876
- if (isPaid && !Number.isFinite(this.maxSteps)) {
877
- stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step})`));
878
- }
879
- else {
880
- const avgStepMs = stepDurations.length > 0
881
- ? stepDurations.reduce((a, b) => a + b, 0) / stepDurations.length
882
- : 12000;
883
- const remainingSteps = (this.maxSteps - step + 1);
884
- const estSecsLeft = Math.max(5, Math.round((remainingSteps * avgStepMs) / 1000));
885
- const estLeftStr = estSecsLeft >= 60
886
- ? `${Math.floor(estSecsLeft / 60)}m ${estSecsLeft % 60}s`
887
- : `${estSecsLeft}s`;
888
- stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step}/${this.maxSteps} • Est. completion: ~${estLeftStr} left)`));
889
- }
890
- try {
891
- let response;
892
- const mcpRouting = getMCPManager().routeTools(userGoal);
893
- const mcpToolDefs = mcpRouting.tools.map((mt) => {
894
- let desc = mt.description;
895
- if (mt.originalName === 'execute_blender_code') {
896
- 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.';
897
- }
898
- else if (mt.originalName === 'generate_3d') {
899
- 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');
900
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) {
901
916
  return {
902
- name: mt.originalName,
903
- description: `[MCP: ${mt.serverName}] ${desc.length > 160 ? desc.slice(0, 157) + '...' : desc}`,
904
- parameters: {
905
- type: 'object',
906
- properties: mt.inputSchema.properties || {},
907
- required: mt.inputSchema.required || [],
908
- },
909
- source: 'mcp',
910
- mcpServerId: mt.serverId,
911
- mcpServerName: mt.serverName,
917
+ success: false,
918
+ summary: `Task cancelled: ${this.cancelReason || 'User abort'}`,
912
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,
913
935
  });
914
- const goalLower = (userGoal || '').toLowerCase();
915
- const needsBrowser = goalLower.includes('browser') || goalLower.includes('website') || goalLower.includes('webpage') || goalLower.includes('http') || goalLower.includes('url') || goalLower.includes('scrape') || goalLower.includes('chrome');
916
- const needsComputer = goalLower.includes('computer') || goalLower.includes('desktop') || goalLower.includes('mouse') || goalLower.includes('coordinate') || goalLower.includes('drag') || goalLower.includes('double click');
917
- let baseTools = this.toolRegistry.getToolsForTier(this.userTier);
918
- if (mcpToolDefs.length > 0 && !needsBrowser && !needsComputer) {
919
- 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']);
920
- 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})`));
921
939
  }
922
- else if (!needsBrowser) {
923
- 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)`));
924
950
  }
925
- const activeLoopTools = [...baseTools, ...mcpToolDefs];
926
951
  try {
927
- response = await callOpenAIWithRetry(async (model) => {
928
- return await openai.chat.completions.create({
929
- model,
930
- messages: sanitizeMessages(this.historyMessages),
931
- tools: activeLoopTools.map((t) => ({ type: 'function', function: t })),
932
- tool_choice: 'auto',
933
- temperature: 0.1,
934
- presence_penalty: 0.1,
935
- frequency_penalty: 0.1,
936
- });
937
- });
938
- }
939
- catch (err) {
940
- const errStr = String(err?.message || err?.error || err || '').toLowerCase();
941
- if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported') || errStr.includes('reasoning')) {
942
- // Model doesn't support native function calling inject tool-call formatting hint
943
- // so extractJsonToolCall can parse the response as a structured tool invocation
944
- const availableToolNames = activeLoopTools.map((t) => t.name).join(', ');
945
- const toolHintMsg = {
946
- role: 'user',
947
- 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:
948
- \`\`\`json
949
- { "tool": "tool_name", "args": { ... } }
950
- \`\`\`
951
- Available tools: ${availableToolNames}
952
- 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,
953
973
  };
954
- 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 {
955
988
  response = await callOpenAIWithRetry(async (model) => {
956
989
  return await openai.chat.completions.create({
957
990
  model,
958
- messages: sanitizeMessages(fallbackMessages),
991
+ messages: sanitizeMessages(this.historyMessages),
992
+ tools: activeLoopTools.map((t) => ({ type: 'function', function: t })),
993
+ tool_choice: 'auto',
959
994
  temperature: 0.1,
960
995
  presence_penalty: 0.1,
961
996
  frequency_penalty: 0.1,
962
997
  });
963
998
  });
964
999
  }
965
- else {
966
- throw err;
967
- }
968
- }
969
- const choice = response.choices[0];
970
- if (!choice) {
971
- stepSpinner.stop(chalk.yellow('No response from Scout. Retrying step...'));
972
- continue;
973
- }
974
- const msg = sanitizeMessage(choice.message);
975
- const tokensThisTurn = response?.usage?.total_tokens || 0;
976
- this.totalTokensConsumed += tokensThisTurn;
977
- this.syncTelemetry('running', `scout: [step ${step}] generated reasoning (${tokensThisTurn} tokens)`);
978
- this.historyMessages.push(msg);
979
- // Render assistant's thought reasoning text if provided on this turn
980
- const textContent = msg.content || '';
981
- if (textContent.trim()) {
982
- safeNote(renderMarkdown(textContent), `Scout Thought (Step ${step})`);
983
- if (this.isAudioMode) {
984
- const speechText = cleanTextForSpeech(textContent);
985
- if (speechText) {
986
- 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;
987
1028
  }
988
1029
  }
989
- }
990
- // Check native tool calls
991
- if (msg.tool_calls && msg.tool_calls.length > 0) {
992
- stepSpinner.stop(chalk.green(`Step ${step}: Scout issued ${msg.tool_calls.length} tool call(s).`));
993
- for (const tc of msg.tool_calls) {
994
- if (tc.type === 'function' && tc.function) {
995
- const fnName = tc.function.name;
996
- let args = {};
997
- try {
998
- args = JSON.parse(tc.function.arguments || '{}');
999
- }
1000
- catch { }
1001
- if (this.isAudioMode && fnName !== 'task_completed') {
1002
- let actionPhrase = '';
1003
- if (fnName === 'run_command')
1004
- actionPhrase = `Running command ${args.command ? args.command.slice(0, 40) : ''}`;
1005
- else if (fnName === 'read_file')
1006
- actionPhrase = `Reading ${path.basename(args.filePath || '')}`;
1007
- else if (fnName === 'write_file')
1008
- actionPhrase = `Creating ${path.basename(args.filePath || '')}`;
1009
- else if (fnName === 'edit_file')
1010
- actionPhrase = `Editing ${path.basename(args.filePath || '')}`;
1011
- else if (fnName.startsWith('mcp_') || fnName.includes('github'))
1012
- actionPhrase = `Querying ${fnName}`;
1013
- if (actionPhrase)
1014
- speakText(actionPhrase, { async: true });
1015
- }
1016
- const toolResult = await this.dispatchToolCall(fnName, args);
1017
- this.totalToolCalls++;
1018
- this.syncTelemetry('running', `scout: [step ${step}] executed tool ${fnName}`);
1019
- const toolResStr = String(toolResult.result || '');
1020
- let cappedResult = toolResStr;
1021
- 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 = {};
1022
1052
  try {
1023
- const origBytes = Buffer.byteLength(toolResStr, 'utf-8');
1024
- const cdp = compress(Buffer.from(toolResStr, 'utf-8'), LEVEL_FAST);
1025
- const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
1026
- cappedResult = toolResStr.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
1027
- this.syncTelemetry('running', `scout: [codrop] compressed ${fnName} output (${ratio}x ratio)`);
1053
+ args = JSON.parse(tc.function.arguments || '{}');
1028
1054
  }
1029
- catch {
1030
- cappedResult = toolResStr.slice(0, 800) + '\n... [output truncated for token efficiency]';
1031
- }
1032
- }
1033
- this.historyMessages.push({
1034
- role: 'user',
1035
- tool_call_id: tc.id,
1036
- content: `Tool Execution Result (${fnName}):\n${cappedResult}`,
1037
- });
1038
- this.handleToolConsecutiveTracking(fnName, args, step);
1039
- // Automatic immediate completion for simple app actions (open/close/switch window)
1040
- if (simpleAppCheck.isSimple && (fnName === 'computer_open_app' || fnName === 'computer_close_app' || fnName === 'computer_switch_window')) {
1041
- let callSucceeded = false;
1042
- try {
1043
- const parsed = JSON.parse(toolResult.result);
1044
- 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
+ }
1045
1081
  }
1046
- catch {
1047
- 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
+ }
1048
1121
  }
1049
- if (callSucceeded) {
1050
- const appName = simpleAppCheck.appName || 'application';
1051
- const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
1052
- finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
1053
- 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
+ });
1054
1145
  saveAgentSession({
1055
1146
  goal: userGoal,
1056
1147
  summary: finalSummary,
1057
- modifiedFiles: [],
1148
+ modifiedFiles: Array.from(this.modifiedFiles),
1058
1149
  });
1059
- this.syncTelemetry('idle', `scout: completed task successfully (${this.modifiedFiles.size} files modified)`);
1060
1150
  return { success: true, summary: finalSummary };
1061
1151
  }
1062
1152
  }
1063
- if (fnName === 'task_completed') {
1064
- finalSummary = args.summary || toolResult.result;
1065
- if (this.modifiedFiles.size > 0) {
1066
- const filesList = getDirectoryFiles(this.cwd);
1067
- const healRes = await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, path.basename(this.cwd), filesList, { maxRetries: 3 });
1068
- if (healRes.verifiedFiles.length > 0) {
1069
- safeNote(chalk.green(` Self-Healing Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build clean!`), ' Code Verification Clean');
1070
- }
1071
- if (healRes.remainingErrors.length > 0) {
1072
- safeNote(chalk.yellow(`️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '️ Verification Warning');
1073
- }
1074
- }
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');
1075
1203
  saveAgentSession({
1076
1204
  goal: userGoal,
1077
1205
  summary: finalSummary,
1078
- modifiedFiles: Array.from(this.modifiedFiles),
1206
+ modifiedFiles: [],
1079
1207
  });
1080
1208
  return { success: true, summary: finalSummary };
1081
1209
  }
1082
1210
  }
1083
- }
1084
- continue;
1085
- }
1086
- // Check text response or fallback JSON tool call
1087
- stepSpinner.stop(chalk.blue(`Step ${step} thinking complete.`));
1088
- const parsedJsonTool = this.extractJsonToolCall(textContent);
1089
- if (parsedJsonTool) {
1090
- const toolResult = await this.dispatchToolCall(parsedJsonTool.tool, parsedJsonTool.args);
1091
- const jsonResStr = String(toolResult.result || '');
1092
- let cappedJsonResult = jsonResStr;
1093
- if (jsonResStr.length > 800) {
1094
- try {
1095
- const origBytes = Buffer.byteLength(jsonResStr, 'utf-8');
1096
- const cdp = compress(Buffer.from(jsonResStr, 'utf-8'), LEVEL_FAST);
1097
- const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
1098
- cappedJsonResult = jsonResStr.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
1099
- }
1100
- catch {
1101
- cappedJsonResult = jsonResStr.slice(0, 800) + '\n... [output truncated for token efficiency]';
1102
- }
1103
- }
1104
- this.historyMessages.push({
1105
- role: 'user',
1106
- content: `Tool Execution Result (${parsedJsonTool.tool}):\n${cappedJsonResult}`,
1107
- });
1108
- this.handleToolConsecutiveTracking(parsedJsonTool.tool, parsedJsonTool.args, step);
1109
- // Automatic immediate completion for simple app actions (open/close/switch window)
1110
- if (simpleAppCheck.isSimple && (parsedJsonTool.tool === 'computer_open_app' || parsedJsonTool.tool === 'computer_close_app' || parsedJsonTool.tool === 'computer_switch_window')) {
1111
- let callSucceeded = false;
1112
- try {
1113
- const parsed = JSON.parse(toolResult.result);
1114
- callSucceeded = parsed.success !== false;
1115
- }
1116
- catch {
1117
- callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
1118
- }
1119
- if (callSucceeded) {
1120
- const appName = simpleAppCheck.appName || 'application';
1121
- const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
1122
- finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
1123
- safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
1211
+ if (parsedJsonTool.tool === 'task_completed') {
1212
+ finalSummary = parsedJsonTool.args?.summary || toolResult.result;
1124
1213
  saveAgentSession({
1125
1214
  goal: userGoal,
1126
1215
  summary: finalSummary,
1127
- modifiedFiles: [],
1216
+ modifiedFiles: Array.from(this.modifiedFiles),
1128
1217
  });
1129
1218
  return { success: true, summary: finalSummary };
1130
1219
  }
1220
+ continue;
1131
1221
  }
1132
- if (parsedJsonTool.tool === 'task_completed') {
1133
- finalSummary = parsedJsonTool.args?.summary || toolResult.result;
1134
- saveAgentSession({
1135
- goal: userGoal,
1136
- summary: finalSummary,
1137
- modifiedFiles: Array.from(this.modifiedFiles),
1138
- });
1139
- 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
+ }
1140
1262
  }
1141
- 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);
1142
1269
  }
1143
- if (textContent.trim()) {
1144
- let cleanedThought = textContent
1145
- .replace(/[\u0600-\u06FF\u0750-\u077F\uAC00-\uD7AF\u3040-\u30FF\u4E00-\u9FFF\u0D80-\u0DFF]+/g, '')
1146
- .trim();
1147
- if (!cleanedThought || cleanedThought.length < 5) {
1148
- cleanedThought = 'Analyzing codebase files and executing next tool operation...';
1149
- }
1150
- safeNote(renderMarkdown(cleanedThought), ` Scout Agent Thought (Step ${step})`);
1151
- const lowerText = textContent.toLowerCase().trim();
1152
- const isExplicitCompletion = lowerText.includes('task is complete') ||
1153
- lowerText.includes('task complete') ||
1154
- lowerText.includes('goal completed') ||
1155
- lowerText.includes('goal is completed') ||
1156
- lowerText.includes('all tasks completed') ||
1157
- lowerText.includes('i have completed') ||
1158
- lowerText.includes('no further changes needed') ||
1159
- lowerText.includes('the fix is complete') ||
1160
- lowerText.includes('has been created') ||
1161
- lowerText.includes('successfully created') ||
1162
- lowerText.includes('created the file') ||
1163
- lowerText.includes('file created') ||
1164
- lowerText.includes('implementation complete') ||
1165
- lowerText.includes('setup is complete') ||
1166
- lowerText.includes('setup complete') ||
1167
- lowerText.includes('instructions have been executed') ||
1168
- lowerText.includes('work is complete');
1169
- 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);
1170
- 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);
1171
- 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()) ||
1172
- !/\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()));
1173
- const isAnsweringInformational = !isIntermediateThought && isInformationalGoal && textContent.trim().length > 20 && !lowerText.includes('error occurred') && !lowerText.includes('cannot complete');
1174
- const isSummaryAfterModifications = !isIntermediateThought && this.modifiedFiles.size > 0 && !lowerText.includes('?') && textContent.trim().length > 30;
1175
- if (isExplicitCompletion || isAnsweringInformational || isSummaryAfterModifications) {
1176
- saveAgentSession({
1177
- goal: userGoal,
1178
- summary: textContent,
1179
- modifiedFiles: Array.from(this.modifiedFiles),
1180
- });
1181
- 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
+ };
1182
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
+ });
1183
1289
  }
1184
- // If assistant responded with text without calling tools, prompt it to execute tools to complete the goal
1185
- this.historyMessages.push({
1186
- role: 'user',
1187
- 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.',
1188
- });
1189
- stepDurations.push(Date.now() - stepStartTime);
1190
- }
1191
- catch (err) {
1192
- stepDurations.push(Date.now() - stepStartTime);
1193
- if (isQuotaExceededError(err)) {
1194
- stepSpinner.stop(chalk.red(`Oops! it\'s not you, it\'s us`));
1195
- safeNote(`${chalk.bold.red(`Something went wrong in (Step ${step}), please try again in a moment`)}\n\n` +
1196
- `${chalk.yellow('The configured AI provider has reached its API usage limit or rate cap.')}\n` +
1197
- `${chalk.dim('This is separate from your Scout credit balance shown by `scout quota`.')}\n\n` +
1198
- `${chalk.bold.cyan(' Please come back and try again in a few hours (or check back later today).')}\n\n` +
1199
- `${chalk.dim('Scout Agent session has ended gracefully to protect remaining workflow.')}`, '️ API Quota Limit Reached');
1200
- return {
1201
- success: false,
1202
- summary: 'Something went wrong, please try again in a moment.',
1203
- };
1290
+ // Step limit check: Free tier only. Pro and Enterprise have unlimited steps.
1291
+ if (!isPaid && step >= this.maxSteps) {
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');
1293
+ break;
1204
1294
  }
1205
- stepSpinner.stop(chalk.red(`Step ${step} execution error: ${err?.message || String(err)}`));
1206
- this.historyMessages.push({
1207
- role: 'user',
1208
- content: `Error in previous turn: ${err?.message || String(err)}. Please try alternative steps or call tools.`,
1209
- });
1210
- }
1211
- // Step limit check: Free tier only. Pro and Enterprise have unlimited steps.
1212
- if (!isPaid && step >= this.maxSteps) {
1213
- 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');
1214
- break;
1215
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();
1216
1305
  }
1217
- return {
1218
- success: false,
1219
- summary: isPaid
1220
- ? `Task ended after ${step} iteration steps. Modified files: ${Array.from(this.modifiedFiles).join(', ')}`
1221
- : `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
1222
- };
1223
1306
  }
1224
1307
  extractJsonToolCall(content) {
1225
1308
  if (!content)
@@ -1348,10 +1431,8 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1348
1431
  actionDesc = `Scratchpad (${args.action || 'read'})`;
1349
1432
  else if (name === 'task_completed')
1350
1433
  actionDesc = `Task finished`;
1351
- const badge = chalk.bgCyan.black.bold(` 🛠️ TOOL: ${name} `);
1352
- const descText = actionDesc ? chalk.yellow.bold(` → ${actionDesc}`) : '';
1353
- console.log(`\n${badge}${descText} ${chalk.dim(JSON.stringify(args))}`);
1354
- 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);
1355
1436
  // 1. Scratchpad Feature Gating (Pro & Enterprise only)
1356
1437
  if (name === 'scratchpad') {
1357
1438
  if (!isPaid) {
@@ -1362,41 +1443,25 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1362
1443
  }
1363
1444
  const action = String(args.action || 'read').toLowerCase().trim();
1364
1445
  const content = String(args.content || '');
1365
- const ftDir = path.join(this.cwd, '.ft');
1366
- const scratchPath = path.join(ftDir, 'scratchpad.md');
1367
- fs.mkdirSync(ftDir, { recursive: true });
1368
- let currentScratch = '';
1369
- if (fs.existsSync(scratchPath)) {
1370
- try {
1371
- currentScratch = fs.readFileSync(scratchPath, 'utf-8');
1372
- }
1373
- catch { }
1374
- }
1375
1446
  if (action === 'write') {
1376
- fs.writeFileSync(scratchPath, content, 'utf-8');
1377
- try {
1378
- fs.writeFileSync(path.join(ftDir, 'scratchpad.cdp'), compress(Buffer.from(content, 'utf-8'), LEVEL_FAST));
1379
- }
1380
- catch { }
1447
+ this.agentWorkspace.setScratchpad(content);
1381
1448
  this.taskStateManager.addObservation(name, `Scratchpad updated (${content.length} chars, Codrop .cdp archived)`, true);
1382
1449
  return { result: `Scratchpad updated successfully:\n${content}` };
1383
1450
  }
1384
1451
  else if (action === 'append') {
1452
+ const currentScratch = this.agentWorkspace.getScratchpad();
1385
1453
  const updated = currentScratch ? `${currentScratch}\n\n---\n<!-- ${new Date().toISOString()} -->\n${content}` : content;
1386
- fs.writeFileSync(scratchPath, updated, 'utf-8');
1387
- try {
1388
- fs.writeFileSync(path.join(ftDir, 'scratchpad.cdp'), compress(Buffer.from(updated, 'utf-8'), LEVEL_FAST));
1389
- }
1390
- catch { }
1454
+ this.agentWorkspace.setScratchpad(updated);
1391
1455
  this.taskStateManager.addObservation(name, `Appended to scratchpad (${content.length} chars, Codrop .cdp archived)`, true);
1392
1456
  return { result: `Appended to scratchpad. Current contents:\n${updated}` };
1393
1457
  }
1394
1458
  else if (action === 'clear') {
1395
- fs.writeFileSync(scratchPath, '', 'utf-8');
1459
+ this.agentWorkspace.clearScratchpad();
1396
1460
  this.taskStateManager.addObservation(name, `Cleared scratchpad`, true);
1397
1461
  return { result: `Scratchpad cleared.` };
1398
1462
  }
1399
1463
  else {
1464
+ const currentScratch = this.agentWorkspace.getScratchpad();
1400
1465
  this.taskStateManager.addObservation(name, `Read scratchpad (${currentScratch.length} chars)`, true);
1401
1466
  return { result: currentScratch ? `Scratchpad Contents:\n${currentScratch}` : `Scratchpad is currently empty.` };
1402
1467
  }
@@ -1522,6 +1587,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1522
1587
  else if (action === 'diagnostic') {
1523
1588
  name = 'computer_diagnostic';
1524
1589
  }
1590
+ else if (action === 'wait') {
1591
+ name = 'computer_wait';
1592
+ }
1525
1593
  }
1526
1594
  // Tier Gating Enforcement
1527
1595
  if (!isPaid) {
@@ -1926,11 +1994,7 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1926
1994
  }
1927
1995
  case 'speak_text': {
1928
1996
  const phrase = String(args.text || args.phrase || '').trim();
1929
- if (phrase) {
1930
- await speakText(phrase, { async: true });
1931
- return { result: `Spoke text out loud: "${phrase}"` };
1932
- }
1933
- return { result: 'No text provided to speak.' };
1997
+ return { result: `[TTS DISABLED]: Text-to-speech audio synthesis has been disabled. Text displayed: "${phrase}"` };
1934
1998
  }
1935
1999
  case 'browser_navigate': {
1936
2000
  const targetUrl = String(args.url || '').trim();
@@ -2002,35 +2066,6 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
2002
2066
  this.taskStateManager.addObservation('browser_scroll', res.output, res.success);
2003
2067
  return { result: res.output };
2004
2068
  }
2005
- case 'scratchpad': {
2006
- const action = String(args.action || 'read').toLowerCase().trim();
2007
- const scratchpadPath = path.resolve(this.cwd, '.ft', 'scratchpad.md');
2008
- fs.mkdirSync(path.dirname(scratchpadPath), { recursive: true });
2009
- if (action === 'read') {
2010
- const content = fs.existsSync(scratchpadPath) ? fs.readFileSync(scratchpadPath, 'utf-8') : '(Scratchpad is empty)';
2011
- this.taskStateManager.addObservation('scratchpad', 'Read scratchpad content', true);
2012
- return { result: `Scratchpad Content:\n${content || '(empty)'}` };
2013
- }
2014
- else if (action === 'write') {
2015
- const newContent = String(args.content || '');
2016
- fs.writeFileSync(scratchpadPath, newContent, 'utf-8');
2017
- this.taskStateManager.addObservation('scratchpad', `Wrote scratchpad (${newContent.length} chars)`, true);
2018
- return { result: `Scratchpad updated (${newContent.length} chars).` };
2019
- }
2020
- else if (action === 'append') {
2021
- const appendContent = String(args.content || '');
2022
- fs.appendFileSync(scratchpadPath, `\n${appendContent}`, 'utf-8');
2023
- this.taskStateManager.addObservation('scratchpad', `Appended to scratchpad (${appendContent.length} chars)`, true);
2024
- return { result: `Appended to scratchpad (${appendContent.length} chars).` };
2025
- }
2026
- else if (action === 'clear') {
2027
- if (fs.existsSync(scratchpadPath))
2028
- fs.unlinkSync(scratchpadPath);
2029
- this.taskStateManager.addObservation('scratchpad', 'Cleared scratchpad', true);
2030
- return { result: 'Scratchpad cleared.' };
2031
- }
2032
- return { result: `Unknown scratchpad action: "${action}". Supported: read, write, append, clear.` };
2033
- }
2034
2069
  case 'computer_screenshot': {
2035
2070
  const res = await this.computerController.screenshot();
2036
2071
  if (res.path) {
@@ -2293,6 +2328,15 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
2293
2328
  };
2294
2329
  return { result: JSON.stringify(structuredRes, null, 2) };
2295
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
+ }
2296
2340
  case 'delegate_subtask': {
2297
2341
  const role = (args.role || 'general');
2298
2342
  const task = String(args.task || '').trim();