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.
This commit is contained in:
2026-06-03 14:59:07 +00:00
parent 7ff99238c9
commit fa8d707923
118 changed files with 15723 additions and 27 deletions

View File

@@ -25,6 +25,8 @@ import type {
PermissionPrompt,
AgentCommand,
WorkspaceState,
FlowRunRow,
FlowStepRow,
} from './types';
// v2.6 Phase 1-UX §9b: chat-scoped agent-session rows. Returned by
@@ -483,6 +485,34 @@ export const api = {
}),
},
// Orchestrator run API — proxied to boocoder at /api/coder/runs/*.
runs: {
launch: (body: {
project_id: string;
flow_name: string;
band: 'small' | 'medium' | 'large';
input: { question: string };
model?: string;
}) =>
request<{ run_id: string }>('/api/coder/runs', {
method: 'POST',
body: JSON.stringify(body),
}),
list: (projectId: string) =>
request<{ runs: FlowRunRow[] }>(
`/api/coder/runs?project_id=${encodeURIComponent(projectId)}`,
),
get: (runId: string) =>
request<{ run: FlowRunRow; steps: FlowStepRow[] }>(
`/api/coder/runs/${encodeURIComponent(runId)}`,
),
cancel: (runId: string) =>
request<{ cancelled: boolean }>(
`/api/coder/runs/${encodeURIComponent(runId)}/cancel`,
{ method: 'POST' },
),
},
agents: {
list: (projectId: string) =>
request<AgentsResponse>(`/api/projects/${projectId}/agents`),

View File

@@ -390,7 +390,8 @@ export type WorkspacePaneKind =
| 'empty'
| 'settings'
| 'markdown_artifact'
| 'html_artifact';
| 'html_artifact'
| 'orchestrator';
// Mixed tabs: a pane can hold tabs of different kinds (a BooChat tab next to a
// BooCode tab next to a Terminal tab). Each tab carries its own kind; the active
@@ -416,6 +417,45 @@ export interface HtmlArtifactState {
title: string;
}
// Orchestrator pane state — carries run identity for fetch-on-mount + reopen.
export interface OrchestratorState {
run_id: string;
flow_name: string;
band: 'small' | 'medium' | 'large';
}
// Orchestrator run API types (returned by GET /api/coder/runs/:id).
export interface FlowRunRow {
id: string;
project_id: string;
flow_name: string;
band: 'small' | 'medium' | 'large';
model: string;
status: 'running' | 'completed' | 'failed' | 'cancelled';
input: { question: string; band?: string; [key: string]: unknown };
report: string | null;
error: string | null;
created_at: string;
updated_at: string;
}
export interface FlowStepRow {
id: string;
run_id: string;
step_id: string;
kind: 'agent' | 'code';
agent: string | null;
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled';
task_id: string | null;
chat_id: string | null;
session_id: string | null;
input: string | null;
output: string | null;
error: string | null;
created_at: string;
updated_at: string;
}
export interface WorkspacePane {
id: string;
// For a tabbed pane (chat/coder/terminal) this mirrors the ACTIVE tab's kind,
@@ -433,6 +473,8 @@ export interface WorkspacePane {
// v1.14.x: populated only when kind === 'markdown_artifact' / 'html_artifact'.
markdown_artifact_state?: MarkdownArtifactState;
html_artifact_state?: HtmlArtifactState;
// orchestrator pane: populated only when kind === 'orchestrator'.
orchestrator_state?: OrchestratorState;
}
// Reopen LIFO stack entry. Shape unchanged from the prior module-level stack;
@@ -527,4 +569,27 @@ export type WsFrame =
status: 'working' | 'blocked' | 'idle' | 'error';
reason?: string;
at: string;
}
// orchestrator frames ([D-6]): run lifecycle. The per-agent content stream
// reuses existing delta/tool_call/message_complete frames keyed by chat_id.
| {
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;
}>;
}
| {
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;
};