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