From 8e89c7ff5d4a51104857f24d0a4a324afb9f3b35 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 21 Feb 2026 10:49:17 -0800 Subject: [PATCH] feat(config): add councils schema and defaults --- config/default.yaml | 41 ++++++++++++++++++++++++ src/config/index.ts | 2 +- src/config/schema.test.ts | 65 +++++++++++++++++++++++++++++++++++++++ src/config/schema.ts | 54 ++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) diff --git a/config/default.yaml b/config/default.yaml index 8d7d760..21eb6fb 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -590,6 +590,47 @@ automation: # Be concise and match the operator's tone. Skip marketing emails. # Never send messages without explicit instruction — draft only. +# ── Councils Pipeline ──────────────────────────────────────────────── +# Deterministic dual-council orchestration (D/P groups) with bridge-only +# exchange and a deterministic meta merge. Requires matching agent_configs +# for the configured role names below. +# +# 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 +# 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 +# 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 + # Optional: explicit intent rules for agent routing. # If enabled, these rules are evaluated before default sender/channel routing. # The research agent can already be auto-routed by prefix (`research ...`, `look up ...`) diff --git a/src/config/index.ts b/src/config/index.ts index d369e73..0bde70f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,3 +1,3 @@ export { loadConfig, deepMerge } from './loader.js'; export { persistConfig } from './persistence.js'; -export { configSchema, MODEL_PROVIDERS, type ModelProvider, type Config, type TelegramConfig, type ModelConfig, type CronJobConfig, type AgentsConfig, type CompactionConfig, type ToolProfile, type ToolOverrideConfig, type ToolsConfig, type SandboxConfig, type AgentConfigEntry, type RoutingConfig, type ServerConfig, type BackupConfig, type K8sConfig, type TtsConfig } from './schema.js'; +export { configSchema, MODEL_PROVIDERS, type ModelProvider, type Config, type TelegramConfig, type ModelConfig, type CronJobConfig, type AgentsConfig, type CompactionConfig, type ToolProfile, type ToolOverrideConfig, type ToolsConfig, type SandboxConfig, type AgentConfigEntry, type CouncilsConfig, type RoutingConfig, type ServerConfig, type BackupConfig, type K8sConfig, type TtsConfig } from './schema.js'; diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 4c3a38c..a706512 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -412,6 +412,71 @@ describe('configSchema — agent_configs', () => { }); }); +describe('configSchema — councils', () => { + const minimalConfig = { + telegram: { bot_token: 'test', allowed_chat_ids: [1] }, + models: { default: { provider: 'anthropic', model: 'claude-3' } }, + }; + + it('defaults councils config with deterministic caps and group presets', () => { + const result = configSchema.parse(minimalConfig); + expect(result.councils.enabled).toBe(false); + 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.novelty_bias).toBe('low'); + expect(result.councils.groups.P.novelty_bias).toBe('high'); + expect(result.councils.meta_arbiter_agent).toBe('council_meta_arbiter'); + }); + + it('accepts explicit councils overrides', () => { + const result = configSchema.parse({ + ...minimalConfig, + councils: { + enabled: true, + defaults: { + max_rounds: 3, + ideas_per_round: 5, + top_ideas_for_bridge: 2, + bridge_packet_max_chars: 1800, + bridge_field_max_bullets: 4, + bridge_entry_max_chars: 200, + novelty_delta_threshold: 5, + repetition_threshold: 80, + }, + strict_grounding: true, + strict_meta_validation: true, + groups: { + D: { + arbiter_agent: 'd_arb', + freethinker_agent: 'd_ft', + group_prompt_prefix: 'd', + novelty_bias: 'low', + risk_tolerance: 'medium', + forbidden_approaches: ['x'], + }, + P: { + arbiter_agent: 'p_arb', + freethinker_agent: 'p_ft', + group_prompt_prefix: 'p', + novelty_bias: 'high', + risk_tolerance: 'high', + forbidden_approaches: ['y'], + }, + }, + meta_arbiter_agent: 'meta', + }, + }); + + expect(result.councils.enabled).toBe(true); + 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.strict_grounding).toBe(true); + expect(result.councils.meta_arbiter_agent).toBe('meta'); + }); +}); + describe('configSchema — backends', () => { const minimalConfig = { telegram: { bot_token: 'test', allowed_chat_ids: [1] }, diff --git a/src/config/schema.ts b/src/config/schema.ts index a02c671..5e5b2ae 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -860,6 +860,58 @@ const agentConfigEntrySchema = z.object({ const agentConfigsSchema = z.record(z.string(), agentConfigEntrySchema).default({}); +const councilsGroupConfigSchema = z.object({ + arbiter_agent: z.string().min(1), + freethinker_agent: z.string().min(1), + 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'), + forbidden_approaches: z.array(z.string().min(1)).default([]), +}); + +const councilsSchema = z.object({ + enabled: z.boolean().default(false), + defaults: z.object({ + max_rounds: z.number().int().min(1).max(6).default(2), + ideas_per_round: z.number().int().min(1).max(20).default(6), + top_ideas_for_bridge: z.number().int().min(1).max(10).default(3), + bridge_packet_max_chars: z.number().int().min(500).max(20_000).default(2500), + bridge_field_max_bullets: z.number().int().min(1).max(20).default(6), + bridge_entry_max_chars: z.number().int().min(20).max(2000).default(300), + novelty_delta_threshold: z.number().int().min(0).max(100).default(10), + repetition_threshold: z.number().int().min(0).max(100).default(70), + }).default({}), + strict_grounding: z.boolean().default(false), + strict_meta_validation: z.boolean().default(true), + groups: z.object({ + D: councilsGroupConfigSchema.default({ + arbiter_agent: 'council_d_arbiter', + freethinker_agent: 'council_d_freethinker', + 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: councilsGroupConfigSchema.default({ + arbiter_agent: 'council_p_arbiter', + freethinker_agent: 'council_p_freethinker', + 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', + ], + }), + }).default({}), + meta_arbiter_agent: z.string().min(1).default('council_meta_arbiter'), +}).default({}); + const routingSchema = z.object({ default_agent: z.string().optional(), channels: z.record(z.string(), z.string()).default({}), @@ -1005,6 +1057,7 @@ export const configSchema = z.object({ tools: toolsSchema, sandbox: sandboxSchema, agent_configs: agentConfigsSchema, + councils: councilsSchema, routing: routingSchema, intents: intentsSchema, routing_policy: routingPolicySchema, @@ -1046,6 +1099,7 @@ export type ToolOverrideConfig = z.infer; export type ToolsConfig = z.infer; export type SandboxConfig = z.infer; export type AgentConfigEntry = z.infer; +export type CouncilsConfig = z.infer; export type RoutingConfig = z.infer; export type IntentTargetType = z.infer; export type IntentRuleConfig = z.infer;