chore: sync outer project state and update vibn-code submodule pointer
This commit is contained in:
@@ -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 { execSync } from "child_process";
|
||||
@@ -19,8 +13,7 @@ import { AgentConfig } from "./agents";
|
||||
import { executeTool, ToolContext } from "./tools";
|
||||
import { resolvePrompt } from "./prompts/loader";
|
||||
|
||||
|
||||
const MAX_TURNS = 60;
|
||||
const MAX_TURNS = 45;
|
||||
|
||||
export interface OutputLine {
|
||||
ts: string;
|
||||
@@ -35,7 +28,6 @@ export interface SessionRunOptions {
|
||||
appPath: string; // relative path within repo, e.g. "apps/admin"
|
||||
repoRoot?: string; // absolute path to the git repo root (for auto-commit)
|
||||
isStopped: () => boolean;
|
||||
// Auto-approve: commit + push + deploy without user confirmation
|
||||
autoApprove?: boolean;
|
||||
giteaRepo?: string; // e.g. "mark/sportsy"
|
||||
coolifyAppUuid?: string;
|
||||
@@ -65,7 +57,6 @@ async function patchSession(
|
||||
body: JSON.stringify(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,
|
||||
@@ -83,6 +74,8 @@ const FILE_WRITE_TOOLS = new Set([
|
||||
"write_file",
|
||||
"replace_in_file",
|
||||
"create_file",
|
||||
"fs_write",
|
||||
"fs_edit",
|
||||
]);
|
||||
|
||||
function extractChangedFile(
|
||||
@@ -100,7 +93,8 @@ function extractChangedFile(
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -240,8 +234,7 @@ export async function runSessionAgent(
|
||||
// Scope the system prompt to the specific app within the monorepo
|
||||
const basePrompt = 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.
|
||||
@@ -255,20 +248,30 @@ Do NOT run git commit or git push — the platform handles committing after you
|
||||
let roundsSinceText = 0;
|
||||
const toolFingerprints: string[] = [];
|
||||
let loopBreakReason: string | null = null;
|
||||
let ralphIteration = 0;
|
||||
|
||||
function fingerprintToolCall(tc: any) {
|
||||
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" });
|
||||
@@ -283,7 +286,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: any;
|
||||
@@ -292,7 +295,7 @@ Do NOT run git commit or git push — the platform handles committing after you
|
||||
systemPrompt: scopedPrompt + extraSystem,
|
||||
messages: history as any[],
|
||||
tools: config.tools,
|
||||
temperature: 0.2
|
||||
temperature: 0.2,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
@@ -302,7 +305,11 @@ Do NOT run git commit or git push — the platform handles committing after you
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -316,8 +323,43 @@ Do NOT run git commit or git push — the platform handles committing after you
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -345,31 +387,141 @@ 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}...` });
|
||||
// ── 4-Level Smart Concurrency Tool Grouping ──
|
||||
const parallelReads = resp.toolCalls.filter((tc: any) =>
|
||||
[
|
||||
"fs_read",
|
||||
"fs_tree",
|
||||
"fs_list",
|
||||
"fs_glob",
|
||||
"fs_grep",
|
||||
"projects_list",
|
||||
"project_recent_errors",
|
||||
].includes(tc.name),
|
||||
);
|
||||
const sequentialWrites = resp.toolCalls.filter((tc: any) =>
|
||||
[
|
||||
"fs_write",
|
||||
"fs_edit",
|
||||
"create_file",
|
||||
"write_file",
|
||||
"replace_in_file",
|
||||
"apps_create",
|
||||
"databases_create",
|
||||
].includes(tc.name),
|
||||
);
|
||||
const otherTools = resp.toolCalls.filter(
|
||||
(tc: any) =>
|
||||
!parallelReads.includes(tc) && !sequentialWrites.includes(tc),
|
||||
);
|
||||
|
||||
let result;
|
||||
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
|
||||
// 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: any) => {
|
||||
let result;
|
||||
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,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Stage 2: Parallelizable Other Tools
|
||||
if (otherTools.length > 0) {
|
||||
await Promise.all(
|
||||
otherTools.map(async (tc: any) => {
|
||||
await emit({
|
||||
ts: now(),
|
||||
type: "step",
|
||||
text: `Running ${tc.name}...`,
|
||||
});
|
||||
let result;
|
||||
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,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 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 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 {
|
||||
await patchSession(opts, { status: "failed", error: "Max turns reached" });
|
||||
|
||||
Submodule vibn-code updated: 5543bf9264...5783cc7931
Reference in New Issue
Block a user