v2.0.0-final: dispatcher + task queue + agent probing

Phase 4 of v2.0. BooCoder can now queue tasks and dispatch them
through the inference loop autonomously.

Dispatcher (services/dispatcher.ts): in-process setInterval(5s) polls
tasks WHERE state='pending', picks one at a time, creates an isolated
session+chat, enqueues inference with the task's input as the user
message, polls for completion, marks state completed/failed with
output_summary. Single-task-at-a-time for v2.0.0; parallel dispatch
is a Phase 5+ concern. Respects onClose hook for graceful shutdown.

Task routes (routes/tasks.ts): POST /api/tasks (create), GET /api/tasks
(list with state/project filters), GET /api/tasks/:id (detail),
POST /api/tasks/:id/cancel (marks cancelled, aborts if running).

Agent probe (services/agent-probe.ts): on startup, probes PATH for
opencode/goose/claude/pi via which + --version. UPSERTs into
available_agents table. Finds nothing inside the container (expected —
Phase 5 addresses host-agent access via ACP/PTY).

Schema: ALTER TABLE tasks ADD COLUMN IF NOT EXISTS session_id (links
task to its auto-created inference session for isolation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:55:18 +00:00
parent 73b53089b0
commit 752ea74f43
5 changed files with 396 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import type { Sql } from '../db.js';
import type { FastifyBaseLogger } from 'fastify';
const execFileAsync = promisify(execFile);
const KNOWN_AGENTS: Array<{ name: string; supportsAcp: boolean }> = [
{ name: 'opencode', supportsAcp: true },
{ name: 'goose', supportsAcp: true },
{ name: 'claude', supportsAcp: false },
{ name: 'pi', supportsAcp: false },
];
export async function probeAgents(sql: Sql, log: FastifyBaseLogger): Promise<void> {
log.info('agent-probe: scanning PATH for known agents');
for (const agent of KNOWN_AGENTS) {
try {
// Check if the agent binary is on PATH
const { stdout: whichOut } = await execFileAsync('which', [agent.name], { timeout: 5_000 });
const installPath = whichOut.trim();
if (!installPath) continue;
// Get version
let version: string | null = null;
try {
const { stdout: verOut } = await execFileAsync(agent.name, ['--version'], { timeout: 10_000 });
version = verOut.trim().slice(0, 100);
} catch {
// Some agents may not support --version — that's fine
}
// UPSERT into available_agents
await sql`
INSERT INTO available_agents (name, install_path, version, supports_acp, last_probed_at)
VALUES (${agent.name}, ${installPath}, ${version}, ${agent.supportsAcp}, clock_timestamp())
ON CONFLICT (name) DO UPDATE SET
install_path = EXCLUDED.install_path,
version = EXCLUDED.version,
supports_acp = EXCLUDED.supports_acp,
last_probed_at = EXCLUDED.last_probed_at
`;
log.info({ agent: agent.name, version, installPath }, 'agent-probe: found');
} catch {
// Agent not found on PATH — skip silently
}
}
log.info('agent-probe: scan complete');
}