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
+8
View File
@@ -424,6 +424,9 @@ describe('configSchema — councils', () => {
expect(result.councils.defaults.max_rounds).toBe(2);
expect(result.councils.defaults.top_ideas_for_bridge).toBe(3);
expect(result.councils.defaults.bridge_packet_max_chars).toBe(2500);
expect(result.councils.groups.D.model_tier).toBe('complex');
expect(result.councils.groups.P.model_tier).toBe('complex');
expect(result.councils.meta_model_tier).toBe('complex');
expect(result.councils.groups.D.novelty_bias).toBe('low');
expect(result.councils.groups.P.novelty_bias).toBe('high');
expect(result.councils.meta_arbiter_agent).toBe('council_meta_arbiter');
@@ -450,6 +453,7 @@ describe('configSchema — councils', () => {
D: {
arbiter_agent: 'd_arb',
freethinker_agent: 'd_ft',
model_tier: 'default',
group_prompt_prefix: 'd',
novelty_bias: 'low',
risk_tolerance: 'medium',
@@ -458,6 +462,7 @@ describe('configSchema — councils', () => {
P: {
arbiter_agent: 'p_arb',
freethinker_agent: 'p_ft',
model_tier: 'fast',
group_prompt_prefix: 'p',
novelty_bias: 'high',
risk_tolerance: 'high',
@@ -465,6 +470,7 @@ describe('configSchema — councils', () => {
},
},
meta_arbiter_agent: 'meta',
meta_model_tier: 'local',
},
});
@@ -472,6 +478,8 @@ describe('configSchema — councils', () => {
expect(result.councils.defaults.max_rounds).toBe(3);
expect(result.councils.groups.D.arbiter_agent).toBe('d_arb');
expect(result.councils.groups.P.freethinker_agent).toBe('p_ft');
expect(result.councils.groups.D.model_tier).toBe('default');
expect(result.councils.meta_model_tier).toBe('local');
expect(result.councils.strict_grounding).toBe(true);
expect(result.councils.meta_arbiter_agent).toBe('meta');
});
+4
View File
@@ -865,6 +865,7 @@ const councilsGroupConfigSchema = z.object({
freethinker_agent: z.string().min(1),
grounder_agent: z.string().min(1).optional(),
writer_agent: z.string().min(1).optional(),
model_tier: modelTierEnum.default('complex'),
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'),
@@ -890,6 +891,7 @@ const councilsSchema = z.object({
D: councilsGroupConfigSchema.default({
arbiter_agent: 'council_d_arbiter',
freethinker_agent: 'council_d_freethinker',
model_tier: 'complex',
group_prompt_prefix: 'Optimize for feasibility and speed-to-test. Prefer boring-but-true.',
novelty_bias: 'low',
risk_tolerance: 'low',
@@ -902,6 +904,7 @@ const councilsSchema = z.object({
P: councilsGroupConfigSchema.default({
arbiter_agent: 'council_p_arbiter',
freethinker_agent: 'council_p_freethinker',
model_tier: 'complex',
group_prompt_prefix: 'Optimize for reframing and non-obvious leverage. Weird is fine; label speculation.',
novelty_bias: 'high',
risk_tolerance: 'high',
@@ -913,6 +916,7 @@ const councilsSchema = z.object({
}),
}).default({}),
meta_arbiter_agent: z.string().min(1).default('council_meta_arbiter'),
meta_model_tier: modelTierEnum.default('complex'),
meta_writer_agent: z.string().min(1).optional(),
}).default({});
+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>;
+6 -2
View File
@@ -28,6 +28,7 @@ import type { SkillInstaller, SkillRegistry, SkillRegistryEntry, SkillRegistrySo
import { auditLogger } from '../audit/index.js';
import { getElevationStatusMessage, setElevationFromInput } from '../security/elevation.js';
import { dirname, resolve } from 'path';
import { loadCouncilScaffoldSafe } from '../councils/scaffold.js';
function buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
@@ -400,9 +401,11 @@ export function createMessageRouter(deps: {
} as AgentDelegateDeps));
if (deps.config.councils?.enabled) {
const scaffold = loadCouncilScaffoldSafe(deps.config.councils.scaffold_path);
effectiveToolRegistry.register(createCouncilRunTool({
registry: deps.agentConfigRegistry,
config: deps.config.councils as CouncilsConfig,
scaffold,
get orchestrator(): AgentOrchestrator {
if (!lazyOrchestrator) {
throw new Error('Agent orchestrator not yet initialized');
@@ -841,8 +844,9 @@ export function createMessageRouter(deps: {
const tool = createCouncilRunTool({
registry: deps.agentConfigRegistry,
orchestrator: agent,
config: deps.config.councils as CouncilsConfig,
});
config: deps.config.councils as CouncilsConfig,
scaffold: loadCouncilScaffoldSafe(deps.config.councils.scaffold_path),
});
const result = await tool.execute({ task: message });
if (!result.success) {
return `Council run failed: ${result.error ?? 'unknown error'}`;
+122
View File
@@ -10,6 +10,47 @@ export interface ConfigHandlerDeps {
modelRouter?: ModelRouter;
}
function ensureCouncilsConfig(config: Config): NonNullable<Config['councils']> {
config.councils ??= {
enabled: false,
defaults: {
max_rounds: 2,
ideas_per_round: 6,
top_ideas_for_bridge: 3,
bridge_packet_max_chars: 2500,
bridge_field_max_bullets: 6,
bridge_entry_max_chars: 300,
novelty_delta_threshold: 10,
repetition_threshold: 70,
},
strict_grounding: false,
strict_meta_validation: true,
groups: {
D: {
arbiter_agent: 'council_d_arbiter',
freethinker_agent: 'council_d_freethinker',
model_tier: 'complex',
group_prompt_prefix: 'Optimize for feasibility and speed-to-test. Prefer boring-but-true.',
novelty_bias: 'low',
risk_tolerance: 'low',
forbidden_approaches: ['moonshots', 'handwavy AI claims', 'unverified assumptions'],
},
P: {
arbiter_agent: 'council_p_arbiter',
freethinker_agent: 'council_p_freethinker',
model_tier: 'complex',
group_prompt_prefix: 'Optimize for reframing and non-obvious leverage. Weird is fine; label speculation.',
novelty_bias: 'high',
risk_tolerance: 'high',
forbidden_approaches: ['incremental tweaks', 'obvious best practices', 'purely conventional solutions'],
},
},
meta_arbiter_agent: 'council_meta_arbiter',
meta_model_tier: 'complex',
};
return config.councils;
}
/**
* Redact sensitive values from config before returning.
* Replaces API keys, tokens, passwords, and other credentials with "***".
@@ -316,6 +357,87 @@ const PATCHABLE_KEYS: Record<string, (config: Config, value: unknown) => boolean
config.agents.background_models.complex_reasoning.fallback_tier = value;
return true;
},
'councils.enabled': (config, value) => {
if (typeof value !== 'boolean') {return false;}
const councils = ensureCouncilsConfig(config);
councils.enabled = value;
return true;
},
'councils.defaults.max_rounds': (config, value) => {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > 6) {return false;}
const councils = ensureCouncilsConfig(config);
councils.defaults.max_rounds = Math.floor(value);
return true;
},
'councils.groups.D.model_tier': (config, value) => {
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
const councils = ensureCouncilsConfig(config);
councils.groups.D.model_tier = value;
return true;
},
'councils.groups.P.model_tier': (config, value) => {
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
const councils = ensureCouncilsConfig(config);
councils.groups.P.model_tier = value;
return true;
},
'councils.meta_model_tier': (config, value) => {
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
const councils = ensureCouncilsConfig(config);
councils.meta_model_tier = value;
return true;
},
'councils.groups.D.arbiter_agent': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
const councils = ensureCouncilsConfig(config);
councils.groups.D.arbiter_agent = value.trim();
return true;
},
'councils.groups.D.freethinker_agent': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
const councils = ensureCouncilsConfig(config);
councils.groups.D.freethinker_agent = value.trim();
return true;
},
'councils.groups.D.grounder_agent': (config, value) => {
if (typeof value !== 'string') {return false;}
const next = value.trim();
const councils = ensureCouncilsConfig(config);
councils.groups.D.grounder_agent = next.length > 0 ? next : undefined;
return true;
},
'councils.groups.P.arbiter_agent': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
const councils = ensureCouncilsConfig(config);
councils.groups.P.arbiter_agent = value.trim();
return true;
},
'councils.groups.P.freethinker_agent': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
const councils = ensureCouncilsConfig(config);
councils.groups.P.freethinker_agent = value.trim();
return true;
},
'councils.groups.P.grounder_agent': (config, value) => {
if (typeof value !== 'string') {return false;}
const next = value.trim();
const councils = ensureCouncilsConfig(config);
councils.groups.P.grounder_agent = next.length > 0 ? next : undefined;
return true;
},
'councils.meta_arbiter_agent': (config, value) => {
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
const councils = ensureCouncilsConfig(config);
councils.meta_arbiter_agent = value.trim();
return true;
},
'councils.scaffold_path': (config, value) => {
if (typeof value !== 'string') {return false;}
const next = value.trim();
const councils = ensureCouncilsConfig(config);
councils.scaffold_path = next.length > 0 ? next : undefined;
return true;
},
'models.default.provider': (config, value) => {
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
config.models.default.provider = value as ModelProvider;
+46
View File
@@ -1234,6 +1234,52 @@ describe('config handlers', () => {
expect(getPath(config, 'tts', 'enabled_channels')).toEqual(['telegram', 'discord']);
});
it('config.patch applies councils model and routing patches', async () => {
const config = makeConfig();
const handlers = createConfigHandlers({ config: asConfigValue(config) });
const req: GatewayRequest = {
id: 22,
method: 'config.patch',
params: {
patches: {
'councils.enabled': true,
'councils.defaults.max_rounds': 3,
'councils.groups.D.model_tier': 'complex',
'councils.groups.P.model_tier': 'fast',
'councils.meta_model_tier': 'default',
'councils.groups.D.arbiter_agent': 'd_arbiter',
'councils.groups.D.freethinker_agent': 'd_ft',
'councils.groups.P.arbiter_agent': 'p_arbiter',
'councils.groups.P.freethinker_agent': 'p_ft',
'councils.meta_arbiter_agent': 'meta_arbiter',
'councils.scaffold_path': 'docs/councils/ai-council-production-scaffold.json',
},
},
};
const result = await handlers['config.patch'](req) as GatewayResponse;
const r = result.result as { applied: string[]; rejected: string[]; persisted: boolean };
expect(r.applied).toEqual([
'councils.enabled',
'councils.defaults.max_rounds',
'councils.groups.D.model_tier',
'councils.groups.P.model_tier',
'councils.meta_model_tier',
'councils.groups.D.arbiter_agent',
'councils.groups.D.freethinker_agent',
'councils.groups.P.arbiter_agent',
'councils.groups.P.freethinker_agent',
'councils.meta_arbiter_agent',
'councils.scaffold_path',
]);
expect(r.rejected).toEqual([]);
expect(getPath(config, 'councils', 'enabled')).toBe(true);
expect(getPath(config, 'councils', 'defaults', 'max_rounds')).toBe(3);
expect(getPath(config, 'councils', 'groups', 'P', 'model_tier')).toBe('fast');
expect(getPath(config, 'councils', 'meta_model_tier')).toBe('default');
expect(getPath(config, 'councils', 'meta_arbiter_agent')).toBe('meta_arbiter');
});
it('config.patch rejects unknown keys', async () => {
const config = makeConfig();
const handlers = createConfigHandlers({ config: asConfigValue(config) });
+220
View File
@@ -14,6 +14,9 @@ let _assistantSaveState = null;
let _lastAssistantConfig = null;
let _assistantManualOverrides = new Set();
let _assistantModelDefaultsDraft = null;
let _lastCouncilTask = '';
let _lastCouncilResult = null;
let _lastCouncilError = null;
let _lastServices = [];
let _serviceConfigState = {
open: false,
@@ -216,6 +219,22 @@ function renderAssistantSaveState() {
return `<div id="ops-assistant-status" class="text-sm ${toneClass} mt-4">${escapeHtml(_assistantSaveState.message)} <span class="text-zinc-500">(at ${escapeHtml(at)})</span></div>`;
}
function extractCouncilResultFromOutput(output) {
if (typeof output !== 'string' || output.trim().length === 0) {
return null;
}
const marker = '{"pipeline_version"';
const idx = output.lastIndexOf(marker);
if (idx < 0) {
return null;
}
try {
return JSON.parse(output.slice(idx));
} catch {
return null;
}
}
// ── Initial full render ─────────────────────────────────────────
function renderSkeleton(el) {
@@ -690,6 +709,64 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
}
}
async function triggerCouncilRun(task, statusEl) {
if (!_dashboardClient) {return false;}
if (!task) {
if (statusEl) {
statusEl.textContent = 'Task is required.';
statusEl.className = 'text-sm text-red-500';
}
return false;
}
if (statusEl) {
statusEl.textContent = 'Running council...';
statusEl.className = 'text-sm text-zinc-500';
}
try {
const result = await _dashboardClient.call('tools.invoke', {
tool: 'council.run',
args: { task },
});
if (!result?.success) {
_lastCouncilError = result?.error ?? 'Council run failed.';
_lastCouncilResult = null;
if (statusEl) {
statusEl.textContent = _lastCouncilError;
statusEl.className = 'text-sm text-red-500';
}
return false;
}
const parsed = extractCouncilResultFromOutput(result.output);
if (!parsed) {
_lastCouncilError = 'Council run succeeded but output could not be parsed.';
_lastCouncilResult = null;
if (statusEl) {
statusEl.textContent = _lastCouncilError;
statusEl.className = 'text-sm text-amber-500';
}
return false;
}
_lastCouncilTask = task;
_lastCouncilResult = parsed;
_lastCouncilError = null;
if (statusEl) {
statusEl.textContent = `Council run complete: ${parsed.stop_snapshot?.stop_reason ?? 'ok'}`;
statusEl.className = 'text-sm text-green-500';
}
return true;
} catch (error) {
_lastCouncilError = `Council run error: ${error instanceof Error ? error.message : String(error)}`;
_lastCouncilResult = null;
if (statusEl) {
statusEl.textContent = _lastCouncilError;
statusEl.className = 'text-sm text-red-500';
}
return false;
}
}
function updateAssistantHealth(configData) {
const el = document.getElementById('ops-assistant-health');
if (!el) {return;}
@@ -723,6 +800,11 @@ function updateAssistantHealth(configData) {
const modelTier = _assistantModelDefaultsDraft?.primaryTier ?? configData?.agents?.primary_tier ?? 'default';
const delegation = configData?.agents?.delegation ?? {};
const backgroundModels = configData?.agents?.background_models ?? {};
const councils = configData?.councils ?? {};
const councilsDefaults = councils.defaults ?? {};
const councilsGroups = councils.groups ?? {};
const councilsD = councilsGroups.D ?? {};
const councilsP = councilsGroups.P ?? {};
const tiers = configData?.models ?? {};
const modelCatalog = configData?.__modelCatalog ?? [];
const providerList = modelCatalog.length > 0
@@ -763,6 +845,10 @@ function updateAssistantHealth(configData) {
{ key: 'tool_summarisation', label: 'Tool summarisation' },
{ key: 'complex_reasoning', label: 'Complex reasoning' },
];
const councilConversations = Array.isArray(_lastCouncilResult?.conversations) ? _lastCouncilResult.conversations : [];
const councilSummary = _lastCouncilResult?.stop_snapshot
? `Last run: ${_lastCouncilResult.stop_snapshot.stop_reason} (round ${_lastCouncilResult.stop_snapshot.round_reached})`
: (_lastCouncilError ? `Last run failed: ${_lastCouncilError}` : 'No council run yet in this dashboard session.');
el.innerHTML = `
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2 mb-4">
@@ -898,6 +984,97 @@ function updateAssistantHealth(configData) {
</button>
</div>
</div>
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
<div class="text-sm font-semibold text-zinc-50 mb-3">Councils</div>
<div class="text-sm text-zinc-500 mb-3">On-demand council orchestration settings and council role model tiers.</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
<label class="flex items-center gap-2 mt-5 md:mt-0">
<input id="assist-councils-enabled" type="checkbox" ${councils.enabled ? 'checked' : ''} />
<span class="text-xs text-zinc-400">Enable councils</span>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">D model tier</span>
<select id="assist-council-d-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none">
${tierOption(councilsD.model_tier ?? 'complex')}
</select>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">P model tier</span>
<select id="assist-council-p-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none">
${tierOption(councilsP.model_tier ?? 'complex')}
</select>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">Meta model tier</span>
<select id="assist-council-meta-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none">
${tierOption(councils.meta_model_tier ?? 'complex')}
</select>
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">D arbiter agent</span>
<input id="assist-council-d-arbiter" type="text" value="${escapeHtml(councilsD.arbiter_agent ?? 'council_d_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">D freethinker agent</span>
<input id="assist-council-d-freethinker" type="text" value="${escapeHtml(councilsD.freethinker_agent ?? 'council_d_freethinker')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">P arbiter agent</span>
<input id="assist-council-p-arbiter" type="text" value="${escapeHtml(councilsP.arbiter_agent ?? 'council_p_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">P freethinker agent</span>
<input id="assist-council-p-freethinker" type="text" value="${escapeHtml(councilsP.freethinker_agent ?? 'council_p_freethinker')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">Meta arbiter agent</span>
<input id="assist-council-meta-arbiter" type="text" value="${escapeHtml(councils.meta_arbiter_agent ?? 'council_meta_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">Scaffold path (optional)</span>
<input id="assist-council-scaffold" type="text" value="${escapeHtml(councils.scaffold_path ?? '')}" placeholder="docs/councils/ai-council-production-scaffold.json" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">Max rounds</span>
<input id="assist-council-max-rounds" type="number" min="1" max="6" value="${escapeHtml(String(councilsDefaults.max_rounds ?? 2))}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
</div>
<div class="flex flex-wrap gap-2">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-councils">
Save Councils
</button>
</div>
<div class="mt-4 p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
<div class="text-sm font-semibold text-zinc-50 mb-2">Council Conversations</div>
<div class="text-xs text-zinc-500 mb-3">${escapeHtml(councilSummary)}</div>
<div class="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-2 mb-3">
<input id="assist-council-task" type="text" value="${escapeHtml(_lastCouncilTask)}" placeholder="Run councils on demand: e.g. design a 2-week experiment plan..." class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="run-council">
Run Council
</button>
</div>
<div id="assist-council-status" class="text-sm text-zinc-500 mb-3"></div>
<div class="max-h-72 overflow-y-auto space-y-2">
${councilConversations.length === 0
? '<div class="text-sm text-zinc-500">No conversation log yet.</div>'
: councilConversations.map((turn, idx) => `
<details class="border border-zinc-800 rounded-md bg-zinc-900/70 p-2">
<summary class="cursor-pointer text-sm text-zinc-100">
#${idx + 1} ${escapeHtml(turn.call_id)} · ${escapeHtml(turn.agent)} @ ${escapeHtml(turn.tier)}
</summary>
<div class="mt-2 text-xs text-zinc-400">Prompt payload</div>
<pre class="text-xs text-zinc-300 whitespace-pre-wrap bg-zinc-950 border border-zinc-800 rounded p-2 mt-1">${escapeHtml(JSON.stringify(turn.prompt_payload, null, 2))}</pre>
<div class="mt-2 text-xs text-zinc-400">Response</div>
<pre class="text-xs text-zinc-300 whitespace-pre-wrap bg-zinc-950 border border-zinc-800 rounded p-2 mt-1">${escapeHtml(String(turn.response ?? ''))}</pre>
</details>
`).join('')}
</div>
</div>
</div>
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
<div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Activation Checklist</div>
<div class="space-y-1 mb-4">
@@ -1064,6 +1241,49 @@ function updateAssistantHealth(configData) {
}
patches[`agents.background_models.${task}.fallback_tier`] = fallbackTier;
}
} else if (action === 'save-councils') {
const enabled = Boolean(el.querySelector('#assist-councils-enabled')?.checked);
const dTier = el.querySelector('#assist-council-d-tier')?.value ?? 'complex';
const pTier = el.querySelector('#assist-council-p-tier')?.value ?? 'complex';
const metaTier = el.querySelector('#assist-council-meta-tier')?.value ?? 'complex';
const dArbiter = (el.querySelector('#assist-council-d-arbiter')?.value ?? '').trim();
const dFreethinker = (el.querySelector('#assist-council-d-freethinker')?.value ?? '').trim();
const pArbiter = (el.querySelector('#assist-council-p-arbiter')?.value ?? '').trim();
const pFreethinker = (el.querySelector('#assist-council-p-freethinker')?.value ?? '').trim();
const metaArbiter = (el.querySelector('#assist-council-meta-arbiter')?.value ?? '').trim();
const scaffoldPath = (el.querySelector('#assist-council-scaffold')?.value ?? '').trim();
const maxRoundsRaw = Number(el.querySelector('#assist-council-max-rounds')?.value ?? 2);
const maxRounds = Number.isFinite(maxRoundsRaw) ? Math.max(1, Math.min(6, Math.floor(maxRoundsRaw))) : 2;
if (!dArbiter || !dFreethinker || !pArbiter || !pFreethinker || !metaArbiter) {
if (statusEl) {
statusEl.textContent = 'All council agent names are required.';
statusEl.className = 'text-sm text-red-500';
}
return;
}
patches = {
'councils.enabled': enabled,
'councils.defaults.max_rounds': maxRounds,
'councils.groups.D.model_tier': dTier,
'councils.groups.P.model_tier': pTier,
'councils.meta_model_tier': metaTier,
'councils.groups.D.arbiter_agent': dArbiter,
'councils.groups.D.freethinker_agent': dFreethinker,
'councils.groups.P.arbiter_agent': pArbiter,
'councils.groups.P.freethinker_agent': pFreethinker,
'councils.meta_arbiter_agent': metaArbiter,
'councils.scaffold_path': scaffoldPath,
};
} else if (action === 'run-council') {
const councilTask = (el.querySelector('#assist-council-task')?.value ?? '').trim();
const councilStatusEl = el.querySelector('#assist-council-status');
const ok = await triggerCouncilRun(councilTask, councilStatusEl);
if (ok && _lastAssistantConfig) {
updateAssistantHealth(_lastAssistantConfig);
}
return;
}
if (!patches) {return;}
const patchResult = await applyAssistantPatch(patches, statusEl);
+17 -1
View File
@@ -1,6 +1,7 @@
import type { AgentConfigRegistry } from '../../agents/registry.js';
import type { Tool, ToolResult } from '../types.js';
import { CouncilsOrchestrator, type CouncilsConfig } from '../../councils/orchestrator.js';
import type { CouncilScaffold } from '../../councils/scaffold.js';
import { councilRunInputSchema } from '../../councils/types.js';
interface DelegateRunner {
@@ -20,6 +21,7 @@ export interface CouncilRunDeps {
registry: AgentConfigRegistry;
orchestrator: DelegateRunner;
config: CouncilsConfig;
scaffold?: CouncilScaffold;
}
export function createCouncilRunTool(deps: CouncilRunDeps): Tool {
@@ -48,6 +50,7 @@ export function createCouncilRunTool(deps: CouncilRunDeps): Tool {
registry: deps.registry,
orchestrator: deps.orchestrator,
config: deps.config,
scaffold: deps.scaffold,
});
const result = await runner.run(args);
@@ -64,11 +67,24 @@ export function createCouncilRunTool(deps: CouncilRunDeps): Tool {
`- Secondary: ${result.meta.selected_secondary.join(', ') || 'none'}`,
`- Open questions: ${result.meta.open_questions.length}`,
`- Next experiments: ${result.meta.next_experiments.length}`,
'',
`Agent conversations: ${result.conversations.length}`,
];
const conversationLog = result.conversations
.map((turn, index) => {
const prompt = JSON.stringify(turn.prompt_payload);
return [
`#${index + 1} ${turn.call_id} [${turn.agent} @ ${turn.tier}]`,
`Prompt payload: ${prompt}`,
`Response: ${turn.response}`,
].join('\n');
})
.join('\n\n');
return {
success: true,
output: `${lines.join('\n')}\n\n${JSON.stringify(result)}`,
output: `${lines.join('\n')}\n\n${conversationLog}\n\n${JSON.stringify(result)}`,
};
} catch (error) {
return {