feat(config): add councils schema and defaults

This commit is contained in:
William Valentin
2026-02-21 10:49:17 -08:00
parent bcb7e7b658
commit 8e89c7ff5d
4 changed files with 161 additions and 1 deletions
+41
View File
@@ -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 ...`)
+1 -1
View File
@@ -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';
+65
View File
@@ -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] },
+54
View File
@@ -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<typeof toolOverrideSchema>;
export type ToolsConfig = z.infer<typeof toolsSchema>;
export type SandboxConfig = z.infer<typeof sandboxSchema>;
export type AgentConfigEntry = z.infer<typeof agentConfigEntrySchema>;
export type CouncilsConfig = z.infer<typeof councilsSchema>;
export type RoutingConfig = z.infer<typeof routingSchema>;
export type IntentTargetType = z.infer<typeof intentTargetTypeSchema>;
export type IntentRuleConfig = z.infer<typeof intentRuleSchema>;