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

@@ -325,6 +325,39 @@ export const AgentStatusUpdatedFrame = z.object({
at: IsoTimestamp,
});
// ---- orchestrator frames ([D-6]) -------------------------------------------
const FlowStepManifestEntry = z.object({
step_id: z.string().min(1),
agent: z.string().min(1),
kind: z.enum(['agent', 'code']),
chat_id: Uuid,
label: z.string().min(1),
});
// Published once when a flow run starts. Carries the full step manifest so
// the OrchestratorPane can build its roster immediately.
export const FlowRunStartedFrame = z.object({
type: z.literal('flow_run_started'),
run_id: Uuid,
flow_name: z.string().min(1),
band: z.enum(['small', 'medium', 'large']),
steps: z.array(FlowStepManifestEntry),
});
// Published on every step status change and on run completion. `report` is
// present (and non-null) only when `run_status === 'completed'` — it rides here
// rather than a dedicated frame (D-6). Phase 6: `cancelled` added to both enums
// so the stop route can publish cancel transitions (DB CHECKs already included it).
export const FlowRunStepUpdatedFrame = z.object({
type: z.literal('flow_run_step_updated'),
run_id: Uuid,
step_id: z.string().min(1),
status: z.enum(['pending', 'running', 'completed', 'failed', 'skipped', 'cancelled']),
run_status: z.enum(['running', 'completed', 'failed', 'cancelled']).optional(),
report: z.string().optional(),
});
// ---- discriminated union ---------------------------------------------------
export const WsFrameSchema = z.discriminatedUnion('type', [
@@ -345,6 +378,9 @@ export const WsFrameSchema = z.discriminatedUnion('type', [
PermissionResolvedFrame,
AgentCommandsFrame,
AgentStatusUpdatedFrame,
// orchestrator
FlowRunStartedFrame,
FlowRunStepUpdatedFrame,
// per-user
ChatStatusFrame,
SessionUpdatedFrame,
@@ -387,6 +423,8 @@ export const KNOWN_FRAME_TYPES: readonly WsFrame['type'][] = [
'permission_resolved',
'agent_commands',
'agent_status_updated',
'flow_run_started',
'flow_run_step_updated',
'chat_status',
'session_updated',
'session_renamed',