diff --git a/config/default.yaml b/config/default.yaml index 21eb6fb..33ec925 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -612,6 +612,7 @@ automation: # 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 @@ -622,6 +623,7 @@ automation: # 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 @@ -630,6 +632,8 @@ automation: # - obvious best practices # - purely conventional solutions # meta_arbiter_agent: council_meta_arbiter +# meta_model_tier: complex +# # scaffold_path: docs/councils/ai-council-production-scaffold.json # Optional: explicit intent rules for agent routing. # If enabled, these rules are evaluated before default sender/channel routing. diff --git a/docs/api/COUNCILS.md b/docs/api/COUNCILS.md index c470666..214f7bf 100644 --- a/docs/api/COUNCILS.md +++ b/docs/api/COUNCILS.md @@ -19,6 +19,7 @@ Flynn supports a deterministic dual-council orchestration pipeline (`council.run - `CouncilDiff` (code-computed deterministic diff) - `MetaSelection` (JSON-only selection/merge record) - `CouncilRunResult` (`pipeline_version`, hashes, trace, stop snapshot) +- `conversations` (per-agent call log with prompt payload + raw response) ## Tool Interface @@ -42,6 +43,7 @@ Flynn supports a deterministic dual-council orchestration pipeline (`council.run - `prompt_payload_hash` is computed from canonical JSON payload passed to model. - `artifact_hash` is computed from canonical validated artifact JSON. - Trace ordering is deterministic (`phase_index`, then `call_id`), independent of async completion order. +- Conversation logs are sorted by (`phase_index`, `call_id`) for stable Web UI display. ## Safety Rules diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index a706512..7d9e6b1 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -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'); }); diff --git a/src/config/schema.ts b/src/config/schema.ts index cf3bff5..0a55792 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -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({}); diff --git a/src/councils/orchestrator.ts b/src/councils/orchestrator.ts index bb2ad92..9bb4cbf 100644 --- a/src/councils/orchestrator.ts +++ b/src/councils/orchestrator.ts @@ -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 { 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, + response: result.content, + usage: result.usage, + }); return result; } @@ -437,6 +456,7 @@ export class CouncilsOrchestrator { previousBrief?: CouncilBrief, ): Promise { 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, + })); + } } export function createCouncilsOrchestrator(deps: { diff --git a/src/councils/types.ts b/src/councils/types.ts index b886edb..4d542a1 100644 --- a/src/councils/types.ts +++ b/src/councils/types.ts @@ -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; export type CouncilRunInput = z.infer; export type CouncilRunResult = z.infer; export type CouncilTraceEvent = z.infer; +export type CouncilConversationTurn = z.infer; export type MetaSelection = z.infer; diff --git a/src/daemon/routing.ts b/src/daemon/routing.ts index 832e051..d6f0d45 100644 --- a/src/daemon/routing.ts +++ b/src/daemon/routing.ts @@ -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> { const providerConfigs: Partial> = {}; @@ -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'}`; diff --git a/src/gateway/handlers/config.ts b/src/gateway/handlers/config.ts index 1a15e6d..2428961 100644 --- a/src/gateway/handlers/config.ts +++ b/src/gateway/handlers/config.ts @@ -10,6 +10,47 @@ export interface ConfigHandlerDeps { modelRouter?: ModelRouter; } +function ensureCouncilsConfig(config: Config): NonNullable { + 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 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; diff --git a/src/gateway/handlers/handlers.test.ts b/src/gateway/handlers/handlers.test.ts index 9919224..d9f3da2 100644 --- a/src/gateway/handlers/handlers.test.ts +++ b/src/gateway/handlers/handlers.test.ts @@ -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) }); diff --git a/src/gateway/ui/pages/dashboard.js b/src/gateway/ui/pages/dashboard.js index aee7155..dc90339 100644 --- a/src/gateway/ui/pages/dashboard.js +++ b/src/gateway/ui/pages/dashboard.js @@ -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 `
${escapeHtml(_assistantSaveState.message)} (at ${escapeHtml(at)})
`; } +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 = `
@@ -898,6 +984,97 @@ function updateAssistantHealth(configData) {
+
+
Councils
+
On-demand council orchestration settings and council role model tiers.
+
+ + + + +
+
+ + + + + + +
+
+ +
+
+ +
+
+
Council Conversations
+
${escapeHtml(councilSummary)}
+
+ + +
+
+
+ ${councilConversations.length === 0 + ? '
No conversation log yet.
' + : councilConversations.map((turn, idx) => ` +
+ + #${idx + 1} ${escapeHtml(turn.call_id)} · ${escapeHtml(turn.agent)} @ ${escapeHtml(turn.tier)} + +
Prompt payload
+
${escapeHtml(JSON.stringify(turn.prompt_payload, null, 2))}
+
Response
+
${escapeHtml(String(turn.response ?? ''))}
+
+ `).join('')} +
+
+
Assistant Activation Checklist
@@ -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); diff --git a/src/tools/builtin/council-run.ts b/src/tools/builtin/council-run.ts index e697622..3e21515 100644 --- a/src/tools/builtin/council-run.ts +++ b/src/tools/builtin/council-run.ts @@ -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 {