feat: in-app Orchestrator (Phase 2) — multi-agent conductor

Brings the deterministic Han-flow conductor into BooCode: launch any read-only
flow from BooChat or BooCoder, watch each agent stream live in a Paseo-style
run pane, get an evidence-disciplined report — on local Qwen, persisted and
resumable. Read-only enforced hard via qwen --approval-mode plan (orchestrator
tasks fail closed if qwen is unavailable; never fall to write-capable native).

Backend (apps/coder): re-homed conductor defs, flow_runs/flow_steps schema,
flow-runner + dispatcher onTaskTerminal hook, restart-resume, runs routes
(launch/list/get/cancel), user-channel WS. Contracts: two flow_run_* frames.
Web: orchestrator pane kind + OrchestratorPane, Workflow button + slash flows
(BooChat/BooCoder parity), FlowLauncherDialog, "New Orchestrator" in the + and
split menus, runs history + export. Plan: openspec/changes/orchestrator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:59:07 +00:00
parent 519b1d2ca1
commit 1937af8df9
118 changed files with 15723 additions and 27 deletions

View File

@@ -24,12 +24,16 @@ import { registerTaskRoutes } from './routes/tasks.js';
import { registerInboxRoutes } from './routes/inbox.js';
import { registerStatsRoutes } from './routes/stats.js';
import { registerArenaRoutes } from './routes/arena.js';
import { registerRunsRoutes } from './routes/runs.js';
import { registerProviderRoutes } from './routes/providers.js';
import { registerWorktreeSafetyRoutes } from './routes/worktree-safety.js';
import { registerLifecycleRoutes } from './routes/lifecycle.js';
import { registerWebSocket } from './routes/ws.js';
// Phase 4: dispatcher + agent probe
import { createDispatcher } from './services/dispatcher.js';
// Orchestrator (Phase 2): DB-backed flow-runner; advances on the dispatcher's
// onTaskTerminal hook.
import { createFlowRunner } from './services/flow-runner.js';
import { agentPool } from './services/agent-pool.js';
import { createOrphanWorktreeReaper } from './services/orphan-worktree-reaper.js';
import { probeAgents } from './services/agent-probe.js';
@@ -214,10 +218,34 @@ async function main() {
);
});
// Phase 4: dispatcher — polls tasks table and runs inference
const dispatcher = createDispatcher({ sql, inference: inferenceApi, broker, log: app.log, config });
// Orchestrator (Phase 2): the flow-runner reacts to the dispatcher's
// onTaskTerminal hook to advance flow_runs. Created before the dispatcher so its
// terminal callback can be wired in. Its launch() is driven by the runs route
// (a later phase); resume on startup is a later phase too.
const flowRunner = createFlowRunner({ sql, broker, log: app.log, config });
// Phase 4: dispatcher — polls tasks table and runs inference. onTaskTerminal
// notifies the flow-runner when a step's task settles (D-2).
const dispatcher = createDispatcher({
sql,
inference: inferenceApi,
broker,
log: app.log,
config,
onTaskTerminal: flowRunner.handleTaskTerminal,
});
dispatcher.start();
// Phase 5: re-advance any flow_runs that were 'running' when the service last
// stopped (D-9). Runs AFTER dispatcher.start() so re-dispatched 'pending' tasks
// are picked up by the dispatcher's startup poll.
void flowRunner.initResume().catch((err) => {
app.log.error(
{ err: err instanceof Error ? err.message : String(err) },
'flow-runner: initResume failed',
);
});
// v2.6 Phase 3: configure + start the agent-pool lifecycle sweep (idle-TTL +
// LRU-cap eviction of warm backends, plus each backend's proactive health probe)
// and the orphan-worktree reaper. Both run on the same periodic timer.
@@ -254,6 +282,7 @@ async function main() {
registerInboxRoutes(app, sql);
registerStatsRoutes(app, sql);
registerArenaRoutes(app, sql);
registerRunsRoutes(app, sql, flowRunner, dispatcher.cancelExternalTask);
registerProviderRoutes(app, sql, config);
registerWorktreeSafetyRoutes(app, sql);
registerLifecycleRoutes(app, sql);