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:
111
src/server.ts
111
src/server.ts
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user