Files
OpenQode/lib/iq-engine.mjs

335 lines
11 KiB
JavaScript

/**
* Intelligent Execution Engine (IQ Exchange Core)
*
* This module is the "brain" that:
* 1. Takes natural language requests
* 2. Uses AI to generate commands
* 3. Executes commands and captures results
* 4. Detects errors and sends them back to AI for correction
* 5. Retries until task is complete or max retries reached
*
* Credit: Inspired by AmberSahdev/Open-Interface & browser-use/browser-use
*/
import { spawn, execSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Absolute paths - critical for reliable execution
const PATHS = {
playwrightBridge: path.join(__dirname, '..', 'bin', 'playwright-bridge.js'),
inputPs1: path.join(__dirname, '..', 'bin', 'input.ps1'),
screenshotDir: path.join(__dirname, '..', 'screenshots')
};
// Ensure screenshot dir exists
if (!fs.existsSync(PATHS.screenshotDir)) {
fs.mkdirSync(PATHS.screenshotDir, { recursive: true });
}
/**
* Execute a single command and return result
*/
export async function executeCommand(commandString, timeout = 30000) {
return new Promise((resolve) => {
const startTime = Date.now();
let proc;
let stdout = '';
let stderr = '';
try {
// Parse command type and execute appropriately
if (commandString.includes('playwright-bridge') || commandString.startsWith('node')) {
// Playwright command
const parts = parseCommandParts(commandString);
proc = spawn('node', parts.args, {
cwd: path.dirname(PATHS.playwrightBridge),
shell: true
});
} else if (commandString.includes('powershell') || commandString.includes('input.ps1')) {
// PowerShell command - ensure proper format
const scriptMatch = commandString.match(/(?:-File\s+)?["']?([^"'\s]+input\.ps1)["']?\s+(.+)/i);
if (scriptMatch) {
const scriptPath = PATHS.inputPs1;
const cmdArgs = scriptMatch[2];
proc = spawn('powershell', [
'-NoProfile', '-ExecutionPolicy', 'Bypass',
'-File', scriptPath,
...cmdArgs.split(/\s+/)
], { shell: true });
} else {
// Try to parse as simple command
proc = spawn('powershell', [commandString], { shell: true });
}
} else {
// Generic shell command
proc = spawn('cmd', ['/c', commandString], { shell: true });
}
proc.stdout.on('data', (data) => { stdout += data.toString(); });
proc.stderr.on('data', (data) => { stderr += data.toString(); });
proc.on('close', (code) => {
const elapsed = Date.now() - startTime;
resolve({
success: code === 0,
exitCode: code,
stdout: stdout.trim(),
stderr: stderr.trim(),
elapsed,
command: commandString
});
});
proc.on('error', (err) => {
resolve({
success: false,
error: err.message,
stdout: stdout.trim(),
stderr: stderr.trim(),
elapsed: Date.now() - startTime,
command: commandString
});
});
// Timeout
setTimeout(() => {
proc.kill();
resolve({
success: false,
error: 'TIMEOUT',
stdout: stdout.trim(),
stderr: stderr.trim(),
elapsed: timeout,
command: commandString
});
}, timeout);
} catch (error) {
resolve({
success: false,
error: error.message,
command: commandString
});
}
});
}
/**
* Parse command string into parts
*/
function parseCommandParts(commandString) {
const matches = commandString.match(/"[^"]+"|'[^']+'|\S+/g) || [];
const clean = matches.map(m => m.replace(/^["']|["']$/g, ''));
return { args: clean.slice(1), full: clean };
}
/**
* Extract code blocks from AI response
*/
export function extractCodeBlocks(response) {
const blocks = [];
const regex = /```(?:bash|powershell|shell|cmd)?\s*([\s\S]*?)```/gi;
let match;
while ((match = regex.exec(response)) !== null) {
const code = match[1].trim();
const lines = code.split('\n').filter(l => l.trim() && !l.startsWith('#'));
blocks.push(...lines);
}
return blocks;
}
/**
* Build context for AI to understand current state and errors
*/
export function buildCorrectionContext(originalRequest, attemptHistory, currentError) {
let context = `
╔══════════════════════════════════════════════════════════════════════════════════╗
║ IQ EXCHANGE - SELF-HEALING EXECUTION ENGINE ║
╚══════════════════════════════════════════════════════════════════════════════════╝
ORIGINAL USER REQUEST: "${originalRequest}"
SYSTEM PATHS (use these EXACT paths):
- Playwright: node "${PATHS.playwrightBridge}"
- PowerShell: powershell -NoProfile -ExecutionPolicy Bypass -File "${PATHS.inputPs1}"
`;
if (attemptHistory.length > 0) {
context += `\nPREVIOUS ATTEMPTS:\n`;
attemptHistory.forEach((attempt, i) => {
context += `
═════ ATTEMPT ${i + 1} ═════
Command: ${attempt.command}
Result: ${attempt.success ? 'SUCCESS' : 'FAILED'}
Output: ${attempt.stdout || attempt.stderr || attempt.error || 'No output'}
`;
});
}
if (currentError) {
context += `
⚠️ CURRENT ERROR TO FIX:
${currentError}
ANALYZE the error and provide CORRECTED commands.
Common fixes:
- Wrong path → Use the EXACT paths shown above
- Element not found → Use different selector or wait for element
- Timeout → Increase wait time or check if page loaded
- Permission denied → Check file/folder permissions
`;
}
context += `
═══════════════════════════════════════════════════════════════════════════════════
INSTRUCTIONS:
1. Analyze what went wrong
2. Provide CORRECTED commands that will work
3. Each command in its own code block
4. If task is complete, say "TASK_COMPLETE"
AVAILABLE COMMANDS:
Browser (Playwright): navigate, fill, click, press, type, content, elements, screenshot
Desktop (PowerShell): open, uiclick, type, key, mouse, click, drag, apps, focus, screenshot, ocr
Respond with corrected commands in code blocks:
`;
return context;
}
/**
* The main intelligent execution loop
*/
export class IntelligentExecutor {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 5;
this.sendToAI = options.sendToAI; // Must be provided - sends text to AI, receives response
this.onExecuting = options.onExecuting || (() => { });
this.onResult = options.onResult || (() => { });
this.onRetry = options.onRetry || (() => { });
this.onComplete = options.onComplete || (() => { });
this.onError = options.onError || (() => { });
}
/**
* Execute a user request with intelligent retry
*/
async execute(userRequest, initialCommands = []) {
const attemptHistory = [];
let commands = initialCommands;
let retryCount = 0;
let allSucceeded = false;
while (retryCount < this.maxRetries && !allSucceeded) {
// If no commands yet, ask AI to generate them
if (commands.length === 0) {
const context = buildCorrectionContext(userRequest, attemptHistory, null);
const aiResponse = await this.sendToAI(context);
commands = extractCodeBlocks(aiResponse);
if (commands.length === 0) {
// AI didn't provide commands
this.onError({
type: 'no_commands',
message: 'AI did not provide executable commands',
response: aiResponse
});
break;
}
}
// Execute each command
let hadError = false;
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
this.onExecuting({ command: cmd, index: i, total: commands.length });
const result = await executeCommand(cmd);
attemptHistory.push(result);
this.onResult(result);
if (!result.success) {
hadError = true;
// Ask AI to fix the error
const errorContext = buildCorrectionContext(
userRequest,
attemptHistory,
result.stderr || result.error || 'Command failed'
);
this.onRetry({
attempt: retryCount + 1,
maxRetries: this.maxRetries,
error: result.stderr || result.error
});
const correctedResponse = await this.sendToAI(errorContext);
// Check if task is complete despite error
if (correctedResponse.includes('TASK_COMPLETE')) {
allSucceeded = true;
break;
}
// Get corrected commands
commands = extractCodeBlocks(correctedResponse);
retryCount++;
break; // Restart with new commands
}
}
if (!hadError) {
allSucceeded = true;
}
}
const finalResult = {
success: allSucceeded,
attempts: attemptHistory.length,
retries: retryCount,
history: attemptHistory
};
if (allSucceeded) {
this.onComplete(finalResult);
} else {
this.onError({ type: 'max_retries', ...finalResult });
}
return finalResult;
}
}
/**
* Quick execution helper for simple cases
*/
export async function quickExecute(commands, onResult = console.log) {
const results = [];
for (const cmd of commands) {
const result = await executeCommand(cmd);
results.push(result);
onResult(result);
if (!result.success) break;
}
return results;
}
export default {
executeCommand,
extractCodeBlocks,
buildCorrectionContext,
IntelligentExecutor,
quickExecute,
PATHS
};