init: vibn-agent-runner — Gemini autonomous agent backend
Made-with: Cursor
This commit is contained in:
198
src/server.ts
Normal file
198
src/server.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import cors from 'cors';
|
||||
import { createJob, getJob, listJobs, updateJob } from './job-store';
|
||||
import { runAgent } from './agent-runner';
|
||||
import { AGENTS } from './agents';
|
||||
import { ToolContext } from './tools';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const PORT = process.env.PORT || 3333;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build ToolContext from environment variables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildContext(repo?: string): ToolContext {
|
||||
const workspaceRoot = repo
|
||||
? `${process.env.WORKSPACE_BASE || '/workspaces'}/${repo.replace('/', '_')}`
|
||||
: (process.env.WORKSPACE_BASE || '/workspaces/default');
|
||||
|
||||
return {
|
||||
workspaceRoot,
|
||||
gitea: {
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
username: process.env.GITEA_USERNAME || ''
|
||||
},
|
||||
coolify: {
|
||||
apiUrl: process.env.COOLIFY_API_URL || '',
|
||||
apiToken: process.env.COOLIFY_API_TOKEN || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Routes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Health check
|
||||
app.get('/health', (_req: Request, res: Response) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// List available agents
|
||||
app.get('/api/agents', (_req: Request, res: Response) => {
|
||||
const agents = Object.values(AGENTS).map(a => ({
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
tools: a.tools.map(t => t.name)
|
||||
}));
|
||||
res.json(agents);
|
||||
});
|
||||
|
||||
// Submit a new job
|
||||
app.post('/api/agent/run', async (req: Request, res: Response) => {
|
||||
const { agent: agentName, task, repo } = req.body as { agent?: string; task?: string; repo?: string };
|
||||
|
||||
if (!agentName || !task) {
|
||||
res.status(400).json({ error: '"agent" and "task" are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const agentConfig = AGENTS[agentName];
|
||||
if (!agentConfig) {
|
||||
const available = Object.keys(AGENTS).join(', ');
|
||||
res.status(400).json({ error: `Unknown agent "${agentName}". Available: ${available}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const job = createJob(agentName, task, repo);
|
||||
res.status(202).json({ jobId: job.id, status: job.status });
|
||||
|
||||
// Run agent asynchronously
|
||||
const ctx = buildContext(repo);
|
||||
runAgent(job, agentConfig, task, ctx)
|
||||
.then(result => {
|
||||
updateJob(job.id, {
|
||||
status: 'completed',
|
||||
result: result.finalText,
|
||||
progress: `Done — ${result.turns} turns, ${result.toolCallCount} tool calls`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
updateJob(job.id, {
|
||||
status: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
progress: 'Agent failed'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Check job status
|
||||
app.get('/api/jobs/:id', (req: Request, res: Response) => {
|
||||
const job = getJob(req.params.id);
|
||||
if (!job) {
|
||||
res.status(404).json({ error: 'Job not found' });
|
||||
return;
|
||||
}
|
||||
res.json(job);
|
||||
});
|
||||
|
||||
// List recent jobs
|
||||
app.get('/api/jobs', (req: Request, res: Response) => {
|
||||
const limit = parseInt((req.query.limit as string) || '20', 10);
|
||||
res.json(listJobs(limit));
|
||||
});
|
||||
|
||||
// Gitea webhook endpoint — triggers agent from a push/issue event
|
||||
app.post('/webhook/gitea', (req: Request, res: Response) => {
|
||||
const event = req.headers['x-gitea-event'] as string;
|
||||
const body = req.body as any;
|
||||
|
||||
// Verify secret if configured
|
||||
const webhookSecret = process.env.WEBHOOK_SECRET;
|
||||
if (webhookSecret) {
|
||||
const sig = req.headers['x-gitea-signature'] as string;
|
||||
if (!sig || sig !== webhookSecret) {
|
||||
res.status(401).json({ error: 'Invalid webhook signature' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let task: string | null = null;
|
||||
let agentName = 'Coder';
|
||||
let repo: string | undefined;
|
||||
|
||||
if (event === 'issues' && body.action === 'opened') {
|
||||
const issue = body.issue;
|
||||
repo = `${body.repository?.owner?.login}/${body.repository?.name}`;
|
||||
const labels: string[] = (issue.labels || []).map((l: any) => l.name as string);
|
||||
|
||||
if (labels.includes('agent:pm')) {
|
||||
agentName = 'PM';
|
||||
} else if (labels.includes('agent:marketing')) {
|
||||
agentName = 'Marketing';
|
||||
} else {
|
||||
agentName = 'Coder';
|
||||
}
|
||||
|
||||
task = `Resolve this GitHub issue:\n\nTitle: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}`;
|
||||
} else if (event === 'push') {
|
||||
// Optionally trigger on push — useful for CI-style automation
|
||||
res.json({ ignored: true, reason: 'push events not auto-processed' });
|
||||
return;
|
||||
} else {
|
||||
res.json({ ignored: true, event });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
res.json({ ignored: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const agentConfig = AGENTS[agentName];
|
||||
const job = createJob(agentName, task, repo);
|
||||
res.status(202).json({ jobId: job.id, agent: agentName, event });
|
||||
|
||||
const ctx = buildContext(repo);
|
||||
runAgent(job, agentConfig, task, ctx)
|
||||
.then(result => {
|
||||
updateJob(job.id, {
|
||||
status: 'completed',
|
||||
result: result.finalText,
|
||||
progress: `Done — ${result.turns} turns, ${result.toolCallCount} tool calls`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
updateJob(job.id, {
|
||||
status: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
progress: 'Agent failed'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ error: err.message });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Start
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`AgentRunner listening on port ${PORT}`);
|
||||
console.log(`Agents available: ${Object.keys(AGENTS).join(', ')}`);
|
||||
if (!process.env.GOOGLE_API_KEY) {
|
||||
console.warn('WARNING: GOOGLE_API_KEY is not set — agents will fail');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user