feat(councils): add scaffold prompt hooks and checked-in scaffold files

This commit is contained in:
William Valentin
2026-02-21 11:01:12 -08:00
parent c322e3ab51
commit cfd7fa6fd0
5 changed files with 613 additions and 2 deletions
+4
View File
@@ -863,6 +863,8 @@ const agentConfigsSchema = z.record(z.string(), agentConfigEntrySchema).default(
const councilsGroupConfigSchema = z.object({
arbiter_agent: z.string().min(1),
freethinker_agent: z.string().min(1),
grounder_agent: z.string().min(1).optional(),
writer_agent: z.string().min(1).optional(),
group_prompt_prefix: z.string().min(1),
novelty_bias: z.enum(['low', 'medium', 'high']).default('medium'),
risk_tolerance: z.enum(['low', 'medium', 'high']).default('medium'),
@@ -883,6 +885,7 @@ const councilsSchema = z.object({
}).default({}),
strict_grounding: z.boolean().default(false),
strict_meta_validation: z.boolean().default(true),
scaffold_path: z.string().optional(),
groups: z.object({
D: councilsGroupConfigSchema.default({
arbiter_agent: 'council_d_arbiter',
@@ -910,6 +913,7 @@ const councilsSchema = z.object({
}),
}).default({}),
meta_arbiter_agent: z.string().min(1).default('council_meta_arbiter'),
meta_writer_agent: z.string().min(1).optional(),
}).default({});
const routingSchema = z.object({
+17 -2
View File
@@ -2,6 +2,7 @@ import type { AgentConfigRegistry } from '../agents/registry.js';
import type { AgentOrchestrator } from '../backends/native/orchestrator.js';
import type { ModelTier } from '../models/router.js';
import type { TokenUsage } from '../models/types.js';
import type { CouncilScaffold } from './scaffold.js';
import {
COUNCIL_PIPELINE_VERSION,
COUNCIL_SCHEMA_VERSION,
@@ -60,11 +61,15 @@ export interface CouncilsConfig {
P: CouncilGroupConfig;
};
meta_arbiter_agent: string;
meta_writer_agent?: string;
scaffold_path?: string;
}
interface CouncilGroupConfig {
arbiter_agent: string;
freethinker_agent: string;
grounder_agent?: string;
writer_agent?: string;
group_prompt_prefix: string;
novelty_bias: 'low' | 'medium' | 'high';
risk_tolerance: 'low' | 'medium' | 'high';
@@ -223,16 +228,19 @@ export class CouncilsOrchestrator {
private readonly _registry: AgentConfigRegistry;
private readonly _delegateRunner: DelegateRunner;
private readonly _config: CouncilsConfig;
private readonly _scaffold?: CouncilScaffold;
private readonly _trace: CouncilTraceEvent[] = [];
constructor(deps: {
registry: AgentConfigRegistry;
orchestrator: DelegateRunner;
config: CouncilsConfig;
scaffold?: CouncilScaffold;
}) {
this._registry = deps.registry;
this._delegateRunner = deps.orchestrator;
this._config = deps.config;
this._scaffold = deps.scaffold;
}
async run(rawInput: unknown): Promise<CouncilRunResult> {
@@ -386,12 +394,14 @@ export class CouncilsOrchestrator {
round?: number;
promptPayload: unknown;
modeDirective: string;
scaffoldPrompt?: string;
maxTokens?: number;
}): Promise<AgentCallResult> {
const agent = this.getAgent(opts.agentName);
const message = canonicalStringify(opts.promptPayload);
const promptHash = hashCanonical(opts.promptPayload);
const systemPrompt = `${agent.systemPrompt}\n\n${opts.modeDirective}`;
const scaffoldPrompt = opts.scaffoldPrompt ? `${opts.scaffoldPrompt}\n\n` : '';
const systemPrompt = `${scaffoldPrompt}${agent.systemPrompt}\n\n${opts.modeDirective}`;
const result = await this._delegateRunner.delegate({
tier: agent.tier,
@@ -451,6 +461,7 @@ export class CouncilsOrchestrator {
round,
promptPayload: ideationPayload,
modeDirective: 'Return JSON only: {"ideas":[IdeaContent,...]}. Do not include IDs. No prose.',
scaffoldPrompt: this._scaffold?.prompts[group].free_thinker,
});
const ideaOutput = parseJsonWithRepair(ideation.content, (value) => ideationOutputSchema.parse(value));
@@ -482,6 +493,7 @@ export class CouncilsOrchestrator {
promptPayload: assessmentPayload,
modeDirective:
'Return JSON only. Assess provided idea IDs only. No new IDs. Include convergence_signal/novelty_score/repetition_rate.',
scaffoldPrompt: this._scaffold?.prompts[group].arbiter,
});
const assessmentOutput = parseJsonWithRepair(assessmentRaw.content, (value) => assessmentOutputSchema.parse(value));
@@ -524,11 +536,12 @@ export class CouncilsOrchestrator {
constraints: input.constraints,
};
const groundingAgent = groupConfig.grounder_agent ?? groupConfig.freethinker_agent;
let groundingFailures = 0;
let grounding = { grounded: [] as Array<{ idea_id: string; mve: string; constraints: string[]; falsifiability_checks: string[] }> };
try {
const groundingRaw = await this.callAgent({
agentName: groupConfig.freethinker_agent,
agentName: groundingAgent,
callId: `${group}.r${round}.ft.ground`,
phaseIndex: phaseBase + 2,
group,
@@ -536,6 +549,7 @@ export class CouncilsOrchestrator {
promptPayload: groundingPayload,
modeDirective:
'Grounder mode. Return JSON only: {"grounded":[{"idea_id", "mve", "constraints", "falsifiability_checks"}]}. No prose.',
scaffoldPrompt: this._scaffold?.prompts[group].grounder ?? this._scaffold?.prompts[group].free_thinker,
});
grounding = parseJsonWithRepair(groundingRaw.content, (value) => groundingOutputSchema.parse(value));
} catch {
@@ -686,6 +700,7 @@ export class CouncilsOrchestrator {
promptPayload: payload,
modeDirective:
'Return JSON only following schema with selected_primary/selected_secondary/merges/rejections/open_questions/next_experiments. Use only known idea IDs.',
scaffoldPrompt: this._scaffold?.prompts.meta_arbiter,
});
return parseJsonWithRepair(metaRaw.content, (value) => metaSelectionSchema.parse(value));
+37
View File
@@ -0,0 +1,37 @@
import { readFileSync } from 'fs';
import { z } from 'zod';
const councilPromptSetSchema = z.object({
arbiter: z.string().min(1),
free_thinker: z.string().min(1),
grounder: z.string().min(1).optional(),
writer: z.string().min(1).optional(),
}).strict();
export const councilScaffoldSchema = z.object({
generated_at: z.string().optional(),
version: z.string().optional(),
prompts: z.object({
D: councilPromptSetSchema,
P: councilPromptSetSchema,
meta_arbiter: z.string().min(1),
}).strict(),
}).strict();
export type CouncilScaffold = z.infer<typeof councilScaffoldSchema>;
export function loadCouncilScaffold(path: string): CouncilScaffold {
const raw = readFileSync(path, 'utf8');
return councilScaffoldSchema.parse(JSON.parse(raw));
}
export function loadCouncilScaffoldSafe(path?: string): CouncilScaffold | undefined {
if (!path) {
return undefined;
}
try {
return loadCouncilScaffold(path);
} catch {
return undefined;
}
}