diff --git a/src/agents/athena/council-orchestrator.test.ts b/src/agents/athena/council-orchestrator.test.ts deleted file mode 100644 index 264ac3b77..000000000 --- a/src/agents/athena/council-orchestrator.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { buildCouncilPrompt } from "./council-prompt" -import { executeCouncil } from "./council-orchestrator" -import type { CouncilConfig } from "./types" - -interface MockLaunchInput { - description: string - prompt: string - agent: string - parentSessionID: string - parentMessageID: string - parentAgent?: string - model?: { providerID: string; modelID: string; variant?: string } -} - -function createMockTask(id: string, launch: MockLaunchInput) { - return { - id, - status: "pending" as const, - parentSessionID: launch.parentSessionID, - parentMessageID: launch.parentMessageID, - description: launch.description, - prompt: launch.prompt, - agent: launch.agent, - } -} - -describe("executeCouncil", () => { - //#given a council with 3 members and a question - //#when executeCouncil is called - //#then all members are launched with the same prompt and parsed model ids - test("launches all members with identical prompt and model params", async () => { - const launches: MockLaunchInput[] = [] - const launcher = { - launch: async (input: MockLaunchInput) => { - launches.push(input) - return createMockTask(`task-${launches.length}`, input) - }, - } - - const council: CouncilConfig = { - members: [ - { model: "openai/gpt-5.3-codex", name: "openai" }, - { model: "anthropic/claude-sonnet-4-5", name: "anthropic" }, - { model: "google/gemini-3-pro", name: "google" }, - ], - } - - const question = "How can we improve the retry strategy?" - const result = await executeCouncil({ - question, - council, - launcher, - parentSessionID: "session-1", - parentMessageID: "message-1", - parentAgent: "sisyphus", - }) - - const expectedPrompt = buildCouncilPrompt(question) - - expect(launches).toHaveLength(3) - expect(result.launched).toHaveLength(3) - expect(result.failures).toHaveLength(0) - expect(result.totalMembers).toBe(3) - - for (const launch of launches) { - expect(launch.prompt).toBe(expectedPrompt) - expect(launch.agent).toBe("council-member") - } - - expect(launches[0]?.model).toEqual({ providerID: "openai", modelID: "gpt-5.3-codex" }) - expect(launches[1]?.model).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-5" }) - expect(launches[2]?.model).toEqual({ providerID: "google", modelID: "gemini-3-pro" }) - }) - - //#given a council with 3 members where 1 launch throws - //#when executeCouncil is called - //#then launch failures are captured separately from successful launches - test("captures launch failures separately from successful launches", async () => { - const launcher = { - launch: async (input: MockLaunchInput) => { - if (input.model?.providerID === "anthropic") { - throw new Error("Provider unavailable") - } - return createMockTask(`task-${input.model?.providerID}`, input) - }, - } - - const result = await executeCouncil({ - question: "Find race condition risks", - council: { - members: [ - { model: "openai/gpt-5.3-codex" }, - { model: "anthropic/claude-sonnet-4-5" }, - { model: "google/gemini-3-pro" }, - ], - }, - launcher, - parentSessionID: "session-1", - parentMessageID: "message-1", - }) - - expect(result.launched).toHaveLength(2) - expect(result.failures).toHaveLength(1) - expect(result.totalMembers).toBe(3) - expect(result.failures[0]?.member.model).toBe("anthropic/claude-sonnet-4-5") - expect(result.failures[0]?.error).toContain("Launch failed") - }) - - //#given a council where all launches throw - //#when executeCouncil is called - //#then all members appear as failures with zero launched - test("returns all failures when every launch throws", async () => { - const launcher = { - launch: async () => { - throw new Error("Model unavailable") - }, - } - - const result = await executeCouncil({ - question: "Analyze unknown module", - council: { - members: [ - { model: "openai/gpt-5.3-codex" }, - { model: "anthropic/claude-sonnet-4-5" }, - ], - }, - launcher, - parentSessionID: "session-1", - parentMessageID: "message-1", - }) - - expect(result.launched).toHaveLength(0) - expect(result.failures).toHaveLength(2) - expect(result.totalMembers).toBe(2) - expect(result.failures.every((f) => f.error.includes("Launch failed"))).toBe(true) - }) - - //#given a council with one invalid model string - //#when executeCouncil is called - //#then invalid member becomes a failure while others still launch - test("handles invalid model strings without crashing council execution", async () => { - const launches: MockLaunchInput[] = [] - const launcher = { - launch: async (input: MockLaunchInput) => { - launches.push(input) - return createMockTask(`task-${launches.length}`, input) - }, - } - - const result = await executeCouncil({ - question: "Audit dependency graph", - council: { - members: [ - { model: "invalid-model" }, - { model: "openai/gpt-5.3-codex" }, - ], - }, - launcher, - parentSessionID: "session-1", - parentMessageID: "message-1", - }) - - expect(launches).toHaveLength(1) - expect(result.launched).toHaveLength(1) - expect(result.failures).toHaveLength(1) - expect(result.failures.find((f) => f.member.model === "invalid-model")?.error).toContain("Launch failed") - }) - - //#given members with per-member variant - //#when executeCouncil is called - //#then launch receives variant in model for each corresponding member - test("passes member variant to launch input model", async () => { - const launches: MockLaunchInput[] = [] - const launcher = { - launch: async (input: MockLaunchInput) => { - launches.push(input) - return createMockTask(`task-${launches.length}`, input) - }, - } - - await executeCouncil({ - question: "Compare architecture options", - council: { - members: [ - { model: "openai/gpt-5.3-codex", variant: "high" }, - { model: "anthropic/claude-sonnet-4-5" }, - ], - }, - launcher, - parentSessionID: "session-1", - parentMessageID: "message-1", - }) - - expect(launches).toHaveLength(2) - expect(launches[0]?.model?.variant).toBe("high") - expect(launches[1]?.model?.variant).toBeUndefined() - }) - - //#given launched members - //#when executeCouncil returns - //#then each launched member has a taskId for background_output retrieval - test("returns task IDs for background_output retrieval", async () => { - const launcher = { - launch: async (input: MockLaunchInput) => - createMockTask(`bg_${input.model?.providerID}`, input), - } - - const result = await executeCouncil({ - question: "Review error handling", - council: { - members: [ - { model: "openai/gpt-5.3-codex", name: "OpenAI" }, - { model: "google/gemini-3-pro", name: "Gemini" }, - ], - }, - launcher, - parentSessionID: "session-1", - parentMessageID: "message-1", - }) - - expect(result.launched).toHaveLength(2) - expect(result.launched[0]?.taskId).toBe("bg_openai") - expect(result.launched[0]?.member.name).toBe("OpenAI") - expect(result.launched[1]?.taskId).toBe("bg_google") - expect(result.launched[1]?.member.name).toBe("Gemini") - }) -}) diff --git a/src/agents/athena/council-orchestrator.ts b/src/agents/athena/council-orchestrator.ts deleted file mode 100644 index f5877d96e..000000000 --- a/src/agents/athena/council-orchestrator.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { LaunchInput, BackgroundTask } from "../../features/background-agent/types" -import { log } from "../../shared/logger" -import { buildCouncilPrompt } from "./council-prompt" -import { parseModelString } from "./model-parser" -import type { CouncilConfig, CouncilLaunchFailure, CouncilLaunchedMember, CouncilLaunchResult, CouncilMemberConfig } from "./types" - -export type CouncilLaunchInput = LaunchInput - -export interface CouncilLauncher { - launch(input: CouncilLaunchInput): Promise -} - -export interface CouncilExecutionInput { - question: string - council: CouncilConfig - launcher: CouncilLauncher - parentSessionID: string - parentMessageID: string - parentAgent?: string -} - -/** - * Launches all council members in parallel and returns launch outcomes. - * Does NOT wait for task completion — actual results are collected by the - * agent via background_output calls after this returns. - */ -export async function executeCouncil(input: CouncilExecutionInput): Promise { - const { question, council, launcher, parentSessionID, parentMessageID, parentAgent } = input - - log("[athena-council] Executing council", { memberCount: council.members.length }) - - const prompt = buildCouncilPrompt(question) - - const launchResults = await Promise.allSettled( - council.members.map((member) => - launchMember(member, prompt, launcher, parentSessionID, parentMessageID, parentAgent) - ) - ) - - const launched: CouncilLaunchedMember[] = [] - const failures: CouncilLaunchFailure[] = [] - - launchResults.forEach((result, index) => { - const member = council.members[index] - - if (result.status === "fulfilled") { - launched.push({ member, taskId: result.value.id }) - return - } - - failures.push({ - member, - error: `Launch failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`, - }) - }) - - log("[athena-council] Council execution complete", { launched: launched.length, failures: failures.length }) - - return { - question, - launched, - failures, - totalMembers: council.members.length, - } -} - -async function launchMember( - member: CouncilMemberConfig, - prompt: string, - launcher: CouncilLauncher, - parentSessionID: string, - parentMessageID: string, - parentAgent: string | undefined -): Promise { - const parsedModel = parseModelString(member.model) - if (!parsedModel) { - throw new Error(`Invalid model string: "${member.model}"`) - } - - const memberName = member.name ?? member.model - return launcher.launch({ - description: `Council member: ${memberName}`, - prompt, - agent: "council-member", - parentSessionID, - parentMessageID, - parentAgent, - model: { - providerID: parsedModel.providerID, - modelID: parsedModel.modelID, - ...(member.variant ? { variant: member.variant } : {}), - }, - }) -} diff --git a/src/agents/athena/council-prompt.ts b/src/agents/athena/council-prompt.ts deleted file mode 100644 index 9ccdff64a..000000000 --- a/src/agents/athena/council-prompt.ts +++ /dev/null @@ -1,24 +0,0 @@ -export function buildCouncilPrompt(question: string): string { - return `You are a council member in a multi-model analysis council. Your role is to provide independent, thorough analysis of the question below. - -## Your Role -- You are one of several AI models analyzing the same question independently -- Your analysis should be thorough and evidence-based -- You are read-only - you cannot modify any files, only analyze -- Focus on finding real issues, not hypothetical ones - -## Instructions -1. Analyze the question carefully -2. Search the codebase thoroughly using available tools (Read, Grep, Glob, LSP) -3. Report your findings with evidence (file paths, line numbers, code snippets) -4. For each finding, state: - - What the issue/observation is - - Where it is (file path, line number) - - Why it matters (severity: critical/high/medium/low) - - Your confidence level (high/medium/low) -5. Be concise but thorough - quality over quantity - -## Question - -${question}` -}