This repository has been archived on 2026-06-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
master-ai/vibn-agent-runner/dist/agent-session-runner.js

644 lines
25 KiB
JavaScript

"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 === ".vibncode" ||
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);
if (!tasksDir)
return [];
const items = [];
try {
const files = fs
.readdirSync(tasksDir)
.filter((f) => f.endsWith(".md"));
files.sort();
for (const file of files) {
const filePath = path.join(tasksDir, file);
const content = fs.readFileSync(filePath, "utf8");
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) {
items.push({
text: match[3].trim(),
filePath,
lineIndex,
isChecked: match[2].toLowerCase() === "x",
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);
const scopedPrompt = `${basePrompt}
## 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" });
}
}