diff --git a/vibn-agent-runner/dist/agent-session-runner.d.ts b/vibn-agent-runner/dist/agent-session-runner.d.ts index edbbe1cf..7c8cc6de 100644 --- a/vibn-agent-runner/dist/agent-session-runner.d.ts +++ b/vibn-agent-runner/dist/agent-session-runner.d.ts @@ -1,15 +1,9 @@ /** * agent-session-runner.ts * - * Streaming variant of runAgent wired to a VIBN agent_sessions row. - * After every LLM turn + tool call, it PATCHes the session in the VIBN DB - * so the frontend can poll (and later WebSocket) the live output. - * - * Key differences from runAgent: - * - Accepts an `emit` callback instead of updating job-store - * - Accepts an `isStopped` check so the frontend can cancel mid-run - * - Tracks which files were written/modified for the changed_files panel - * - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid] + * 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 { AgentConfig } from "./agents"; import { ToolContext } from "./tools"; diff --git a/vibn-agent-runner/dist/agent-session-runner.js b/vibn-agent-runner/dist/agent-session-runner.js index 201682d1..b52c6649 100644 --- a/vibn-agent-runner/dist/agent-session-runner.js +++ b/vibn-agent-runner/dist/agent-session-runner.js @@ -2,15 +2,9 @@ /** * agent-session-runner.ts * - * Streaming variant of runAgent wired to a VIBN agent_sessions row. - * After every LLM turn + tool call, it PATCHes the session in the VIBN DB - * so the frontend can poll (and later WebSocket) the live output. - * - * Key differences from runAgent: - * - Accepts an `emit` callback instead of updating job-store - * - Accepts an `isStopped` check so the frontend can cancel mid-run - * - Tracks which files were written/modified for the changed_files panel - * - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid] + * 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; @@ -18,7 +12,7 @@ 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 = 60; +const MAX_TURNS = 45; // ── VIBN DB bridge ──────────────────────────────────────────────────────────── async function patchSession(opts, payload) { const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`; @@ -33,7 +27,6 @@ async function patchSession(opts, payload) { }); } catch (err) { - // Log but don't crash — output will be lost for this line but loop continues console.warn("[session-runner] PATCH failed:", err instanceof Error ? err.message : err); } } @@ -45,6 +38,8 @@ 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)) @@ -56,7 +51,7 @@ function extractChangedFile(toolName, args, workspaceRoot, appPath) { const fullPrefix = `${workspaceRoot}/${appPath}/`; const appPrefix = `${appPath}/`; let displayPath = rawPath.replace(fullPrefix, "").replace(appPrefix, ""); - const fileStatus = toolName === "write_file" ? "added" : "modified"; + const fileStatus = toolName === "write_file" || toolName === "fs_write" ? "added" : "modified"; return { path: displayPath, status: fileStatus }; } // ── Auto-commit helper ──────────────────────────────────────────────────────── @@ -166,8 +161,7 @@ async function runSessionAgent(config, task, ctx, opts) { // Scope the system prompt to the specific app within the monorepo const basePrompt = (0, loader_1.resolvePrompt)(config.promptId); const scopedPrompt = `${basePrompt} - -## Active context +\n\n## Active context You are working inside the monorepo directory: ${opts.appPath} All file paths you use should be relative to this directory unless otherwise specified. When running commands, always cd into ${opts.appPath} first unless already there. @@ -179,18 +173,25 @@ Do NOT run git commit or git push — the platform handles committing after you let roundsSinceText = 0; const toolFingerprints = []; let loopBreakReason = null; + let ralphIteration = 0; function fingerprintToolCall(tc) { if (tc.name === "shell_exec") { const cmd = String(tc.args?.command ?? "").trim(); - const verb = cmd.split("&&").map(s => s.trim()).find(s => !s.startsWith("cd "))?.split(/\s+/)[0] ?? "shell"; + const verb = cmd + .split("&&") + .map((s) => s.trim()) + .find((s) => !s.startsWith("cd ")) + ?.split(/\s+/)[0] ?? "shell"; return `shell_exec:${verb}`; } - if (tc.name === "fs_write" || tc.name === "fs_edit" || tc.name === "fs_read") { + if (tc.name === "fs_write" || + tc.name === "fs_edit" || + tc.name === "fs_read") { return `${tc.name}:${tc.args?.path}`; } return `${tc.name}:${Object.values(tc.args ?? {})[0]}`; } - while (turn < 30) { + while (turn < MAX_TURNS) { if (opts.isStopped()) { await emit({ ts: now(), type: "info", text: "Stopped by user." }); await patchSession(opts, { status: "stopped" }); @@ -203,7 +204,7 @@ Do NOT run git commit or git push — the platform handles committing after you `${toolCallsSinceText} tool call(s) over ${roundsSinceText} round(s) ` + "without sending the user any text. Before any more tool calls, " + "send ONE short sentence describing what you are currently working " + - "on and why. The user is staring at silent tool pills." + "on and why." : ""; let resp; try { @@ -211,7 +212,7 @@ Do NOT run git commit or git push — the platform handles committing after you systemPrompt: scopedPrompt + extraSystem, messages: history, tools: config.tools, - temperature: 0.2 + temperature: 0.2, }); } catch (err) { @@ -221,7 +222,11 @@ Do NOT run git commit or git push — the platform handles committing after you return; } if (resp.error) { - await emit({ ts: now(), type: "error", text: `LLM error: ${resp.error}` }); + await emit({ + ts: now(), + type: "error", + text: `LLM error: ${resp.error}`, + }); await patchSession(opts, { status: "failed", error: resp.error }); return; } @@ -234,8 +239,39 @@ Do NOT run git commit or git push — the platform handles committing after you roundsSinceText++; toolCallsSinceText += resp.toolCalls.length; } + // ── Self-Correcting Ralph Loop Autonomy ── if (!resp.toolCalls.length) { - await patchSession(opts, { status: "completed" }); + const text = resp.text || ""; + const incompleteSignals = [ + "I need to", + "Let me", + "Next, I should", + "I should also", + "Additionally", + "I will now", + "I need first to", + ]; + const needsMoreWork = incompleteSignals.some((signal) => text.includes(signal)); + if (needsMoreWork && ralphIteration < 3) { + ralphIteration++; + await emit({ + ts: now(), + type: "info", + text: `🔄 [Ralph Loop] Self-reflection triggered (iteration ${ralphIteration}/3). Resuming execution...`, + }); + history.push({ + role: "user", + content: "Please continue implementing the outstanding next steps to complete the task.", + }); + continue; + } + // If fully complete, trigger auto-commit and finish + if (opts.autoApprove) { + await autoCommitAndDeploy(opts, task, emit); + } + else { + await patchSession(opts, { status: "completed" }); + } return; } for (const tc of resp.toolCalls) { @@ -260,28 +296,119 @@ Do NOT run git commit or git push — the platform handles committing after you history.push({ role: "assistant", content: resp.text, - toolCalls: resp.toolCalls + toolCalls: resp.toolCalls, }); - for (const tc of resp.toolCalls) { - 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 + // ── 4-Level Smart Concurrency Tool Grouping ── + const parallelReads = resp.toolCalls.filter((tc) => [ + "fs_read", + "fs_tree", + "fs_list", + "fs_glob", + "fs_grep", + "projects_list", + "project_recent_errors", + ].includes(tc.name)); + const sequentialWrites = resp.toolCalls.filter((tc) => [ + "fs_write", + "fs_edit", + "create_file", + "write_file", + "replace_in_file", + "apps_create", + "databases_create", + ].includes(tc.name)); + const otherTools = resp.toolCalls.filter((tc) => !parallelReads.includes(tc) && !sequentialWrites.includes(tc)); + // Stage 1: Parallel Reads + if (parallelReads.length > 0) { + await emit({ + ts: now(), + type: "step", + text: `Executing ${parallelReads.length} read operations concurrently...`, }); + await Promise.all(parallelReads.map(async (tc) => { + 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, + }); + })); + } + // Stage 2: Parallelizable Other Tools + if (otherTools.length > 0) { + await Promise.all(otherTools.map(async (tc) => { + 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, + }); + })); + } + // Stage 3: Sequential User-Safe Writes/Edits + if (sequentialWrites.length > 0) { + for (const tc of sequentialWrites) { + await emit({ + ts: now(), + type: "step", + text: `Writing modifications: ${tc.name}...`, + }); + let result; + try { + result = await (0, tools_1.executeTool)(tc.name, tc.args, ctx); + const changedFile = extractChangedFile(tc.name, tc.args, ctx.workspaceRoot, opts.appPath); + if (changedFile) { + await patchSession(opts, { changedFile }); + } + } + 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, + }); + } } } if (loopBreakReason) { - await emit({ ts: now(), type: "error", text: `Loop broken: ${loopBreakReason}` }); + await emit({ + ts: now(), + type: "error", + text: `Loop broken: ${loopBreakReason}`, + }); await patchSession(opts, { status: "failed", error: loopBreakReason }); } else { diff --git a/vibn-agent-runner/dist/server.js b/vibn-agent-runner/dist/server.js index b469b95f..a6bbb3f1 100644 --- a/vibn-agent-runner/dist/server.js +++ b/vibn-agent-runner/dist/server.js @@ -95,7 +95,7 @@ function buildContext(repo) { apiToken: process.env.COOLIFY_API_TOKEN || '' }, mcpToken: '', - vibnApiUrl: 'http://localhost:3000', + vibnApiUrl: process.env.VIBN_API_URL ?? 'https://vibnai.com', memoryUpdates: [] }; } @@ -177,7 +177,7 @@ app.get('/api/agents', (_req, res) => { }); const activeSessions = new Map(); app.post('/agent/execute', async (req, res) => { - const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, } = req.body; + const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, mcpToken, } = req.body; if (!sessionId || !projectId || !appPath || !task) { res.status(400).json({ error: 'sessionId, projectId, appPath and task are required' }); return; @@ -207,6 +207,14 @@ app.post('/agent/execute', async (req, res) => { } // Capture repo root before scoping to appPath — needed for git commit in auto-approve const repoRoot = ctx.workspaceRoot; + // Wire the ToolContext so its tools can call back into the VIBN frontend MCP + // with the right URL and auth. buildContext() defaults these to safe values, + // but the authoritative ones come from env (VIBN_API_URL) and the frontend + // (mcpToken passed in the /agent/execute body). Without this, tools fetch + // http://localhost:3000 with no token and fail with "fetch failed". + ctx.vibnApiUrl = vibnApiUrl; + ctx.mcpToken = mcpToken ?? ctx.mcpToken; + ctx.projectId = projectId; // Scope workspace to the app subdirectory so the agent works there naturally if (appPath) { const path = require('path'); diff --git a/vibn-agent-runner/src/server.ts b/vibn-agent-runner/src/server.ts index 6b504c5f..3185b1b7 100644 --- a/vibn-agent-runner/src/server.ts +++ b/vibn-agent-runner/src/server.ts @@ -68,7 +68,7 @@ function buildContext(repo?: string): ToolContext { apiToken: process.env.COOLIFY_API_TOKEN || '' }, mcpToken: '', - vibnApiUrl: 'http://localhost:3000', + vibnApiUrl: process.env.VIBN_API_URL ?? 'https://vibnai.com', memoryUpdates: [] }; } @@ -172,7 +172,7 @@ const activeSessions = new Map(); app.post('/agent/execute', async (req: Request, res: Response) => { const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, - autoApprove, coolifyAppUuid, + autoApprove, coolifyAppUuid, mcpToken, } = req.body as { sessionId?: string; projectId?: string; @@ -183,6 +183,7 @@ app.post('/agent/execute', async (req: Request, res: Response) => { continueTask?: string; autoApprove?: boolean; coolifyAppUuid?: string; + mcpToken?: string; }; if (!sessionId || !projectId || !appPath || !task) { @@ -219,6 +220,15 @@ app.post('/agent/execute', async (req: Request, res: Response) => { // Capture repo root before scoping to appPath — needed for git commit in auto-approve const repoRoot = ctx.workspaceRoot; + // Wire the ToolContext so its tools can call back into the VIBN frontend MCP + // with the right URL and auth. buildContext() defaults these to safe values, + // but the authoritative ones come from env (VIBN_API_URL) and the frontend + // (mcpToken passed in the /agent/execute body). Without this, tools fetch + // http://localhost:3000 with no token and fail with "fetch failed". + ctx.vibnApiUrl = vibnApiUrl; + ctx.mcpToken = mcpToken ?? ctx.mcpToken; + ctx.projectId = projectId; + // Scope workspace to the app subdirectory so the agent works there naturally if (appPath) { const path = require('path') as typeof import('path');