diff --git a/src/agents/builtin-agents.ts b/src/agents/builtin-agents.ts index 95f360b3..2f2cd0c7 100644 --- a/src/agents/builtin-agents.ts +++ b/src/agents/builtin-agents.ts @@ -19,6 +19,7 @@ import { fetchAvailableModels, readConnectedProvidersCache, readProviderModelsCache, + log, } from "../shared" import { CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants" import { mergeCategories } from "../shared/merge-categories" @@ -201,13 +202,13 @@ export async function createBuiltinAgents( result["atlas"] = atlasConfig } - if (councilConfig && councilConfig.members.length >= 2) { + if (councilConfig && councilConfig.members.length >= 2 && result["athena"]) { const { agents: councilAgents, registeredKeys } = registerCouncilMemberAgents(councilConfig) for (const [key, config] of Object.entries(councilAgents)) { result[key] = config } - if (result["athena"] && registeredKeys.length > 0) { + if (registeredKeys.length > 0) { const memberList = registeredKeys.map((key) => `- "${key}"`).join("\n") const councilTaskInstructions = `\n\n## Registered Council Members (use these as subagent_type in task calls)\n\n${memberList}` result["athena"] = { @@ -215,6 +216,8 @@ export async function createBuiltinAgents( prompt: (result["athena"].prompt ?? "") + councilTaskInstructions, } } + } else if (councilConfig && councilConfig.members.length >= 2 && !result["athena"]) { + log("[builtin-agents] Skipping council member registration — Athena is disabled") } return result diff --git a/src/agents/builtin-agents/council-member-agents.test.ts b/src/agents/builtin-agents/council-member-agents.test.ts new file mode 100644 index 00000000..6f4e30f7 --- /dev/null +++ b/src/agents/builtin-agents/council-member-agents.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test" +import { registerCouncilMemberAgents } from "./council-member-agents" + +describe("council-member-agents", () => { + test("throws on duplicate model without name", () => { + //#given + const config = { + members: [ + { model: "openai/gpt-5.3-codex" }, + { model: "openai/gpt-5.3-codex" }, + ], + } + //#when + #then + expect(() => registerCouncilMemberAgents(config)).toThrow("already registered") + }) + + test("registers different models without error", () => { + //#given + const config = { + members: [ + { model: "openai/gpt-5.3-codex" }, + { model: "anthropic/claude-opus-4-6" }, + ], + } + //#when + const result = registerCouncilMemberAgents(config) + //#then + expect(result.registeredKeys).toHaveLength(2) + expect(result.registeredKeys).toContain("Council: GPT 5.3 Codex") + expect(result.registeredKeys).toContain("Council: Claude Opus 4.6") + }) + + test("allows same model with different names", () => { + //#given + const config = { + members: [ + { model: "openai/gpt-5.3-codex", name: "GPT Codex" }, + { model: "openai/gpt-5.3-codex", name: "Codex GPT" }, + ], + } + //#when + const result = registerCouncilMemberAgents(config) + //#then + expect(result.registeredKeys).toHaveLength(2) + expect(result.agents).toHaveProperty("Council: GPT Codex") + expect(result.agents).toHaveProperty("Council: Codex GPT") + }) + + test("returns empty when valid members below 2", () => { + //#given - one valid model, one invalid (no slash separator) + const config = { + members: [ + { model: "openai/gpt-5.3-codex" }, + { model: "invalid-no-slash" }, + ], + } + //#when + const result = registerCouncilMemberAgents(config) + //#then + expect(result.registeredKeys).toHaveLength(0) + expect(result.agents).toEqual({}) + }) +}) diff --git a/src/agents/builtin-agents/council-member-agents.ts b/src/agents/builtin-agents/council-member-agents.ts index ff4c3b39..0bdc1d3d 100644 --- a/src/agents/builtin-agents/council-member-agents.ts +++ b/src/agents/builtin-agents/council-member-agents.ts @@ -71,11 +71,19 @@ export function registerCouncilMemberAgents( const friendlyName = member.name ?? humanizeModelId(member.model) const description = `Council member: ${friendlyName} (${member.model}). Independent read-only code analyst for Athena council. (OhMyOpenCode)` + if (agents[key]) { + const existingModel = agents[key].model ?? "unknown" + throw new Error( + `Council member key "${key}" is already registered (model: ${existingModel}). Use distinct "name" fields to avoid collisions.` + ) + } + agents[key] = { ...config, description, model: member.model, ...(member.variant ? { variant: member.variant } : {}), + ...(member.temperature !== undefined ? { temperature: member.temperature } : {}), } registeredKeys.push(key) @@ -87,5 +95,10 @@ export function registerCouncilMemberAgents( }) } + if (registeredKeys.length < 2) { + log("[council-member-agents] Fewer than 2 valid council members after model parsing — disabling council mode") + return { agents: {}, registeredKeys: [] } + } + return { agents, registeredKeys } }