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

@@ -7,6 +7,7 @@ import type {
ErrorReason,
HtmlArtifactState,
MarkdownArtifactState,
OrchestratorState,
Project,
Session,
} from '@/api/types';
@@ -184,6 +185,52 @@ export interface GitDiffRefreshEvent {
type: 'git_diff_refresh';
}
// Orchestrator: emitted client-side to open an orchestrator pane.
// useWorkspacePanes subscribes and inserts the pane (or focuses an existing one).
// placement carries the surface context: 'new' (+ menu) or 'split' (split-pane
// menu). addOrchestratorPane appends in both cases; the hint is available for
// future positional differentiation.
export interface OpenOrchestratorPaneEvent {
type: 'open_orchestrator_pane';
state: OrchestratorState;
placement?: 'new' | 'split';
}
// Orchestrator: emitted by the Workflow button on ChatInput (or "New Orchestrator"
// menu items) to request the flow launcher dialog. Carries the current pane's
// project and the placement context ('new' from the + menu, 'split' from the
// split-pane menu) so the resulting open_orchestrator_pane can be placed correctly.
export interface OpenFlowLauncherEvent {
type: 'open_flow_launcher';
project_id: string;
placement?: 'new' | 'split';
}
// Orchestrator: run-level frames forwarded from the coder user channel by
// useCoderUserEvents. OrchestratorPane subscribes to update its roster/report.
export interface FlowRunStartedEvent {
type: 'flow_run_started';
run_id: string;
flow_name: string;
band: 'small' | 'medium' | 'large';
steps: Array<{
step_id: string;
agent: string;
kind: 'agent' | 'code';
chat_id: string;
label: string;
}>;
}
export interface FlowRunStepUpdatedEvent {
type: 'flow_run_step_updated';
run_id: string;
step_id: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled';
run_status?: 'running' | 'completed' | 'failed' | 'cancelled';
report?: string;
}
export type SessionEvent =
| SessionRenamedEvent
| ProjectCreatedEvent
@@ -211,7 +258,11 @@ export type SessionEvent =
| ProjectUpdatedEvent
| ChatStatusEvent
| RefetchMessagesEvent
| GitDiffRefreshEvent;
| GitDiffRefreshEvent
| OpenOrchestratorPaneEvent
| FlowRunStartedEvent
| FlowRunStepUpdatedEvent
| OpenFlowLauncherEvent;
type Listener = (event: SessionEvent) => void;
const listeners = new Set<Listener>();