830 lines
25 KiB
TypeScript
830 lines
25 KiB
TypeScript
/**
|
|
* 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.
|
|
*/
|
|
|
|
import { execSync } from "child_process";
|
|
import { callVibnChat } from "./llm/vibn-chat-model";
|
|
import { ChatMessage } from "./llm/gemini-chat";
|
|
import { AgentConfig } from "./agents";
|
|
import { executeTool, ToolContext } from "./tools";
|
|
import { resolvePrompt } from "./prompts/loader";
|
|
|
|
const MAX_TURNS = 80;
|
|
|
|
function runBuildVerification(
|
|
repoRoot: string,
|
|
appPath: string,
|
|
): { success: boolean; error?: string } {
|
|
const fs = require("fs") as typeof import("fs");
|
|
const path = require("path") as typeof import("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: string[] = [];
|
|
|
|
function scan(dir: string) {
|
|
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: any) {
|
|
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 };
|
|
}
|
|
|
|
export interface OutputLine {
|
|
ts: string;
|
|
type: "step" | "stdout" | "stderr" | "info" | "error" | "done";
|
|
text: string;
|
|
}
|
|
|
|
export interface SessionRunOptions {
|
|
sessionId: string;
|
|
projectId: string;
|
|
vibnApiUrl: string; // e.g. https://vibnai.com
|
|
appPath: string; // relative path within repo, e.g. "apps/admin"
|
|
repoRoot?: string; // absolute path to the git repo root (for auto-commit)
|
|
isStopped: () => boolean;
|
|
autoApprove?: boolean;
|
|
giteaRepo?: string; // e.g. "mark/sportsy"
|
|
coolifyAppUuid?: string;
|
|
coolifyApiUrl?: string;
|
|
coolifyApiToken?: string;
|
|
}
|
|
|
|
// ── VIBN DB bridge ────────────────────────────────────────────────────────────
|
|
|
|
async function patchSession(
|
|
opts: SessionRunOptions,
|
|
payload: {
|
|
status?: string;
|
|
outputLine?: OutputLine;
|
|
changedFile?: { path: string; status: string };
|
|
error?: string;
|
|
},
|
|
): Promise<void> {
|
|
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(): string {
|
|
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: string,
|
|
args: Record<string, unknown>,
|
|
workspaceRoot: string,
|
|
appPath: string,
|
|
): { path: string; status: string } | null {
|
|
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: SessionRunOptions,
|
|
task: string,
|
|
emit: (line: OutputLine) => Promise<void>,
|
|
): Promise<void> {
|
|
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" as const };
|
|
const giteaApiUrl = process.env.GITEA_API_URL || "";
|
|
const giteaUsername = process.env.GITEA_USERNAME || "agent";
|
|
const giteaToken = process.env.GITEA_API_TOKEN || "";
|
|
|
|
try {
|
|
try {
|
|
execSync('git config user.email "agent@vibnai.com"', gitOpts);
|
|
execSync('git config user.name "VIBN Agent"', gitOpts);
|
|
} catch {
|
|
/* already set */
|
|
}
|
|
|
|
execSync("git add -A", gitOpts);
|
|
|
|
const status = 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");
|
|
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}@`,
|
|
);
|
|
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" });
|
|
}
|
|
}
|
|
|
|
// ── Main streaming execution loop ─────────────────────────────────────────────
|
|
|
|
interface TaskItem {
|
|
text: string;
|
|
filePath: string;
|
|
lineIndex: number;
|
|
isChecked: boolean;
|
|
fileName: string;
|
|
}
|
|
|
|
function findTasksDir(root: string): string | null {
|
|
const fs = require("fs") as typeof import("fs");
|
|
const path = require("path") as typeof import("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: string): TaskItem[] {
|
|
const fs = require("fs") as typeof import("fs");
|
|
const path = require("path") as typeof import("path");
|
|
const tasksDir = findTasksDir(repoRoot);
|
|
console.log(
|
|
`[Orchestrator] repoRoot: "${repoRoot}", resolved tasksDir: "${tasksDir}"`,
|
|
);
|
|
if (!tasksDir) return [];
|
|
|
|
const items: TaskItem[] = [];
|
|
try {
|
|
const files = fs
|
|
.readdirSync(tasksDir)
|
|
.filter((f: string) => 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: string, lineIndex: number) => {
|
|
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: TaskItem): void {
|
|
const fs = require("fs") as typeof import("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: string,
|
|
repoRoot: string,
|
|
): Promise<void> {
|
|
const fs = require("fs") as typeof import("fs");
|
|
const path = require("path") as typeof import("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 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: TaskItem, repoRoot: string) {
|
|
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: TaskItem,
|
|
config: AgentConfig,
|
|
ctx: ToolContext,
|
|
opts: SessionRunOptions,
|
|
emit: (line: OutputLine) => Promise<void>,
|
|
): Promise<boolean> {
|
|
const path = require("path") as typeof import("path");
|
|
const fs = require("fs") as typeof import("fs");
|
|
const basePrompt = resolvePrompt(config.promptId);
|
|
|
|
let devServersContext = "No active dev servers running on port 3000.";
|
|
try {
|
|
const listResult = await executeTool(
|
|
"dev_server_list",
|
|
{ projectId: opts.projectId },
|
|
ctx,
|
|
);
|
|
if (Array.isArray(listResult) && listResult.length > 0) {
|
|
devServersContext = listResult
|
|
.map(
|
|
(s: any) =>
|
|
`- 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: ChatMessage[] = [{ role: "user", content: userPrompt }];
|
|
|
|
let subTurn = 0;
|
|
const SUB_MAX_TURNS = 40;
|
|
let toolCallsSinceText = 0;
|
|
let roundsSinceText = 0;
|
|
const toolFingerprints: string[] = [];
|
|
let ralphIteration = 0;
|
|
|
|
function fingerprintToolCall(tc: any) {
|
|
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: any;
|
|
try {
|
|
resp = await callVibnChat({
|
|
systemPrompt: scopedPrompt + extraSystem,
|
|
messages: history as any[],
|
|
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<string, number>();
|
|
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: any;
|
|
try {
|
|
result = await 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;
|
|
}
|
|
|
|
export async function runSessionAgent(
|
|
config: AgentConfig,
|
|
task: string,
|
|
ctx: ToolContext,
|
|
opts: SessionRunOptions,
|
|
): Promise<void> {
|
|
const emit = async (line: OutputLine) => {
|
|
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: any) {
|
|
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" });
|
|
}
|
|
}
|