"use strict"; /** * agent-session-runner.ts * * Upgraded Cloud Agent Executor for VibnCode. * Implements 4-level Smart Concurrency (parallel reads/lookups) and the * Ralph Loop (autonomous self-correction) entirely inside your secure Cloud VM. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.runSessionAgent = runSessionAgent; const child_process_1 = require("child_process"); const vibn_chat_model_1 = require("./llm/vibn-chat-model"); const tools_1 = require("./tools"); const loader_1 = require("./prompts/loader"); const MAX_TURNS = 80; function runBuildVerification(repoRoot, appPath) { const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); const absoluteAppPath = path.join(repoRoot, appPath); // Find all directories containing package.json (excluding node_modules, .git, .next, .vibncode, dist) const pkgDirs = []; function scan(dir) { try { const files = fs.readdirSync(dir); if (files.includes("package.json")) { pkgDirs.push(dir); } for (const file of files) { if (file === "node_modules" || file === ".git" || file === ".next" || file === ".vibncode" || file === "dist") { continue; } const full = path.join(dir, file); if (fs.statSync(full).isDirectory()) { scan(full); } } } catch { } } scan(absoluteAppPath); if (pkgDirs.length === 0) { return { success: true }; // No package.json anywhere, skip build check } for (const dir of pkgDirs) { const pkgJsonPath = path.join(dir, "package.json"); try { const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")); // Skip if there's no build script or if it's the root container which is just a workspace wrapper if (!pkg.scripts || !pkg.scripts.build || pkg.name === "workspace") { continue; } console.log(`[Ralph Loop] Running automatic build verification: npm run build inside ${dir}...`); execSync("npm run build", { cwd: dir, stdio: "pipe", timeout: 60000, }); } catch (err) { const stderr = err.stderr ? err.stderr.toString() : err.message || String(err); console.warn(`[Ralph Loop] Build verification failed inside ${dir}:`, stderr); return { success: false, error: `Build failed in directory "${path.relative(repoRoot, dir)}":\n${stderr.slice(-3000)}`, }; } } return { success: true }; } // ── VIBN DB bridge ──────────────────────────────────────────────────────────── async function patchSession(opts, payload) { const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`; try { await fetch(url, { method: "PATCH", headers: { "Content-Type": "application/json", "x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET ?? "", }, body: JSON.stringify(payload), }); } catch (err) { console.warn("[session-runner] PATCH failed:", err instanceof Error ? err.message : err); } } function now() { return new Date().toISOString(); } // ── File change tracking ────────────────────────────────────────────────────── const FILE_WRITE_TOOLS = new Set([ "write_file", "replace_in_file", "create_file", "fs_write", "fs_edit", ]); function extractChangedFile(toolName, args, workspaceRoot, appPath) { if (!FILE_WRITE_TOOLS.has(toolName)) return null; const rawPath = String(args.path ?? args.file_path ?? ""); if (!rawPath) return null; // Make path relative to appPath for display const fullPrefix = `${workspaceRoot}/${appPath}/`; const appPrefix = `${appPath}/`; let displayPath = rawPath.replace(fullPrefix, "").replace(appPrefix, ""); const fileStatus = toolName === "write_file" || toolName === "fs_write" ? "added" : "modified"; return { path: displayPath, status: fileStatus }; } // ── Auto-commit helper ──────────────────────────────────────────────────────── async function autoCommitAndDeploy(opts, task, emit) { const repoRoot = opts.repoRoot; if (!repoRoot || !opts.giteaRepo) { await emit({ ts: now(), type: "info", text: "Auto-approve skipped — no repo root available.", }); return; } const gitOpts = { cwd: repoRoot, stdio: "pipe" }; const giteaApiUrl = process.env.GITEA_API_URL || ""; const giteaUsername = process.env.GITEA_USERNAME || "agent"; const giteaToken = process.env.GITEA_API_TOKEN || ""; try { try { (0, child_process_1.execSync)('git config user.email "agent@vibnai.com"', gitOpts); (0, child_process_1.execSync)('git config user.name "VIBN Agent"', gitOpts); } catch { /* already set */ } (0, child_process_1.execSync)("git add -A", gitOpts); const status = (0, child_process_1.execSync)("git status --porcelain", gitOpts) .toString() .trim(); if (!status) { await emit({ ts: now(), type: "info", text: "✓ No file changes to commit.", }); await patchSession(opts, { status: "approved" }); return; } const commitMsg = `agent: ${task.slice(0, 72)}`; const msgFile = require("path").join(opts.repoRoot || process.cwd(), ".git", "COMMIT_EDITMSG"); require("fs").writeFileSync(msgFile, commitMsg, "utf8"); (0, child_process_1.execSync)("git commit -F .git/COMMIT_EDITMSG", gitOpts); try { require("fs").unlinkSync(msgFile); } catch { } await emit({ ts: now(), type: "info", text: `✓ Committed: "${commitMsg}"`, }); const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git`.replace("https://", `https://${giteaUsername}:${giteaToken}@`); (0, child_process_1.execSync)(`git push "${authedUrl}" HEAD:main`, gitOpts); await emit({ ts: now(), type: "info", text: "✓ Pushed to Gitea." }); // Optional Coolify deploy let deployed = false; if (opts.coolifyApiUrl && opts.coolifyApiToken && opts.coolifyAppUuid) { try { const deployRes = await fetch(`${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`, { method: "POST", headers: { Authorization: `Bearer ${opts.coolifyApiToken}` }, }); deployed = deployRes.ok; if (deployed) await emit({ ts: now(), type: "info", text: "✓ Deployment triggered.", }); } catch { /* best-effort */ } } await patchSession(opts, { status: "approved", outputLine: { ts: now(), type: "done", text: `✓ Auto-committed & ${deployed ? "deployed" : "pushed"}. No approval needed.`, }, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); await emit({ ts: now(), type: "error", text: `Auto-commit failed: ${msg}`, }); // Fall back to done so user can manually approve await patchSession(opts, { status: "done" }); } } function findTasksDir(root) { const fs = require("fs"); const path = require("path"); // 1. Check if root/.vibncode/tasks exists directly const direct = path.join(root, ".vibncode", "tasks"); if (fs.existsSync(direct)) return direct; // 2. Recursively scan subdirectories (excluding node_modules, .git, etc.) try { const files = fs.readdirSync(root); for (const file of files) { if (file === "node_modules" || file === ".git" || file === ".next" || file === "dist") { continue; } const full = path.join(root, file); if (fs.statSync(full).isDirectory()) { const found = findTasksDir(full); if (found) return found; } } } catch { } return null; } function parseTaskItems(repoRoot) { const fs = require("fs"); const path = require("path"); const tasksDir = findTasksDir(repoRoot); console.log(`[Orchestrator] repoRoot: "${repoRoot}", resolved tasksDir: "${tasksDir}"`); if (!tasksDir) return []; const items = []; try { const files = fs .readdirSync(tasksDir) .filter((f) => f.endsWith(".md")); console.log(`[Orchestrator] Found task files:`, files); files.sort(); for (const file of files) { const filePath = path.join(tasksDir, file); const content = fs.readFileSync(filePath, "utf8"); console.log(`[Orchestrator] Reading ${file} (length: ${content.length} bytes). Head:\n${content.slice(0, 500)}`); const lines = content.split("\n"); lines.forEach((line, lineIndex) => { const match = line.match(/^(\s*)(?:-\s*)?\[([ xX])\]\s+(.+)$/); if (match && match[2] !== undefined && match[3] !== undefined) { const checked = match[2].toLowerCase() === "x"; console.log(`[Orchestrator] Parsed line ${lineIndex + 1}: isChecked=${checked}, text="${match[3].trim()}"`); items.push({ text: match[3].trim(), filePath, lineIndex, isChecked: checked, fileName: file, }); } }); } } catch (err) { console.error("[Orchestrator] Error parsing task items:", err); } return items; } function toggleTaskOnDisk(task) { const fs = require("fs"); const content = fs.readFileSync(task.filePath, "utf8"); const lines = content.split("\n"); const line = lines[task.lineIndex]; if (line) { const match = line.match(/^(\s*)(?:-\s*)?\[([ xX])\]\s+(.+)$/); if (match && match[1] !== undefined && match[3] !== undefined) { const indent = match[1] || ""; const hasDash = line.includes("-"); const prefix = hasDash ? `${indent}- ` : indent; lines[task.lineIndex] = `${prefix}[x] ${match[3]}`; fs.writeFileSync(task.filePath, lines.join("\n"), "utf8"); } } } async function generateBacklogFromPrompt(taskPrompt, repoRoot) { const fs = require("fs"); const path = require("path"); const tasksDir = path.join(repoRoot, ".vibncode", "tasks"); fs.mkdirSync(tasksDir, { recursive: true }); const prompt = `You are an elite Software Engineering Orchestrator. Your goal is to break down the user's high-level objective into a highly detailed, sequential checklist of concrete, atomic, self-contained implementation tasks. High-Level Objective: "${taskPrompt}" Please output a standard Markdown file containing: 1. A brief 1-sentence overview. 2. A list of tasks, where each task MUST be formatted as a standard Markdown checkbox starting with "- [ ] ": - [ ] Implement database schema changes for ... - [ ] Add endpoint handler for ... - [ ] Write tests ... Be extremely thorough and break the objective down into small, digestible units of work (e.g. 5-15 tasks). Do NOT include any extra conversational text or explanations. Just output the clean markdown.`; const resp = await (0, vibn_chat_model_1.callVibnChat)({ systemPrompt: "You are a precise technical orchestrator who only outputs markdown checklist files.", messages: [{ role: "user", content: prompt }], temperature: 0.1, }); const content = resp.text || `# Delegated Backlog\n\n- [ ] ${taskPrompt}`; const backlogPath = path.join(tasksDir, "00-delegated-backlog.md"); fs.writeFileSync(backlogPath, content, "utf8"); } function commitTaskProgress(task, repoRoot) { const { execSync } = require("child_process"); try { console.log(`[Orchestrator] Committing task progress: ${task.text}`); execSync("git add -A", { cwd: repoRoot, stdio: "pipe" }); const msg = `feat(tasks): [Completed] ${task.text}`; execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: repoRoot, stdio: "pipe", }); } catch (err) { // If nothing to commit, that's fine } } async function runSingleSubTask(task, config, ctx, opts, emit) { const path = require("path"); const fs = require("fs"); const basePrompt = (0, loader_1.resolvePrompt)(config.promptId); let devServersContext = "No active dev servers running on port 3000."; try { const listResult = await (0, tools_1.executeTool)("dev_server_list", { projectId: opts.projectId }, ctx); if (Array.isArray(listResult) && listResult.length > 0) { devServersContext = listResult .map((s) => `- Port ${s.port} (${s.state}): ${s.command} -> Preview URL: ${s.previewUrl}`) .join("\n"); } } catch { } const scopedPrompt = `${basePrompt} ## ACTIVE DEVELOPER WORKSPACE STATE Active Dev Servers: ${devServersContext} ## ACTIVE SUBTASK OBJECTIVE You are working on a single task in your task queue: TASK: "${task.text}" File: "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" (line ${task.lineIndex + 1}) ## CRITICAL EXECUTION CONSTRAINTS 1. 🎯 STAY HIGHLY FOCUSED: Your only objective is to implement this specific task. Do NOT wander, do NOT explore other unrelated parts of the codebase, and do NOT attempt unrelated tasks. 2. 🚫 NO EXPLORATION COMMANDS: DO NOT execute generic orientation/search commands like 'ls', 'find', 'pwd', 'grep', 'git diff', 'git status'. You already know the repository structure. Go straight to editing or reading the targeted files. 3. 🛠️ TOGGLE CHECKBOX: Once your implementation is done, you MUST read and rewrite "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" at line ${task.lineIndex + 1} to change "- [ ]" to "- [x]". 4. 🔴 NO COMMITS: Do NOT run 'git commit' or 'git push'. The platform handles committing automatically after you finish. 5. 🟢 COMPLETED SIGNAL: When you are finished, verify the build compiles clean using the Ralph Loop checks. If successful, stop executing tools and end your response. `; const userPrompt = `Please implement the following task: "${task.text}" and then check it off in the task list.`; const history = [{ role: "user", content: userPrompt }]; let subTurn = 0; const SUB_MAX_TURNS = 40; let toolCallsSinceText = 0; let roundsSinceText = 0; const toolFingerprints = []; let ralphIteration = 0; function fingerprintToolCall(tc) { const name = tc.name; const args = tc.args ?? {}; if (name === "shell_exec") { const cmd = String(args.command ?? "").trim(); const firstWord = cmd.split(/\s+/)[0] ?? "shell"; return `shell_exec:${firstWord}`; } // Determine target based on most common descriptive parameter keys const target = args.path ?? args.pattern ?? args.command ?? args.commandId ?? args.appUuid ?? args.uuid ?? ""; if (target) { return `${name}:${target}`; } // Filter out common metadata like projectId, and use first real argument const keys = Object.keys(args).filter((k) => k !== "projectId"); if (keys.length > 0) { return `${name}:${args[keys[0]]}`; } return `${name}:default`; } while (subTurn < SUB_MAX_TURNS) { if (opts.isStopped()) { await emit({ ts: now(), type: "info", text: "Stopped by user." }); return false; } subTurn++; const isSilent = roundsSinceText >= 8 || toolCallsSinceText >= 12; const extraSystem = isSilent ? "\n\n[STATUS NUDGE] Focus on completing the current task. Do not make any more tool calls without a short sentence explaining what you are working on." : ""; let resp; try { resp = await (0, vibn_chat_model_1.callVibnChat)({ systemPrompt: scopedPrompt + extraSystem, messages: history, tools: config.tools, temperature: 0.1, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); await emit({ ts: now(), type: "error", text: `LLM sub-session error: ${msg}`, }); return false; } if (resp.error) { await emit({ ts: now(), type: "error", text: `LLM sub-session error: ${resp.error}`, }); return false; } if (resp.text) { await emit({ ts: now(), type: "info", text: resp.text }); roundsSinceText = 0; toolCallsSinceText = 0; } else if (resp.toolCalls.length) { roundsSinceText++; toolCallsSinceText += resp.toolCalls.length; } if (!resp.toolCalls.length) { if (opts.repoRoot && ralphIteration < 3) { await emit({ ts: now(), type: "info", text: "🔍 [Ralph Loop] Verifying build for this task...", }); const verification = runBuildVerification(opts.repoRoot, opts.appPath); if (!verification.success) { ralphIteration++; await emit({ ts: now(), type: "error", text: `❌ [Ralph Loop] Build failed (iteration ${ralphIteration}/3) for this task.`, }); history.push({ role: "user", content: `Your previous edits completed, but the project's build check failed with compilation errors. ========================================= 🚨 SURGICAL HEALING PROTOCOL ACTIVE 🚨 ========================================= The project's compilation/build has failed. You are currently in an autonomous, auto-correcting healing loop and must fix this compilation error immediately. To prevent cognitive loop spirals and command limits, you MUST follow this strict, non-negotiable troubleshooting protocol: 1. 🚫 STRICTLY BLOCK EXPLORATION: DO NOT execute general directory exploration or orientation commands such as 'ls', 'find', 'pwd', 'grep', 'git status', 'git diff', or other search commands. You do not need to look around. 2. 🎯 SURGICAL TARGETING: Scan the compiler error logs below to locate the EXACT filename, line number, and column where the compilation failed. 3. 🛠️ IMMEDIATE CORRECTION: Read that file immediately using your specific file-reading tool (using precise start/end lines if it is large) and apply a targeted, surgical edit to correct the exact syntax or type error. Do not write a placeholder or partial fix. Here are the precise compilation errors from the compiler: \`\`\`text ${verification.error} \`\`\` Implement the exact fix directly in the code now.`, }); continue; } else { await emit({ ts: now(), type: "info", text: "🟢 [Ralph Loop] Build passed successfully! 0 errors.", }); } } let diskChecked = false; try { const fileContent = fs.readFileSync(task.filePath, "utf8"); const lines = fileContent.split("\n"); const line = lines[task.lineIndex]; if (line) { const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/); if (match && match[2].toLowerCase() === "x") { diskChecked = true; } } } catch { } if (!diskChecked) { await emit({ ts: now(), type: "info", text: `✍️ [Orchestrator] Task implementation completed. Automatically checking off task on disk.`, }); toggleTaskOnDisk(task); } return true; } for (const tc of resp.toolCalls) { toolFingerprints.push(fingerprintToolCall(tc)); } const window = toolFingerprints.slice(-12); const counts = new Map(); for (const fp of window) counts.set(fp, (counts.get(fp) ?? 0) + 1); let maxRepeats = 0; let repeatedCmd = ""; for (const [fp, n] of counts.entries()) { if (n > maxRepeats) { maxRepeats = n; repeatedCmd = fp; } } if (maxRepeats >= 6) { await emit({ ts: now(), type: "error", text: `Loop detected in subtask execution (repeated "${repeatedCmd}" ${maxRepeats}x in last 12 calls), breaking loop.`, }); return false; } history.push({ role: "assistant", content: resp.text, toolCalls: resp.toolCalls, }); for (const tc of resp.toolCalls) { if (opts.isStopped()) return false; await emit({ ts: now(), type: "step", text: `Running ${tc.name}...`, }); let result; try { result = await (0, tools_1.executeTool)(tc.name, tc.args, ctx); } catch (err) { result = { error: err instanceof Error ? err.message : String(err) }; } const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2); history.push({ role: "tool", content: resultStr, toolCallId: tc.id, toolName: tc.name, }); } } await emit({ ts: now(), type: "error", text: `Subtask exceeded maximum turns limit of ${SUB_MAX_TURNS}.`, }); return false; } async function runSessionAgent(config, task, ctx, opts) { const emit = async (line) => { console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`); await patchSession(opts, { outputLine: line }); }; await emit({ ts: now(), type: "info", text: `Agent started offline delegation orchestrator in ${opts.appPath}`, }); const repoRoot = opts.repoRoot ?? ctx.workspaceRoot; let tasks = parseTaskItems(repoRoot); if (tasks.length === 0) { await emit({ ts: now(), type: "info", text: "🤖 [Orchestrator] No active tasks backlog found on disk. Analyzing prompt to plan atomic execution backlog...", }); try { await generateBacklogFromPrompt(task, repoRoot); tasks = parseTaskItems(repoRoot); } catch (err) { await emit({ ts: now(), type: "error", text: `❌ [Orchestrator] Failed to generate backlog: ${err.message || String(err)}`, }); await patchSession(opts, { status: "failed", error: "Backlog generation failed", }); return; } } const openTasks = tasks.filter((t) => !t.isChecked); if (openTasks.length === 0) { await emit({ ts: now(), type: "info", text: "🟢 [Orchestrator] All tasks in the queue are already completed!", }); await patchSession(opts, { status: "completed" }); return; } await emit({ ts: now(), type: "info", text: `🤖 [Orchestrator] Found ${openTasks.length} open tasks. Executing task-by-task Meta-Loop...`, }); for (let i = 0; i < openTasks.length; i++) { const currentTask = openTasks[i]; await emit({ ts: now(), type: "info", text: `🚀 [Orchestrator] Task ${i + 1}/${openTasks.length}: "${currentTask.text}"`, }); const success = await runSingleSubTask(currentTask, config, ctx, opts, emit); if (!success) { await emit({ ts: now(), type: "error", text: `❌ [Orchestrator] Bailed out! Task execution failed on: "${currentTask.text}".`, }); await patchSession(opts, { status: "failed", error: `Delegation loop halted at task: "${currentTask.text}"`, }); return; } commitTaskProgress(currentTask, repoRoot); } await emit({ ts: now(), type: "info", text: `🎉 [Orchestrator] All delegated tasks completed successfully with green compilation builds!`, }); if (opts.autoApprove) { await autoCommitAndDeploy(opts, task, emit); } else { await patchSession(opts, { status: "completed" }); } }