Files
boocode/apps/coder/src/conductor/spine.ts
indifferentketchup 1937af8df9 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>
2026-06-03 15:22:48 +00:00

157 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 ?? '');
// model is injected by the flow-runner from flow_runs.model — no env var fallback
const model = ctx.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';
}