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.
- package/.firebase/hosting.d2Vi.cache +8 -8
- package/.scoutrules +13 -0
- package/AUTONOMOUS_FEATURES.md +15 -10
- package/README.md +588 -482
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +11 -24
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/authCommand.d.ts.map +1 -1
- package/bin/src/commands/authCommand.js +4 -10
- package/bin/src/commands/authCommand.js.map +1 -1
- package/bin/src/commands/benchmark.d.ts.map +1 -1
- package/bin/src/commands/benchmark.js +0 -5
- package/bin/src/commands/benchmark.js.map +1 -1
- package/bin/src/commands/init.d.ts.map +1 -1
- package/bin/src/commands/init.js +14 -17
- package/bin/src/commands/init.js.map +1 -1
- package/bin/src/commands/login.d.ts.map +1 -1
- package/bin/src/commands/login.js +2 -17
- package/bin/src/commands/login.js.map +1 -1
- package/bin/src/commands/plan.d.ts.map +1 -1
- package/bin/src/commands/plan.js +36 -6
- package/bin/src/commands/plan.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +415 -371
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/computer/computerController.d.ts.map +1 -1
- package/bin/src/engine/computer/computerController.js +46 -0
- package/bin/src/engine/computer/computerController.js.map +1 -1
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +21 -51
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.js +305 -210
- package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
- package/bin/src/engine/toolRegistry.js +1 -1
- package/bin/src/engine/toolRegistry.js.map +1 -1
- package/bin/src/engine/voiceEngine.d.ts.map +1 -1
- package/bin/src/engine/voiceEngine.js +27 -238
- package/bin/src/engine/voiceEngine.js.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.js +22 -9
- package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
- package/bin/src/index.js +30 -9
- package/bin/src/index.js.map +1 -1
- package/bin/src/mcp/client.js +1 -1
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +35 -1
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/admin.d.ts.map +1 -1
- package/bin/src/utils/admin.js +63 -0
- package/bin/src/utils/admin.js.map +1 -1
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js +10 -5
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/claudeUI.d.ts.map +1 -0
- package/bin/src/utils/claudeUI.js +404 -0
- package/bin/src/utils/claudeUI.js.map +1 -0
- package/bin/src/utils/interruptHandler.d.ts.map +1 -0
- package/bin/src/utils/interruptHandler.js +121 -0
- package/bin/src/utils/interruptHandler.js.map +1 -0
- package/bin/src/utils/payment.d.ts.map +1 -1
- package/bin/src/utils/payment.js +1 -1
- package/bin/src/utils/payment.js.map +1 -1
- package/bin/src/utils/pricing.d.ts.map +1 -1
- package/bin/src/utils/pricing.js +106 -45
- package/bin/src/utils/pricing.js.map +1 -1
- package/bin/src/utils/qrBridge.d.ts.map +1 -1
- package/bin/src/utils/qrBridge.js +17 -17
- package/bin/src/utils/qrBridge.js.map +1 -1
- package/bin/src/utils/rules.d.ts.map +1 -0
- package/bin/src/utils/rules.js +105 -0
- package/bin/src/utils/rules.js.map +1 -0
- package/bin/src/utils/updateCheck.d.ts.map +1 -1
- package/bin/src/utils/updateCheck.js +12 -1
- package/bin/src/utils/updateCheck.js.map +1 -1
- package/firebase.json +1 -1
- package/package.json +1 -1
- package/web/app.js +108 -51
- 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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
903
|
-
|
|
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
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
923
|
-
|
|
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
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
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 (
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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:
|
|
1206
|
+
modifiedFiles: [],
|
|
1079
1207
|
});
|
|
1080
1208
|
return { success: true, summary: finalSummary };
|
|
1081
1209
|
}
|
|
1082
1210
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
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 (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
.
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
//
|
|
1185
|
-
this.
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
-
|
|
1352
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|