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
|
@@ -20,6 +20,9 @@ import { robustSnippetReplace, buildDirectoryTree } from '../agentEngine.js';
|
|
|
20
20
|
import { loadAuthConfig, isProOrHigher } from '../../utils/auth.js';
|
|
21
21
|
import { getMCPManager } from '../../mcp/index.js';
|
|
22
22
|
import { compress, LEVEL_FAST } from 'codrop';
|
|
23
|
+
import { renderStepHeader, renderThoughtBox, renderToolCall, renderToolResult, renderTaskCompletedCard, } from '../../utils/claudeUI.js';
|
|
24
|
+
import { loadWorkspaceRules } from '../../utils/rules.js';
|
|
25
|
+
import { InterruptManager } from '../../utils/interruptHandler.js';
|
|
23
26
|
export class AgentRuntime {
|
|
24
27
|
cwd;
|
|
25
28
|
autoApprove;
|
|
@@ -55,7 +58,7 @@ export class AgentRuntime {
|
|
|
55
58
|
this.language = options?.language;
|
|
56
59
|
this.userTier = (options?.tier || (options?.mockMode ? 'pro' : loadAuthConfig().currentTier) || 'free').toLowerCase();
|
|
57
60
|
// Free tier is bounded to basic direct actions (max 5 steps). Full autonomous multi-step loops require Pro/Enterprise (unlimited steps).
|
|
58
|
-
const isPaid = this.mockMode || this.userTier
|
|
61
|
+
const isPaid = this.mockMode || isProOrHigher(this.userTier);
|
|
59
62
|
const requestedSteps = options?.maxSteps;
|
|
60
63
|
this.maxSteps = isPaid ? (requestedSteps || Infinity) : Math.min(requestedSteps || 5, 5);
|
|
61
64
|
this.workspace = new AgentWorkspace(options?.taskId, '', this.cwd);
|
|
@@ -136,6 +139,8 @@ export class AgentRuntime {
|
|
|
136
139
|
buildSystemPrompt() {
|
|
137
140
|
const filesList = getDirectoryFiles(this.cwd).slice(0, 100);
|
|
138
141
|
const workspaceState = this.workspace.compactWorkspaceForPrompt();
|
|
142
|
+
const workspaceRules = loadWorkspaceRules(this.cwd);
|
|
143
|
+
const rulesSection = workspaceRules.found ? `\n${workspaceRules.formattedPrompt}\n` : '';
|
|
139
144
|
const tierNotice = !this.isProOrEnterprise()
|
|
140
145
|
? `\nTIER RESTRICTION (FREE DEVELOPER TIER):
|
|
141
146
|
- You are operating under the Free Developer Tier.
|
|
@@ -156,7 +161,7 @@ Current Year: ${new Date().getFullYear()}
|
|
|
156
161
|
${tierNotice}
|
|
157
162
|
|
|
158
163
|
${workspaceState}
|
|
159
|
-
|
|
164
|
+
${rulesSection}
|
|
160
165
|
Available Workspace Files (sample):
|
|
161
166
|
${filesList.join('\n')}
|
|
162
167
|
|
|
@@ -227,253 +232,333 @@ Action Rules:
|
|
|
227
232
|
this.historyMessages.push({ role: 'user', content: userGoal });
|
|
228
233
|
let step = 0;
|
|
229
234
|
let finalSummary = '';
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let response;
|
|
256
|
-
const mcpRouting = getMCPManager().routeTools(userGoal);
|
|
257
|
-
const mcpToolDefs = mcpRouting.tools.map((mt) => {
|
|
258
|
-
let desc = mt.description;
|
|
259
|
-
if (mt.originalName === 'execute_blender_code') {
|
|
260
|
-
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.';
|
|
261
|
-
}
|
|
262
|
-
else if (mt.originalName === 'generate_3d') {
|
|
263
|
-
desc = 'Cloud AI text-to-3d generator (requires external API key). If keys are not set, use execute_blender_code to model directly.';
|
|
235
|
+
const interruptManager = new InterruptManager({
|
|
236
|
+
getStepInfo: () => ({
|
|
237
|
+
step: this.currentStep || 1,
|
|
238
|
+
maxSteps: this.maxSteps,
|
|
239
|
+
goal: userGoal,
|
|
240
|
+
modifiedFilesCount: this.modifiedFiles.size,
|
|
241
|
+
}),
|
|
242
|
+
onPause: () => {
|
|
243
|
+
this.workspace.setStatus('paused');
|
|
244
|
+
},
|
|
245
|
+
onResume: () => {
|
|
246
|
+
this.workspace.setStatus('running');
|
|
247
|
+
},
|
|
248
|
+
onFeedback: (feedback) => {
|
|
249
|
+
this.historyMessages.push({
|
|
250
|
+
role: 'user',
|
|
251
|
+
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.`,
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
onRevert: () => {
|
|
255
|
+
if (this.backups.length === 0)
|
|
256
|
+
return false;
|
|
257
|
+
for (const b of this.backups) {
|
|
258
|
+
try {
|
|
259
|
+
fs.writeFileSync(b.filePath, b.originalContent, 'utf-8');
|
|
264
260
|
}
|
|
261
|
+
catch { }
|
|
262
|
+
}
|
|
263
|
+
this.modifiedFiles.clear();
|
|
264
|
+
return true;
|
|
265
|
+
},
|
|
266
|
+
onAbort: (reason) => {
|
|
267
|
+
this.isCancelled = true;
|
|
268
|
+
this.cancelReason = reason;
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
interruptManager.attach();
|
|
272
|
+
try {
|
|
273
|
+
while (step < this.maxSteps) {
|
|
274
|
+
if (this.isCancelled) {
|
|
265
275
|
return {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
},
|
|
273
|
-
source: 'mcp',
|
|
274
|
-
mcpServerId: mt.serverId,
|
|
275
|
-
mcpServerName: mt.serverName,
|
|
276
|
+
success: false,
|
|
277
|
+
summary: `Task cancelled: ${this.cancelReason || 'User abort'}`,
|
|
278
|
+
taskId,
|
|
279
|
+
stepsTaken: step,
|
|
280
|
+
modifiedFiles: Array.from(this.modifiedFiles),
|
|
281
|
+
status: 'cancelled',
|
|
276
282
|
};
|
|
277
|
-
});
|
|
278
|
-
const goalLower = (userGoal || '').toLowerCase();
|
|
279
|
-
const needsBrowser = goalLower.includes('browser') || goalLower.includes('website') || goalLower.includes('webpage') || goalLower.includes('http') || goalLower.includes('url') || goalLower.includes('scrape') || goalLower.includes('chrome');
|
|
280
|
-
const needsComputer = goalLower.includes('computer') || goalLower.includes('desktop') || goalLower.includes('mouse') || goalLower.includes('coordinate') || goalLower.includes('drag') || goalLower.includes('double click');
|
|
281
|
-
let baseTools = this.toolRegistry.getToolsForTier(this.userTier);
|
|
282
|
-
if (mcpToolDefs.length > 0 && !needsBrowser && !needsComputer) {
|
|
283
|
-
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']);
|
|
284
|
-
baseTools = baseTools.filter((t) => ESSENTIAL_BASE_TOOLS.has(t.name));
|
|
285
283
|
}
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
step++;
|
|
285
|
+
this.currentStep = step;
|
|
286
|
+
this.workspace.startStep(step);
|
|
287
|
+
// Context Compaction check: If history is long, compact middle turns using workspace state
|
|
288
|
+
if (this.historyMessages.length > 10) {
|
|
289
|
+
this.historyMessages = this.workspace.compactHistoryMessages(this.historyMessages, 6);
|
|
288
290
|
}
|
|
289
|
-
const
|
|
291
|
+
const isPaid = this.isProOrEnterprise();
|
|
292
|
+
renderStepHeader({
|
|
293
|
+
step,
|
|
294
|
+
maxSteps: this.maxSteps,
|
|
295
|
+
startTimeMs: this.workspace.getData().createdAt ? new Date(this.workspace.getData().createdAt).getTime() : Date.now(),
|
|
296
|
+
tokensConsumed: 0,
|
|
297
|
+
userTier: this.userTier,
|
|
298
|
+
});
|
|
299
|
+
const stepSpinner = spinner();
|
|
300
|
+
const stepText = isPaid && !Number.isFinite(this.maxSteps)
|
|
301
|
+
? `Scout Operating (Step ${step})`
|
|
302
|
+
: `Scout Operating (Step ${step}/${this.maxSteps})`;
|
|
303
|
+
stepSpinner.start(chalk.cyan(stepText));
|
|
290
304
|
try {
|
|
291
|
-
response
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
let response;
|
|
306
|
+
const mcpRouting = getMCPManager().routeTools(userGoal);
|
|
307
|
+
const mcpToolDefs = mcpRouting.tools.map((mt) => {
|
|
308
|
+
let desc = mt.description;
|
|
309
|
+
if (mt.originalName === 'execute_blender_code') {
|
|
310
|
+
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.';
|
|
311
|
+
}
|
|
312
|
+
else if (mt.originalName === 'generate_3d') {
|
|
313
|
+
desc = 'Cloud AI text-to-3d generator (requires external API key). If keys are not set, use execute_blender_code to model directly.';
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
name: mt.originalName,
|
|
317
|
+
description: `[MCP: ${mt.serverName}] ${desc.length > 250 ? desc.slice(0, 247) + '...' : desc}`,
|
|
318
|
+
parameters: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
properties: mt.inputSchema.properties || {},
|
|
321
|
+
required: mt.inputSchema.required || [],
|
|
322
|
+
},
|
|
323
|
+
source: 'mcp',
|
|
324
|
+
mcpServerId: mt.serverId,
|
|
325
|
+
mcpServerName: mt.serverName,
|
|
307
326
|
};
|
|
327
|
+
});
|
|
328
|
+
const goalLower = (userGoal || '').toLowerCase();
|
|
329
|
+
const needsBrowser = goalLower.includes('browser') || goalLower.includes('website') || goalLower.includes('webpage') || goalLower.includes('http') || goalLower.includes('url') || goalLower.includes('scrape') || goalLower.includes('chrome');
|
|
330
|
+
const needsComputer = goalLower.includes('computer') || goalLower.includes('desktop') || goalLower.includes('mouse') || goalLower.includes('coordinate') || goalLower.includes('drag') || goalLower.includes('double click');
|
|
331
|
+
let baseTools = this.toolRegistry.getToolsForTier(this.userTier);
|
|
332
|
+
if (mcpToolDefs.length > 0 && !needsBrowser && !needsComputer) {
|
|
333
|
+
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']);
|
|
334
|
+
baseTools = baseTools.filter((t) => ESSENTIAL_BASE_TOOLS.has(t.name));
|
|
335
|
+
}
|
|
336
|
+
else if (!needsBrowser) {
|
|
337
|
+
baseTools = baseTools.filter((t) => !t.name.startsWith('browser_'));
|
|
338
|
+
}
|
|
339
|
+
const tierTools = [...baseTools, ...mcpToolDefs];
|
|
340
|
+
try {
|
|
308
341
|
response = await callOpenAIWithRetry(async (model) => {
|
|
309
342
|
return await openai.chat.completions.create({
|
|
310
343
|
model,
|
|
311
|
-
messages: sanitizeMessages(
|
|
344
|
+
messages: sanitizeMessages(this.historyMessages),
|
|
345
|
+
tools: tierTools.map((t) => ({ type: 'function', function: t })),
|
|
346
|
+
tool_choice: 'auto',
|
|
312
347
|
temperature: 0.1,
|
|
313
348
|
});
|
|
314
349
|
});
|
|
315
350
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
safeNote(renderMarkdown(msg.content), `Scout Step ${step}`);
|
|
329
|
-
}
|
|
330
|
-
let toolCallsToExecute = [];
|
|
331
|
-
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
332
|
-
for (const tc of msg.tool_calls) {
|
|
333
|
-
if (tc.type === 'function' && tc.function) {
|
|
334
|
-
try {
|
|
335
|
-
toolCallsToExecute.push({
|
|
336
|
-
name: tc.function.name,
|
|
337
|
-
args: JSON.parse(tc.function.arguments || '{}'),
|
|
338
|
-
id: tc.id,
|
|
351
|
+
catch (err) {
|
|
352
|
+
const errStr = String(err?.message || err || '').toLowerCase();
|
|
353
|
+
if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported')) {
|
|
354
|
+
const fallbackMsg = {
|
|
355
|
+
role: 'user',
|
|
356
|
+
content: `Format your tool call as a JSON code block:\n\`\`\`json\n{ "tool": "tool_name", "args": { ... } }\n\`\`\`\nAvailable tools: ${tierTools.map((t) => t.name).join(', ')}`,
|
|
357
|
+
};
|
|
358
|
+
response = await callOpenAIWithRetry(async (model) => {
|
|
359
|
+
return await openai.chat.completions.create({
|
|
360
|
+
model,
|
|
361
|
+
messages: sanitizeMessages([...this.historyMessages, fallbackMsg]),
|
|
362
|
+
temperature: 0.1,
|
|
339
363
|
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
throw err;
|
|
344
368
|
}
|
|
345
369
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
toolCallsToExecute.push({ name: parsed.tool, args: parsed.args });
|
|
370
|
+
const choice = response.choices[0];
|
|
371
|
+
if (!choice) {
|
|
372
|
+
stepSpinner.stop(chalk.yellow('No response from model. Retrying step...'));
|
|
373
|
+
continue;
|
|
351
374
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
break;
|
|
357
|
-
const toolResult = await this.dispatchTool(tc.name, tc.args);
|
|
358
|
-
// Build the tool result message with correct role
|
|
359
|
-
const rawContent = typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result);
|
|
360
|
-
let cappedContent = rawContent;
|
|
361
|
-
if (rawContent.length > 800) {
|
|
362
|
-
try {
|
|
363
|
-
const origBytes = Buffer.byteLength(rawContent, 'utf-8');
|
|
364
|
-
const cdp = compress(Buffer.from(rawContent, 'utf-8'), LEVEL_FAST);
|
|
365
|
-
const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
|
|
366
|
-
cappedContent = rawContent.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
cappedContent = rawContent.slice(0, 800) + '\n... [output truncated for token efficiency]';
|
|
370
|
-
}
|
|
375
|
+
const msg = sanitizeMessage(choice.message);
|
|
376
|
+
this.historyMessages.push(msg);
|
|
377
|
+
if (msg.content && msg.content.trim()) {
|
|
378
|
+
renderThoughtBox(msg.content, step);
|
|
371
379
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
380
|
+
let toolCallsToExecute = [];
|
|
381
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
382
|
+
for (const tc of msg.tool_calls) {
|
|
383
|
+
if (tc.type === 'function' && tc.function) {
|
|
384
|
+
try {
|
|
385
|
+
toolCallsToExecute.push({
|
|
386
|
+
name: tc.function.name,
|
|
387
|
+
args: JSON.parse(tc.function.arguments || '{}'),
|
|
388
|
+
id: tc.id,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
toolCallsToExecute.push({ name: tc.function.name, args: {}, id: tc.id });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
379
396
|
}
|
|
380
|
-
else {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
});
|
|
397
|
+
else if (msg.content) {
|
|
398
|
+
const parsed = this.extractJsonToolCall(msg.content);
|
|
399
|
+
if (parsed) {
|
|
400
|
+
toolCallsToExecute.push({ name: parsed.tool, args: parsed.args });
|
|
401
|
+
}
|
|
386
402
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
this.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
403
|
+
stepSpinner.stop(chalk.green(`Step ${step}: Executing ${toolCallsToExecute.length} action(s).`));
|
|
404
|
+
for (const tc of toolCallsToExecute) {
|
|
405
|
+
if (this.isCancelled)
|
|
406
|
+
break;
|
|
407
|
+
renderToolCall({ name: tc.name, args: tc.args, step });
|
|
408
|
+
const toolStartTime = Date.now();
|
|
409
|
+
const toolResult = await this.dispatchTool(tc.name, tc.args);
|
|
410
|
+
const durationMs = Date.now() - toolStartTime;
|
|
411
|
+
renderToolResult({
|
|
412
|
+
name: tc.name,
|
|
413
|
+
args: tc.args,
|
|
414
|
+
result: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
|
|
415
|
+
durationMs,
|
|
416
|
+
success: !String(toolResult.result).includes('[PLAN RESTRICTION') && !String(toolResult.result).includes('[ACTION BLOCKED'),
|
|
396
417
|
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
418
|
+
// Build the tool result message with correct role
|
|
419
|
+
const rawContent = typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result);
|
|
420
|
+
let cappedContent = rawContent;
|
|
421
|
+
if (rawContent.length > 800) {
|
|
422
|
+
try {
|
|
423
|
+
const origBytes = Buffer.byteLength(rawContent, 'utf-8');
|
|
424
|
+
const cdp = compress(Buffer.from(rawContent, 'utf-8'), LEVEL_FAST);
|
|
425
|
+
const ratio = (origBytes / Math.max(1, cdp.length)).toFixed(1);
|
|
426
|
+
cappedContent = rawContent.slice(0, 800) + `\n... [Codrop: ${origBytes}B compressed to ${cdp.length}B (${ratio}x ratio) for LLM context]`;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
cappedContent = rawContent.slice(0, 800) + '\n... [output truncated for token efficiency]';
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (tc.id) {
|
|
433
|
+
// Native tool call: use 'tool' role with matching tool_call_id
|
|
434
|
+
this.historyMessages.push({
|
|
435
|
+
role: 'tool',
|
|
436
|
+
tool_call_id: tc.id,
|
|
437
|
+
content: cappedContent,
|
|
438
|
+
});
|
|
405
439
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
440
|
+
else {
|
|
441
|
+
// JSON-extracted tool call (no tc.id): use 'user' role
|
|
442
|
+
this.historyMessages.push({
|
|
443
|
+
role: 'user',
|
|
444
|
+
content: `Tool Execution Result (${tc.name}):\n${cappedContent}`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
// If the tool returned screenshot data, append a vision message so the model can see the screen
|
|
448
|
+
if (toolResult.screenshotBase64) {
|
|
449
|
+
this.pruneOldScreenshots();
|
|
450
|
+
this.historyMessages.push({
|
|
451
|
+
role: 'user',
|
|
452
|
+
content: [
|
|
453
|
+
{ type: 'text', text: `[Screenshot after ${tc.name}]: ${toolResult.screenshotSummary || 'Current screen state.'}` },
|
|
454
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${toolResult.screenshotBase64}`, detail: 'low' } },
|
|
455
|
+
],
|
|
456
|
+
});
|
|
409
457
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
458
|
+
// Automatic immediate completion for simple app actions (open/close/switch window)
|
|
459
|
+
const simpleAppCheck = checkSimpleAppAction(userGoal);
|
|
460
|
+
if (simpleAppCheck.isSimple && (tc.name === 'computer_open_app' || tc.name === 'computer_close_app' || tc.name === 'computer_switch_window')) {
|
|
461
|
+
let callSucceeded = false;
|
|
462
|
+
try {
|
|
463
|
+
const parsed = typeof toolResult.result === 'string' ? JSON.parse(toolResult.result) : toolResult.result;
|
|
464
|
+
callSucceeded = parsed?.success !== false;
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
const str = String(toolResult.result || '').toLowerCase();
|
|
468
|
+
callSucceeded = !str.includes('failed') && !str.includes('error');
|
|
469
|
+
}
|
|
470
|
+
if (callSucceeded) {
|
|
471
|
+
const appName = simpleAppCheck.appName || 'application';
|
|
472
|
+
const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
|
|
473
|
+
finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
|
|
474
|
+
renderTaskCompletedCard({
|
|
475
|
+
objective: userGoal,
|
|
476
|
+
summary: finalSummary,
|
|
477
|
+
modifiedFiles: [],
|
|
478
|
+
steps: step,
|
|
479
|
+
maxSteps: this.maxSteps,
|
|
480
|
+
totalTokens: 0,
|
|
481
|
+
totalToolCalls: step,
|
|
482
|
+
elapsedSecs: Math.round((Date.now() - (this.workspace.getData().createdAt ? new Date(this.workspace.getData().createdAt).getTime() : Date.now())) / 1000),
|
|
483
|
+
userTier: this.userTier,
|
|
484
|
+
});
|
|
485
|
+
this.workspace.setStatus('completed');
|
|
486
|
+
this.workspace.completeStep(step, finalSummary);
|
|
487
|
+
saveAgentSession({
|
|
488
|
+
goal: userGoal,
|
|
489
|
+
summary: finalSummary,
|
|
490
|
+
modifiedFiles: [],
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
success: true,
|
|
494
|
+
summary: finalSummary,
|
|
495
|
+
taskId,
|
|
496
|
+
stepsTaken: step,
|
|
497
|
+
modifiedFiles: [],
|
|
498
|
+
status: 'completed',
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (tc.name === 'task_completed') {
|
|
503
|
+
finalSummary = tc.args.summary || toolResult.result;
|
|
504
|
+
if (this.modifiedFiles.size > 0) {
|
|
505
|
+
const filesList = getDirectoryFiles(this.cwd);
|
|
506
|
+
await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, path.basename(this.cwd), filesList, { maxRetries: 3 });
|
|
507
|
+
}
|
|
508
|
+
renderTaskCompletedCard({
|
|
509
|
+
objective: userGoal,
|
|
510
|
+
summary: finalSummary,
|
|
511
|
+
modifiedFiles: Array.from(this.modifiedFiles),
|
|
512
|
+
steps: step,
|
|
513
|
+
maxSteps: this.maxSteps,
|
|
514
|
+
totalTokens: 0,
|
|
515
|
+
totalToolCalls: step,
|
|
516
|
+
elapsedSecs: Math.round((Date.now() - (this.workspace.getData().createdAt ? new Date(this.workspace.getData().createdAt).getTime() : Date.now())) / 1000),
|
|
517
|
+
userTier: this.userTier,
|
|
518
|
+
});
|
|
415
519
|
this.workspace.setStatus('completed');
|
|
416
520
|
this.workspace.completeStep(step, finalSummary);
|
|
417
521
|
saveAgentSession({
|
|
418
522
|
goal: userGoal,
|
|
419
523
|
summary: finalSummary,
|
|
420
|
-
modifiedFiles:
|
|
524
|
+
modifiedFiles: Array.from(this.modifiedFiles),
|
|
421
525
|
});
|
|
422
526
|
return {
|
|
423
527
|
success: true,
|
|
424
528
|
summary: finalSummary,
|
|
425
529
|
taskId,
|
|
426
530
|
stepsTaken: step,
|
|
427
|
-
modifiedFiles:
|
|
531
|
+
modifiedFiles: Array.from(this.modifiedFiles),
|
|
428
532
|
status: 'completed',
|
|
429
533
|
};
|
|
430
534
|
}
|
|
431
535
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
saveAgentSession({
|
|
441
|
-
goal: userGoal,
|
|
442
|
-
summary: finalSummary,
|
|
443
|
-
modifiedFiles: Array.from(this.modifiedFiles),
|
|
444
|
-
});
|
|
445
|
-
return {
|
|
446
|
-
success: true,
|
|
447
|
-
summary: finalSummary,
|
|
448
|
-
taskId,
|
|
449
|
-
stepsTaken: step,
|
|
450
|
-
modifiedFiles: Array.from(this.modifiedFiles),
|
|
451
|
-
status: 'completed',
|
|
452
|
-
};
|
|
453
|
-
}
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
stepSpinner.stop(chalk.red(`Step ${step} error: ${err?.message || String(err)}`));
|
|
539
|
+
this.workspace.recordError(err?.message || String(err), 'runtime', undefined, false);
|
|
540
|
+
this.historyMessages.push({
|
|
541
|
+
role: 'user',
|
|
542
|
+
content: `System Error encountered on step ${step}: ${err?.message || String(err)}. Diagnose and adapt action.`,
|
|
543
|
+
});
|
|
454
544
|
}
|
|
455
545
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
546
|
+
this.workspace.setStatus('failed');
|
|
547
|
+
const isPaid = this.isProOrEnterprise();
|
|
548
|
+
return {
|
|
549
|
+
success: false,
|
|
550
|
+
summary: isPaid
|
|
551
|
+
? `Task ended after ${step} iteration steps. Modified files: ${Array.from(this.modifiedFiles).join(', ')}`
|
|
552
|
+
: `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
|
|
553
|
+
taskId,
|
|
554
|
+
stepsTaken: step,
|
|
555
|
+
modifiedFiles: Array.from(this.modifiedFiles),
|
|
556
|
+
status: 'failed',
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
interruptManager.detach();
|
|
464
561
|
}
|
|
465
|
-
this.workspace.setStatus('failed');
|
|
466
|
-
const isPaid = this.isProOrEnterprise();
|
|
467
|
-
return {
|
|
468
|
-
success: false,
|
|
469
|
-
summary: isPaid
|
|
470
|
-
? `Task ended after ${step} iteration steps. Modified files: ${Array.from(this.modifiedFiles).join(', ')}`
|
|
471
|
-
: `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
|
|
472
|
-
taskId,
|
|
473
|
-
stepsTaken: step,
|
|
474
|
-
modifiedFiles: Array.from(this.modifiedFiles),
|
|
475
|
-
status: 'failed',
|
|
476
|
-
};
|
|
477
562
|
}
|
|
478
563
|
async executeTool(name, args) {
|
|
479
564
|
return await this.dispatchTool(name, args);
|
|
@@ -608,6 +693,9 @@ Action Rules:
|
|
|
608
693
|
else if (action === 'diagnostic') {
|
|
609
694
|
name = 'computer_diagnostic';
|
|
610
695
|
}
|
|
696
|
+
else if (action === 'wait') {
|
|
697
|
+
name = 'computer_wait';
|
|
698
|
+
}
|
|
611
699
|
}
|
|
612
700
|
switch (name) {
|
|
613
701
|
case 'computer_screenshot': {
|
|
@@ -879,6 +967,15 @@ Action Rules:
|
|
|
879
967
|
const diag = await this.computerController.runDiagnostic();
|
|
880
968
|
return { result: JSON.stringify(diag, null, 2) };
|
|
881
969
|
}
|
|
970
|
+
case 'computer': {
|
|
971
|
+
return {
|
|
972
|
+
result: JSON.stringify({
|
|
973
|
+
success: false,
|
|
974
|
+
error: 'InvalidComputerAction',
|
|
975
|
+
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.`
|
|
976
|
+
}, null, 2)
|
|
977
|
+
};
|
|
978
|
+
}
|
|
882
979
|
default:
|
|
883
980
|
return { result: `Unknown computer tool: "${name}"` };
|
|
884
981
|
}
|
|
@@ -1234,8 +1331,7 @@ Action Rules:
|
|
|
1234
1331
|
}
|
|
1235
1332
|
case 'speak_text': {
|
|
1236
1333
|
const txt = String(args.text || '');
|
|
1237
|
-
|
|
1238
|
-
return { result: `Spoke: "${txt}"` };
|
|
1334
|
+
return { result: `[TTS DISABLED]: Text-to-speech audio synthesis has been disabled. Text displayed: "${txt}"` };
|
|
1239
1335
|
}
|
|
1240
1336
|
case 'task_completed': {
|
|
1241
1337
|
const summary = String(args.summary || 'Completed');
|
|
@@ -1254,7 +1350,6 @@ Action Rules:
|
|
|
1254
1350
|
};
|
|
1255
1351
|
}
|
|
1256
1352
|
}
|
|
1257
|
-
speakText(summary, { async: true });
|
|
1258
1353
|
return { result: summary };
|
|
1259
1354
|
}
|
|
1260
1355
|
default: {
|