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
+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);