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

155
conductor/src/spine.ts Normal file
View File

@@ -0,0 +1,155 @@
/**
* Spine factory: turns a declarative `Spine` (a Han skill expressed as data)
* into a runnable `Flow`. The shape is the one ~most Han skills share —
*
* angle₁ ─┐
* angle₂ ─┼─▶ fold (code) ─▶ [synthesizer] ─▶ adversarial gate ─▶ render
* angle₃ ─┘ (fan-in) (optional) (validator)
*
* — so new skills are added as config (flows/*.ts), not new code. Band gating
* selects how many angles fan out (small = core only; large = all). Skills
* with a genuinely different shape (code-review's per-finding verify pipeline)
* get a bespoke Flow instead of a Spine.
*/
import type { Band, Flow, Spine, Step, StepContext } from './types.js';
import { produceContract, reviewContract, type Contract } from './contracts.js';
import { slugify } from './render.js';
const BAND_ORDER: Record<Band, number> = { small: 0, medium: 1, large: 2 };
export function readBand(input: StepContext['input']): Band {
const b = input.band;
return typeof b === 'string' && b in BAND_ORDER ? (b as Band) : 'small';
}
function bandAtLeast(ctx: StepContext, min: Band = 'small'): boolean {
return BAND_ORDER[readBand(ctx.input)] >= BAND_ORDER[min];
}
/** Appended to every worker when --fast is set — caps the slow tool loop. */
export function fastNote(ctx: StepContext): string {
if (!ctx.input.concise) return '';
return '\n\nFAST MODE — optimise for speed over exhaustiveness: limit external/tool calls to the few that matter, cite only decisive evidence, keep every section short, return quickly.';
}
interface ResolvedGate {
agent: string;
label: string;
task: (ctx: StepContext) => string;
}
/** The adversarial gate, built with the Han review checklists for the spine's contracts. */
function defaultValidator(contracts: Contract[]): ResolvedGate {
return {
agent: 'adversarial-validator',
label: 'Validation (adversarial-validator)',
task: (ctx) =>
[
`Adversarially validate the analysis below, for: "${String(ctx.input.question)}".`,
'Attack the evidence, the framing, the conclusion, and the integrity of how the evidence was gathered.',
'Emit findings as V1, V2, … each with a severity and whether it changes the conclusion.',
reviewContract(contracts).trim(),
'End with, in this order: a one-line VERDICT (does the conclusion survive?); a plain-language SUMMARY (23 sentences, no jargon or IDs); and a CONFIDENCE rating on its own line — `Confidence: High | Medium | Low`.',
'',
'----- ANALYSIS TO ATTACK -----',
ctx.results.synthesis ?? ctx.results.fold ?? '',
].join('\n'),
};
}
export function buildSpineFlow(spine: Spine): Flow {
const contracts: Contract[] = spine.contracts ?? ['evidence'];
const validator: ResolvedGate = spine.validator ?? defaultValidator(contracts);
const angleIds = spine.angles.map((a) => a.id);
const steps: Step[] = [];
for (const angle of spine.angles) {
steps.push({
id: angle.id,
kind: 'agent',
agent: angle.agent,
when: (ctx) => bandAtLeast(ctx, angle.minBand) && (angle.when ? angle.when(ctx) : true),
run: (ctx) => angle.task(ctx) + produceContract(contracts) + fastNote(ctx),
});
}
// Code fold: concatenate whatever angles produced (skipped angles absent).
steps.push({
id: 'fold',
kind: 'code',
deps: angleIds,
run: (ctx) => foldAngles(spine, ctx),
});
if (spine.synthesizer) {
steps.push({
id: 'synthesis',
kind: 'agent',
agent: spine.synthesizer.agent,
deps: ['fold'],
run: (ctx) => spine.synthesizer!.task(ctx) + produceContract(contracts) + fastNote(ctx),
});
}
steps.push({
id: 'validation',
kind: 'agent',
agent: validator.agent,
deps: [spine.synthesizer ? 'synthesis' : 'fold'],
run: (ctx) => validator.task(ctx) + fastNote(ctx),
});
return {
name: spine.name,
description: spine.description,
steps,
render: (ctx) => renderSpine(spine, validator, contracts, ctx),
output: (ctx) => `conductor-report-${spine.name}-${slugify(String(ctx.input.question))}.md`,
};
}
function foldAngles(spine: Spine, ctx: StepContext): string {
const blocks: string[] = [];
for (const angle of spine.angles) {
const out = ctx.results[angle.id];
if (out) blocks.push(`### ${angle.label}\n\n${out}`);
}
return blocks.join('\n\n') || '_(no angle produced output)_';
}
function renderSpine(spine: Spine, validator: ResolvedGate, contracts: Contract[], ctx: StepContext): string {
const question = String(ctx.input.question ?? '');
const model = process.env.CONDUCTOR_MODEL ?? 'llama-swap/qwen3.6-35b-a3b-mxfp4';
const band = readBand(ctx.input);
const chain: string[] = [];
const rules = [
contracts.includes('evidence') ? 'evidence-rule (trust classes · single-source web gate · no-evidence labeling)' : '',
contracts.includes('yagni') ? 'YAGNI gate' : '',
]
.filter(Boolean)
.join(' · ');
const parts: string[] = [
`# Conductor Report — ${spine.name}: ${question}`,
`> BooCode code conductor · band=${band}${ctx.input.concise ? ' · fast' : ''} · workers on \`${model}\`. Sequencing, fan-out, and fold are deterministic code; each agent ran as a bounded single-task worker. Han rules applied: ${rules}. The plain-language summary, the **Confidence** rating, and any \`## Deferred (YAGNI)\` items are in the **Validation** section — read it before trusting the conclusion.`,
];
for (const angle of spine.angles) {
if (ctx.results[angle.id]) {
parts.push(`## ${angle.label}\n\n${ctx.results[angle.id]}`);
chain.push(angle.agent);
}
}
if (spine.synthesizer && ctx.results.synthesis) {
parts.push(`## ${spine.synthesizer.label}\n\n${ctx.results.synthesis}`);
chain.push(spine.synthesizer.agent);
}
parts.push(`## ${validator.label}\n\n${ctx.results.validation ?? '_no validation output_'}`);
chain.push(validator.agent);
parts.push(
`---\n\n_Conducted by the code conductor: ${chain.join(' → ')}. Band=${band}. The conductor chose every step and passed full outputs forward; no model decided the sequence._`,
);
return parts.join('\n\n') + '\n';
}