wire up /agent/execute and /agent/stop endpoints

- Add runSessionAgent: streaming variant of runAgent that PATCHes VIBN DB
  after every LLM turn and tool call so frontend can poll live output
- Track changed files from write_file / replace_in_file tool calls
- Add /agent/execute: receives sessionId + giteaRepo + task, clones repo,
  scopes workspace to appPath, runs Coder agent async (returns 202 immediately)
- Add /agent/stop: sets stopped flag; agent checks between turns and exits cleanly
- Agent does NOT commit on completion — leaves changes for user review/approval

Made-with: Cursor
This commit is contained in:
2026-03-06 18:01:30 -08:00
parent 335c7a7e97
commit 5aeddace91
13 changed files with 850 additions and 5 deletions

View File

@@ -6,6 +6,7 @@ import * as crypto from 'crypto';
import { execSync } from 'child_process';
import { createJob, getJob, listJobs, updateJob } from './job-store';
import { runAgent } from './agent-runner';
import { runSessionAgent } from './agent-session-runner';
import { AGENTS } from './agents';
import { ToolContext } from './tools';
import { PROTECTED_GITEA_REPOS } from './tools/security';
@@ -343,6 +344,116 @@ app.post('/webhook/gitea', (req: Request, res: Response) => {
});
});
// ---------------------------------------------------------------------------
// Agent Execute — VIBN Build > Code > Agent tab
//
// Receives a task from the VIBN frontend, runs the Coder agent against
// the project's Gitea repo, and streams progress back to the VIBN DB
// via PATCH /api/projects/[id]/agent/sessions/[sid].
//
// This endpoint returns immediately (202) and runs the agent async so
// the browser can close without killing the loop.
// ---------------------------------------------------------------------------
// Track active sessions for stop support
const activeSessions = new Map<string, { stopped: boolean }>();
app.post('/agent/execute', async (req: Request, res: Response) => {
const { sessionId, projectId, appName, appPath, giteaRepo, task } = req.body as {
sessionId?: string;
projectId?: string;
appName?: string;
appPath?: string;
giteaRepo?: string;
task?: string;
};
if (!sessionId || !projectId || !appPath || !task) {
res.status(400).json({ error: 'sessionId, projectId, appPath and task are required' });
return;
}
const vibnApiUrl = process.env.VIBN_API_URL ?? 'https://vibnai.com';
// Register session as active
const sessionState = { stopped: false };
activeSessions.set(sessionId, sessionState);
// Respond immediately — execution is async
res.status(202).json({ sessionId, status: 'running' });
// Build workspace context — clone/update the Gitea repo if provided
let ctx: ReturnType<typeof buildContext>;
try {
ctx = buildContext(giteaRepo);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error('[agent/execute] buildContext failed:', msg);
// Notify VIBN DB of failure
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'failed', error: msg }),
}).catch(() => {});
activeSessions.delete(sessionId);
return;
}
// Scope workspace to the app subdirectory so the agent works there naturally
if (appPath) {
const path = require('path') as typeof import('path');
ctx.workspaceRoot = path.join(ctx.workspaceRoot, appPath);
const fs = require('fs') as typeof import('fs');
fs.mkdirSync(ctx.workspaceRoot, { recursive: true });
}
const agentConfig = AGENTS['Coder'];
if (!agentConfig) {
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'failed', error: 'Coder agent not registered' }),
}).catch(() => {});
activeSessions.delete(sessionId);
return;
}
// Run the streaming agent loop (fire and forget)
runSessionAgent(agentConfig, task, ctx, {
sessionId,
projectId,
vibnApiUrl,
appPath,
isStopped: () => sessionState.stopped,
})
.catch(err => {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[agent/execute] session ${sessionId} crashed:`, msg);
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'failed', error: msg }),
}).catch(() => {});
})
.finally(() => {
activeSessions.delete(sessionId);
});
});
app.post('/agent/stop', (req: Request, res: Response) => {
const { sessionId } = req.body as { sessionId?: string };
if (!sessionId) { res.status(400).json({ error: 'sessionId required' }); return; }
const session = activeSessions.get(sessionId);
if (session) {
session.stopped = true;
res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' });
} else {
// Session may have already finished
res.json({ ok: true, message: 'Session not active (may have already completed).' });
}
});
// ---------------------------------------------------------------------------
// Generate — thin structured-generation endpoint (no session, no system prompt)
// Use this for one-shot tasks like architecture recommendations.