feat(councils-ui): add on-demand council conversations panel and model config controls

This commit is contained in:
William Valentin
2026-02-21 11:26:04 -08:00
parent cfd7fa6fd0
commit 7c121b82c6
11 changed files with 481 additions and 4 deletions
+34 -1
View File
@@ -22,6 +22,7 @@ import {
type CouncilGroup,
type CouncilRunInput,
type CouncilRunResult,
type CouncilConversationTurn,
type CouncilTraceEvent,
type IdeaAssessment,
type IdeaCard,
@@ -61,6 +62,7 @@ export interface CouncilsConfig {
P: CouncilGroupConfig;
};
meta_arbiter_agent: string;
meta_model_tier?: ModelTier;
meta_writer_agent?: string;
scaffold_path?: string;
}
@@ -70,6 +72,7 @@ interface CouncilGroupConfig {
freethinker_agent: string;
grounder_agent?: string;
writer_agent?: string;
model_tier?: ModelTier;
group_prompt_prefix: string;
novelty_bias: 'low' | 'medium' | 'high';
risk_tolerance: 'low' | 'medium' | 'high';
@@ -230,6 +233,7 @@ export class CouncilsOrchestrator {
private readonly _config: CouncilsConfig;
private readonly _scaffold?: CouncilScaffold;
private readonly _trace: CouncilTraceEvent[] = [];
private readonly _conversations: CouncilConversationTurn[] = [];
constructor(deps: {
registry: AgentConfigRegistry;
@@ -250,6 +254,7 @@ export class CouncilsOrchestrator {
}
this._trace.length = 0;
this._conversations.length = 0;
const inputHash = hashCanonical(input);
const maxRounds = input.max_rounds ?? this._config.defaults.max_rounds;
@@ -370,6 +375,7 @@ export class CouncilsOrchestrator {
meta,
stop_snapshot: stopSnapshot,
trace: this.getSortedTrace(),
conversations: this.getSortedConversations(),
});
return result;
@@ -395,6 +401,7 @@ export class CouncilsOrchestrator {
promptPayload: unknown;
modeDirective: string;
scaffoldPrompt?: string;
tierOverride?: ModelTier;
maxTokens?: number;
}): Promise<AgentCallResult> {
const agent = this.getAgent(opts.agentName);
@@ -404,7 +411,7 @@ export class CouncilsOrchestrator {
const systemPrompt = `${scaffoldPrompt}${agent.systemPrompt}\n\n${opts.modeDirective}`;
const result = await this._delegateRunner.delegate({
tier: agent.tier,
tier: opts.tierOverride ?? agent.tier,
systemPrompt,
message,
maxTokens: opts.maxTokens ?? 4096,
@@ -421,6 +428,18 @@ export class CouncilsOrchestrator {
artifact_hash: hashCanonical(result.content),
token_usage: result.usage,
}));
this._conversations.push({
schema_version: COUNCIL_SCHEMA_VERSION,
phase_index: opts.phaseIndex,
call_id: opts.callId,
agent: opts.agentName,
tier: opts.tierOverride ?? agent.tier,
group: opts.group,
round: opts.round,
prompt_payload: opts.promptPayload as Record<string, unknown>,
response: result.content,
usage: result.usage,
});
return result;
}
@@ -437,6 +456,7 @@ export class CouncilsOrchestrator {
previousBrief?: CouncilBrief,
): Promise<GroupRoundResult> {
const groupConfig = this._config.groups[group];
const groupTier = groupConfig.model_tier;
const phaseBase = round * 10 + (group === 'D' ? 1 : 2);
const ideationPayload = {
@@ -462,6 +482,7 @@ export class CouncilsOrchestrator {
promptPayload: ideationPayload,
modeDirective: 'Return JSON only: {"ideas":[IdeaContent,...]}. Do not include IDs. No prose.',
scaffoldPrompt: this._scaffold?.prompts[group].free_thinker,
tierOverride: groupTier,
});
const ideaOutput = parseJsonWithRepair(ideation.content, (value) => ideationOutputSchema.parse(value));
@@ -494,6 +515,7 @@ export class CouncilsOrchestrator {
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,
tierOverride: groupTier,
});
const assessmentOutput = parseJsonWithRepair(assessmentRaw.content, (value) => assessmentOutputSchema.parse(value));
@@ -550,6 +572,7 @@ export class CouncilsOrchestrator {
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,
tierOverride: groupTier,
});
grounding = parseJsonWithRepair(groundingRaw.content, (value) => groundingOutputSchema.parse(value));
} catch {
@@ -701,6 +724,7 @@ export class CouncilsOrchestrator {
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,
tierOverride: this._config.meta_model_tier,
});
return parseJsonWithRepair(metaRaw.content, (value) => metaSelectionSchema.parse(value));
@@ -743,6 +767,15 @@ export class CouncilsOrchestrator {
validation_failure: normalizeOptional(event.validation_failure),
}));
}
private getSortedConversations(): CouncilConversationTurn[] {
return [...this._conversations]
.sort((a, b) => a.phase_index - b.phase_index || a.call_id.localeCompare(b.call_id))
.map((entry) => ({
...entry,
prompt_payload: JSON.parse(canonicalStringify(entry.prompt_payload)) as Record<string, unknown>,
}));
}
}
export function createCouncilsOrchestrator(deps: {
+18
View File
@@ -170,6 +170,22 @@ export const councilTraceEventSchema = z.object({
validation_failure: validationFailureReasonSchema.optional(),
}).strict();
export const councilConversationTurnSchema = z.object({
schema_version: schemaVersionField,
phase_index: z.number().int().min(1),
call_id: z.string().min(1),
agent: z.string().min(1),
tier: z.enum(['fast', 'default', 'complex', 'local']),
group: councilGroupSchema.optional(),
round: z.number().int().min(1).optional(),
prompt_payload: z.record(z.string(), z.unknown()),
response: z.string(),
usage: z.object({
inputTokens: z.number().int().min(0),
outputTokens: z.number().int().min(0),
}).strict(),
}).strict();
export const stopSnapshotSchema = z.object({
stop_reason: stopReasonSchema,
round_reached: z.number().int().min(1),
@@ -193,6 +209,7 @@ export const councilRunResultSchema = z.object({
meta: metaSelectionSchema,
stop_snapshot: stopSnapshotSchema,
trace: z.array(councilTraceEventSchema),
conversations: z.array(councilConversationTurnSchema),
}).strict();
export const councilRunInputSchema = z.object({
@@ -246,4 +263,5 @@ export type CouncilDiff = z.infer<typeof councilDiffSchema>;
export type CouncilRunInput = z.infer<typeof councilRunInputSchema>;
export type CouncilRunResult = z.infer<typeof councilRunResultSchema>;
export type CouncilTraceEvent = z.infer<typeof councilTraceEventSchema>;
export type CouncilConversationTurn = z.infer<typeof councilConversationTurnSchema>;
export type MetaSelection = z.infer<typeof metaSelectionSchema>;