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
@@ -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 === 'pro' || this.userTier === 'enterprise';
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
- while (step < this.maxSteps) {
231
- if (this.isCancelled) {
232
- return {
233
- success: false,
234
- summary: `Task cancelled: ${this.cancelReason || 'User abort'}`,
235
- taskId,
236
- stepsTaken: step,
237
- modifiedFiles: Array.from(this.modifiedFiles),
238
- status: 'cancelled',
239
- };
240
- }
241
- step++;
242
- this.currentStep = step;
243
- this.workspace.startStep(step);
244
- // Context Compaction check: If history is long, compact middle turns using workspace state
245
- if (this.historyMessages.length > 10) {
246
- this.historyMessages = this.workspace.compactHistoryMessages(this.historyMessages, 6);
247
- }
248
- const isPaid = this.isProOrEnterprise();
249
- const stepSpinner = spinner();
250
- const stepText = isPaid && !Number.isFinite(this.maxSteps)
251
- ? `Scout Operating (Step ${step})`
252
- : `Scout Operating (Step ${step}/${this.maxSteps})`;
253
- stepSpinner.start(chalk.cyan(stepText));
254
- try {
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
- name: mt.originalName,
267
- description: `[MCP: ${mt.serverName}] ${desc.length > 250 ? desc.slice(0, 247) + '...' : desc}`,
268
- parameters: {
269
- type: 'object',
270
- properties: mt.inputSchema.properties || {},
271
- required: mt.inputSchema.required || [],
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
- else if (!needsBrowser) {
287
- baseTools = baseTools.filter((t) => !t.name.startsWith('browser_'));
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 tierTools = [...baseTools, ...mcpToolDefs];
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 = await callOpenAIWithRetry(async (model) => {
292
- return await openai.chat.completions.create({
293
- model,
294
- messages: sanitizeMessages(this.historyMessages),
295
- tools: tierTools.map((t) => ({ type: 'function', function: t })),
296
- tool_choice: 'auto',
297
- temperature: 0.1,
298
- });
299
- });
300
- }
301
- catch (err) {
302
- const errStr = String(err?.message || err || '').toLowerCase();
303
- if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported')) {
304
- const fallbackMsg = {
305
- role: 'user',
306
- 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(', ')}`,
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([...this.historyMessages, fallbackMsg]),
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
- else {
317
- throw err;
318
- }
319
- }
320
- const choice = response.choices[0];
321
- if (!choice) {
322
- stepSpinner.stop(chalk.yellow('No response from model. Retrying step...'));
323
- continue;
324
- }
325
- const msg = sanitizeMessage(choice.message);
326
- this.historyMessages.push(msg);
327
- if (msg.content && msg.content.trim()) {
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
- catch {
342
- toolCallsToExecute.push({ name: tc.function.name, args: {}, id: tc.id });
343
- }
364
+ });
365
+ }
366
+ else {
367
+ throw err;
344
368
  }
345
369
  }
346
- }
347
- else if (msg.content) {
348
- const parsed = this.extractJsonToolCall(msg.content);
349
- if (parsed) {
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
- stepSpinner.stop(chalk.green(`Step ${step}: Executing ${toolCallsToExecute.length} action(s).`));
354
- for (const tc of toolCallsToExecute) {
355
- if (this.isCancelled)
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
- if (tc.id) {
373
- // Native tool call: use 'tool' role with matching tool_call_id
374
- this.historyMessages.push({
375
- role: 'tool',
376
- tool_call_id: tc.id,
377
- content: cappedContent,
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
- // JSON-extracted tool call (no tc.id): use 'user' role
382
- this.historyMessages.push({
383
- role: 'user',
384
- content: `Tool Execution Result (${tc.name}):\n${cappedContent}`,
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
- // If the tool returned screenshot data, append a vision message so the model can see the screen
388
- if (toolResult.screenshotBase64) {
389
- this.pruneOldScreenshots();
390
- this.historyMessages.push({
391
- role: 'user',
392
- content: [
393
- { type: 'text', text: `[Screenshot after ${tc.name}]: ${toolResult.screenshotSummary || 'Current screen state.'}` },
394
- { type: 'image_url', image_url: { url: `data:image/png;base64,${toolResult.screenshotBase64}`, detail: 'low' } },
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
- // Automatic immediate completion for simple app actions (open/close/switch window)
399
- const simpleAppCheck = checkSimpleAppAction(userGoal);
400
- if (simpleAppCheck.isSimple && (tc.name === 'computer_open_app' || tc.name === 'computer_close_app' || tc.name === 'computer_switch_window')) {
401
- let callSucceeded = false;
402
- try {
403
- const parsed = typeof toolResult.result === 'string' ? JSON.parse(toolResult.result) : toolResult.result;
404
- callSucceeded = parsed?.success !== false;
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
- catch {
407
- const str = String(toolResult.result || '').toLowerCase();
408
- callSucceeded = !str.includes('failed') && !str.includes('error');
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
- if (callSucceeded) {
411
- const appName = simpleAppCheck.appName || 'application';
412
- const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
413
- finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
414
- safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
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
- if (tc.name === 'task_completed') {
433
- finalSummary = tc.args.summary || toolResult.result;
434
- if (this.modifiedFiles.size > 0) {
435
- const filesList = getDirectoryFiles(this.cwd);
436
- await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, path.basename(this.cwd), filesList, { maxRetries: 3 });
437
- }
438
- this.workspace.setStatus('completed');
439
- this.workspace.completeStep(step, finalSummary);
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
- catch (err) {
457
- stepSpinner.stop(chalk.red(`Step ${step} error: ${err?.message || String(err)}`));
458
- this.workspace.recordError(err?.message || String(err), 'runtime', undefined, false);
459
- this.historyMessages.push({
460
- role: 'user',
461
- content: `System Error encountered on step ${step}: ${err?.message || String(err)}. Diagnose and adapt action.`,
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
- speakText(txt, { async: true });
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: {