diff --git a/src/agents/athena/non-interactive-prompt.ts b/src/agents/athena/non-interactive-prompt.ts index a0d83ae2b..f3f15b769 100644 --- a/src/agents/athena/non-interactive-prompt.ts +++ b/src/agents/athena/non-interactive-prompt.ts @@ -20,8 +20,8 @@ background_wait_timeout_ms: {BACKGROUND_WAIT_TIMEOUT_MS} -Council members are listed at the end of this prompt after config injection. -Use EXACTLY the subagent_type names listed there. +Council members are listed in the athena_council tool description. +Use member names from there when filtering with the members parameter. @@ -55,14 +55,12 @@ Precedence for ambiguous cases: DIAGNOSE > AUDIT > PLAN > EVALUATE > EXPLAIN > C - mode: from Step 2 - intent: from Step 3 -#### Step 4.2: For each resolved member, call the task tool with: -- subagent_type: the exact member name from registered council members -- run_in_background: true -- write_output_to_file: true -- prompt: "Read for your instructions." (path from Step 4.1) -- load_skills: [] -- description: the member name -Launch ALL members before collecting results. Track every task_id. +#### Step 4.2: Call athena_council to launch ALL members at once: +- prompt_file: the path returned from Step 4.1 +- members: the resolved member names from Step 1 (omit to launch all configured members) + +athena_council launches all members in parallel and returns JSON with task IDs. +Track every task_id from the response for use in Step 5. ### Step 5: Track progress with background_wait. - Call background_wait(task_ids=[...all task IDs...], timeout={BACKGROUND_WAIT_TIMEOUT_MS}). @@ -134,11 +132,13 @@ After synthesis, you MUST output EXACTLY this structured format: } +IMPORTANT: is your FINAL output. Do NOT output anything after this tag. +No commentary, no summary, no follow-up text. The closing tag IS the end of your response. + Status values: - "complete": Quorum met, synthesis performed - "partial": Some members failed but quorum met - "failed": Quorum not met (<2 successful members) - - NEVER use the Question tool — it is unavailable in non-interactive mode. @@ -148,6 +148,6 @@ Status values: - ALWAYS auto-select analysis mode from config (Step 2). - ALWAYS return the structured output. - Use background_wait for progress tracking and council_finalize for result collection. -- Preserve confidence caveats (especially single-member claims) in synthesis. +- After outputting , STOP IMMEDIATELY. No text after the closing tag. ` diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 2a9cc13f7..5d42c89ad 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -30,6 +30,7 @@ import { createTaskUpdateTool, createHashlineEditTool, createPrepareCouncilPromptTool, + createAthenaCouncilTool, } from "../tools" import { createCouncilFinalize } from "../tools/council-archive" import { getMainSessionID } from "../features/claude-code-session-state" @@ -282,6 +283,10 @@ export function createToolRegistry(args: { ...hashlineToolsRecord, prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory), council_finalize: createCouncilFinalize(ctx.directory), + athena_council: createAthenaCouncilTool({ + backgroundManager: managers.backgroundManager, + councilConfig: pluginConfig.agents?.athena?.council, + }), } for (const toolDefinition of Object.values(allTools)) { diff --git a/src/tools/athena-council/council-launcher.test.ts b/src/tools/athena-council/council-launcher.test.ts new file mode 100644 index 000000000..b332a2797 --- /dev/null +++ b/src/tools/athena-council/council-launcher.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, it, mock, beforeEach } from "bun:test" +import { launchCouncilMember } from "./council-launcher" +import type { CouncilLaunchContext } from "./council-launcher" +import type { BackgroundManager } from "../../features/background-agent" +import type { CouncilMemberConfig } from "../../config/schema/athena" + +const makeManager = (overrides?: Partial): BackgroundManager => + ({ + launch: mock(async () => ({ + id: "task-123", + sessionID: undefined, + status: "running", + })), + getTask: mock(() => undefined), + ...overrides, + }) as unknown as BackgroundManager + +const makeContext = (): CouncilLaunchContext => ({ + parentSessionID: "ses-parent", + parentMessageID: "msg-parent", + parentAgent: "athena", +}) + +describe("launchCouncilMember", () => { + describe("#given a valid member with provider/model format", () => { + describe("#when launched successfully", () => { + let manager: BackgroundManager + let result: Awaited> + + beforeEach(async () => { + manager = makeManager() + const member: CouncilMemberConfig = { + name: "Claude Opus", + model: "anthropic/claude-opus-4-6", + } + result = await launchCouncilMember(member, "analyze this", manager, makeContext()) + }) + + it("#then returns the member in the outcome", () => { + expect(result.member.name).toBe("Claude Opus") + }) + + it("#then returns a task with an id", () => { + expect(result.task.id).toBe("task-123") + }) + + it("#then calls manager.launch once", () => { + expect(manager.launch).toHaveBeenCalledTimes(1) + }) + + it("#then passes the correct agent key with Council: prefix", () => { + const launchArgs = (manager.launch as ReturnType).mock.calls[0][0] + expect(launchArgs.agent).toBe("Council: Claude Opus") + }) + + it("#then passes the prompt content", () => { + const launchArgs = (manager.launch as ReturnType).mock.calls[0][0] + expect(launchArgs.prompt).toBe("analyze this") + }) + + it("#then passes the correct providerID", () => { + const launchArgs = (manager.launch as ReturnType).mock.calls[0][0] + expect(launchArgs.model.providerID).toBe("anthropic") + }) + + it("#then passes the correct modelID", () => { + const launchArgs = (manager.launch as ReturnType).mock.calls[0][0] + expect(launchArgs.model.modelID).toBe("claude-opus-4-6") + }) + + it("#then passes the parent session ID", () => { + const launchArgs = (manager.launch as ReturnType).mock.calls[0][0] + expect(launchArgs.parentSessionID).toBe("ses-parent") + }) + }) + }) + + describe("#given a member with a variant", () => { + describe("#when launched", () => { + it("#then passes the variant through to the model config", async () => { + const manager = makeManager() + const member: CouncilMemberConfig = { + name: "GPT Codex", + model: "openai/gpt-5.3-codex", + variant: "medium", + } + await launchCouncilMember(member, "prompt", manager, makeContext()) + const launchArgs = (manager.launch as ReturnType).mock.calls[0][0] + expect(launchArgs.model.variant).toBe("medium") + }) + }) + }) + + describe("#given a member without a variant", () => { + describe("#when launched", () => { + it("#then does not include variant in the model config", async () => { + const manager = makeManager() + const member: CouncilMemberConfig = { + name: "Gemini Flash", + model: "google/gemini-3-flash", + } + await launchCouncilMember(member, "prompt", manager, makeContext()) + const launchArgs = (manager.launch as ReturnType).mock.calls[0][0] + expect(launchArgs.model.variant).toBeUndefined() + }) + }) + }) + + describe("#given a member with an invalid model format (no slash)", () => { + describe("#when launched", () => { + it("#then throws an error about invalid model format", async () => { + const manager = makeManager() + const member: CouncilMemberConfig = { + name: "Bad Model", + model: "not-a-valid-model", + } + await expect(launchCouncilMember(member, "prompt", manager, makeContext())).rejects.toThrow( + 'Invalid model format: "not-a-valid-model"', + ) + }) + }) + }) + + describe("#given a member with an empty model string", () => { + describe("#when launched", () => { + it("#then throws an error about invalid model format", async () => { + const manager = makeManager() + const member: CouncilMemberConfig = { + name: "Empty Model", + model: "", + } + await expect(launchCouncilMember(member, "prompt", manager, makeContext())).rejects.toThrow( + "Invalid model format", + ) + }) + }) + }) +}) diff --git a/src/tools/athena-council/council-launcher.ts b/src/tools/athena-council/council-launcher.ts new file mode 100644 index 000000000..ce188844d --- /dev/null +++ b/src/tools/athena-council/council-launcher.ts @@ -0,0 +1,52 @@ +import type { BackgroundManager } from "../../features/background-agent" +import type { BackgroundTask } from "../../features/background-agent/types" +import type { CouncilMemberConfig } from "../../config/schema/athena" +import { parseModelString } from "../delegate-task/model-string-parser" +import { COUNCIL_MEMBER_KEY_PREFIX } from "../../agents/builtin-agents/council-member-agents" + +export interface CouncilLaunchContext { + parentSessionID: string + parentMessageID: string + parentAgent?: string +} + +interface LaunchOutcome { + member: CouncilMemberConfig + task: BackgroundTask +} + +/** + * Launches a single council member as a background task. + * The agent key follows the "Council: " pattern used by council-member-agents.ts. + */ +export async function launchCouncilMember( + member: CouncilMemberConfig, + prompt: string, + manager: BackgroundManager, + context: CouncilLaunchContext, +): Promise { + const parsed = parseModelString(member.model) + if (!parsed) { + throw new Error(`Invalid model format: "${member.model}" (expected "provider/model-id")`) + } + + const agentKey = `${COUNCIL_MEMBER_KEY_PREFIX}${member.name}` + const memberName = member.name ?? member.model + + const task = await manager.launch({ + description: `Council member: ${memberName}`, + prompt, + agent: agentKey, + parentSessionID: context.parentSessionID, + parentMessageID: context.parentMessageID, + parentAgent: context.parentAgent, + writeOutputToFile: true, + model: { + providerID: parsed.providerID, + modelID: parsed.modelID, + ...(member.variant ? { variant: member.variant } : {}), + }, + }) + + return { member, task } +} diff --git a/src/tools/athena-council/index.ts b/src/tools/athena-council/index.ts new file mode 100644 index 000000000..82fd5a725 --- /dev/null +++ b/src/tools/athena-council/index.ts @@ -0,0 +1,2 @@ +export { createAthenaCouncilTool } from "./tools" +export type { AthenaCouncilToolArgs, AthenaCouncilResult, LaunchedMemberInfo } from "./types" diff --git a/src/tools/athena-council/tools.test.ts b/src/tools/athena-council/tools.test.ts new file mode 100644 index 000000000..833db5bc7 --- /dev/null +++ b/src/tools/athena-council/tools.test.ts @@ -0,0 +1,231 @@ +import { describe, expect, it, mock, beforeEach, afterAll } from "bun:test" +import { writeFile, mkdir, rm } from "node:fs/promises" +import { join } from "node:path" +import { tmpdir } from "node:os" + +const mockLaunchCouncilMember = mock(async (member: { name: string; model: string }) => ({ + member, + task: { id: `task-${member.name}`, sessionID: undefined, status: "running" }, +})) + +mock.module("./council-launcher", () => ({ + launchCouncilMember: mockLaunchCouncilMember, +})) + +import { createAthenaCouncilTool } from "./tools" +import type { BackgroundManager } from "../../features/background-agent" +import type { CouncilConfig } from "../../config/schema/athena" + +const TEST_TMP_DIR = join(tmpdir(), "athena-council-test") +const PROMPT_FILE = join(TEST_TMP_DIR, "test-prompt.md") + +const makeManager = (): BackgroundManager => + ({ + launch: mock(async () => ({ id: "task-123", sessionID: undefined, status: "running" })), + getTask: mock((taskId: string) => ({ id: taskId, sessionID: `ses-${taskId}`, status: "running" })), + }) as unknown as BackgroundManager + +const makeToolContext = () => ({ + sessionID: "ses-test", + messageID: "msg-test", + agent: "athena", + abort: undefined, +}) + +const makeCouncilConfig = (members?: Array<{ name: string; model: string; variant?: string }>): CouncilConfig => ({ + members: members ?? [ + { name: "Claude Opus", model: "anthropic/claude-opus-4-6" }, + { name: "GPT Codex", model: "openai/gpt-5.3-codex" }, + ], + retry_on_fail: 0, + retry_failed_if_others_finished: false, + cancel_retrying_on_quorum: true, + stuck_threshold_seconds: 300, + member_max_running_seconds: 600, +}) + +describe("createAthenaCouncilTool", () => { + beforeEach(async () => { + await mkdir(TEST_TMP_DIR, { recursive: true }) + await writeFile(PROMPT_FILE, "prompt file content", "utf-8") + mockLaunchCouncilMember.mockImplementation(async (member: { name: string; model: string }) => ({ + member, + task: { id: `task-${member.name}`, sessionID: undefined, status: "running" }, + })) + }) + + afterAll(async () => { + await rm(TEST_TMP_DIR, { recursive: true, force: true }) + }) + + describe("#given council is not configured (undefined)", () => { + describe("#when execute is called", () => { + it("#then returns error message about council not configured", async () => { + const tool = createAthenaCouncilTool({ backgroundManager: makeManager(), councilConfig: undefined }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + expect(result).toContain("Council not configured") + }) + }) + }) + + describe("#given council has zero members", () => { + describe("#when execute is called", () => { + it("#then returns error message about council not configured", async () => { + const config = makeCouncilConfig([]) + const tool = createAthenaCouncilTool({ backgroundManager: makeManager(), councilConfig: config }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + expect(result).toContain("Council not configured") + }) + }) + }) + + describe("#given prompt_file does not exist", () => { + describe("#when execute is called", () => { + it("#then returns error message about failed file read", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute({ prompt_file: "/nonexistent/prompt.md" }, makeToolContext()) + expect(result).toContain("Failed to read prompt file") + expect(result).toContain("/nonexistent/prompt.md") + }) + }) + }) + + describe("#given an unknown member name in the members filter", () => { + describe("#when execute is called with that unknown name", () => { + it("#then returns error listing unknown member and available members", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute( + { prompt_file: PROMPT_FILE, members: ["NonExistentMember"] }, + makeToolContext(), + ) + expect(result).toContain("Unknown council members: NonExistentMember") + expect(result).toContain("Available:") + }) + }) + }) + + describe("#given a valid subset of member names in the filter", () => { + describe("#when execute is called with one member name", () => { + it("#then launches only the specified member", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute( + { prompt_file: PROMPT_FILE, members: ["Claude Opus"] }, + makeToolContext(), + ) + const jsonMatch = result.match(/\{[\s\S]*\}/) + expect(jsonMatch).not.toBeNull() + const parsed = JSON.parse(jsonMatch![0]) + expect(parsed.total_requested).toBe(1) + expect(parsed.launched).toHaveLength(1) + expect(parsed.launched[0].member_name).toBe("Claude Opus") + }) + }) + }) + + describe("#given all members are valid and prompt file is readable", () => { + describe("#when execute is called without member filter", () => { + it("#then returns JSON with all launched members", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + const jsonMatch = result.match(/\{[\s\S]*\}/) + expect(jsonMatch).not.toBeNull() + const parsed = JSON.parse(jsonMatch![0]) + expect(parsed.total_requested).toBe(2) + expect(parsed.launched).toHaveLength(2) + expect(parsed.failures).toHaveLength(0) + }) + + it("#then includes task IDs and background_wait instructions in the output", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + expect(result).toContain("background_wait") + expect(result).toContain("task_ids=") + }) + }) + }) + + describe("#given some members fail to launch", () => { + describe("#when execute is called", () => { + beforeEach(() => { + let callCount = 0 + mockLaunchCouncilMember.mockImplementation(async (member: { name: string; model: string }) => { + callCount++ + if (callCount === 1) { + return { member, task: { id: `task-${member.name}`, sessionID: undefined, status: "running" } } + } + throw new Error("Launch failed for member") + }) + }) + + it("#then includes successful launches in the result", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + const jsonMatch = result.match(/\{[\s\S]*\}/) + expect(jsonMatch).not.toBeNull() + const parsed = JSON.parse(jsonMatch![0]) + expect(parsed.launched).toHaveLength(1) + }) + + it("#then includes failures in the result", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + const jsonMatch = result.match(/\{[\s\S]*\}/) + expect(jsonMatch).not.toBeNull() + const parsed = JSON.parse(jsonMatch![0]) + expect(parsed.failures).toHaveLength(1) + expect(parsed.failures[0].error).toContain("Launch failed for member") + }) + }) + }) + + describe("#given all members fail to launch", () => { + describe("#when execute is called", () => { + beforeEach(() => { + mockLaunchCouncilMember.mockImplementation(async () => { + throw new Error("All launches failed") + }) + }) + + it("#then returns a plain error string (not JSON)", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + expect(result).toContain("All council member launches failed") + expect(result).not.toContain('"launched"') + }) + + it("#then lists each failure in the error message", async () => { + const tool = createAthenaCouncilTool({ + backgroundManager: makeManager(), + councilConfig: makeCouncilConfig(), + }) + const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext()) + expect(result).toContain("Claude Opus") + expect(result).toContain("GPT Codex") + }) + }) + }) +}) diff --git a/src/tools/athena-council/tools.ts b/src/tools/athena-council/tools.ts new file mode 100644 index 000000000..d1c85d9ed --- /dev/null +++ b/src/tools/athena-council/tools.ts @@ -0,0 +1,195 @@ +import { tool, type ToolDefinition } from "@opencode-ai/plugin" +import { readFile } from "node:fs/promises" +import type { BackgroundManager } from "../../features/background-agent" +import type { CouncilConfig, CouncilMemberConfig } from "../../config/schema/athena" +import { launchCouncilMember, type CouncilLaunchContext } from "./council-launcher" +import type { AthenaCouncilToolArgs, LaunchedMemberInfo, AthenaCouncilResult } from "./types" +import { log } from "../../shared/logger" + +const SESSION_WAIT_INTERVAL_MS = 100 +const SESSION_WAIT_TIMEOUT_MS = 30_000 + +function buildToolDescription(councilConfig: CouncilConfig | undefined): string { + const memberList = councilConfig?.members.length + ? councilConfig.members.map((m) => `- ${m.name} (${m.model})`).join("\n") + : "No members configured." + + return `Launch all council members in parallel for multi-model analysis. + +Takes a prompt file path (from prepare_council_prompt) and launches all specified council members +as background tasks. Returns an array of task IDs for use with background_wait and background_output. + +Available council members: +${memberList} + +Returns JSON with launched task IDs and any launch failures.` +} + +function filterMembers( + allMembers: CouncilMemberConfig[], + selectedNames: string[] | undefined, +): { members: CouncilMemberConfig[]; error?: string } { + if (!selectedNames || selectedNames.length === 0) { + return { members: allMembers } + } + + const lookup = new Map() + for (const member of allMembers) { + lookup.set(member.model.toLowerCase(), member) + if (member.name) { + lookup.set(member.name.toLowerCase(), member) + } + } + + const filtered: CouncilMemberConfig[] = [] + const seen = new Set() + const unresolved: string[] = [] + + for (const name of selectedNames) { + const match = lookup.get(name.toLowerCase()) + if (!match) { + unresolved.push(name) + continue + } + if (!seen.has(match)) { + seen.add(match) + filtered.push(match) + } + } + + if (unresolved.length > 0) { + const available = allMembers.map((m) => m.name ?? m.model).join(", ") + return { members: [], error: `Unknown council members: ${unresolved.join(", ")}. Available: ${available}` } + } + + return { members: filtered } +} + +/** + * Waits briefly for background sessions to acquire session IDs. + * Non-blocking — returns whatever is available within the timeout. + */ +async function waitForSessionIds( + taskIds: string[], + manager: BackgroundManager, + abort?: AbortSignal, +): Promise> { + const result = new Map() + const pending = new Set(taskIds) + const deadline = Date.now() + SESSION_WAIT_TIMEOUT_MS + + while (pending.size > 0 && Date.now() < deadline) { + if (abort?.aborted) break + + for (const taskId of pending) { + const task = manager.getTask(taskId) + if (task?.sessionID) { + result.set(taskId, task.sessionID) + pending.delete(taskId) + } + } + + if (pending.size > 0) { + await new Promise((resolve) => setTimeout(resolve, SESSION_WAIT_INTERVAL_MS)) + } + } + + return result +} + +export function createAthenaCouncilTool(args: { + backgroundManager: BackgroundManager + councilConfig: CouncilConfig | undefined +}): ToolDefinition { + const { backgroundManager, councilConfig } = args + const description = buildToolDescription(councilConfig) + + return tool({ + description, + args: { + prompt_file: tool.schema.string().describe("Path to the prompt file created by prepare_council_prompt"), + members: tool.schema + .array(tool.schema.string()) + .optional() + .describe("Optional list of council member names to launch. Defaults to all configured members."), + }, + async execute(toolArgs: AthenaCouncilToolArgs, toolContext) { + if (!councilConfig || councilConfig.members.length === 0) { + return "Council not configured. Add agents.athena.council.members to your config." + } + + const { members, error } = filterMembers(councilConfig.members, toolArgs.members) + if (error) return error + if (members.length === 0) return "No council members to launch." + + let promptContent: string + try { + promptContent = await readFile(toolArgs.prompt_file, "utf-8") + } catch (err) { + return `Failed to read prompt file: ${toolArgs.prompt_file}. Error: ${String(err)}` + } + + const context: CouncilLaunchContext = { + parentSessionID: toolContext.sessionID, + parentMessageID: toolContext.messageID, + parentAgent: toolContext.agent, + } + + log("[athena_council] Launching council members", { count: members.length }) + + const launchResults = await Promise.allSettled( + members.map((member) => launchCouncilMember(member, promptContent, backgroundManager, context)), + ) + + const launched: Array<{ taskId: string; member: CouncilMemberConfig }> = [] + const failures: AthenaCouncilResult["failures"] = [] + + launchResults.forEach((result, index) => { + const member = members[index] + if (result.status === "fulfilled") { + launched.push({ taskId: result.value.task.id, member: result.value.member }) + } else { + failures.push({ + member_name: member.name ?? member.model, + model: member.model, + error: String(result.reason), + }) + } + }) + + if (launched.length === 0) { + return `All council member launches failed:\n${failures.map((f) => `- ${f.member_name}: ${f.error}`).join("\n")}` + } + + const sessionMap = await waitForSessionIds( + launched.map((l) => l.taskId), + backgroundManager, + toolContext.abort, + ) + + const launchedInfo: LaunchedMemberInfo[] = launched.map((l) => ({ + task_id: l.taskId, + session_id: sessionMap.get(l.taskId), + member_name: l.member.name ?? l.member.model, + model: l.member.model, + })) + + const output: AthenaCouncilResult = { + launched: launchedInfo, + failures, + total_requested: members.length, + } + + log("[athena_council] Launch complete", { + launched: launchedInfo.length, + failed: failures.length, + }) + + const taskIdList = launchedInfo.map((l) => l.task_id) + return `${JSON.stringify(output, null, 2)} + +Use background_wait with task_ids=${JSON.stringify(taskIdList)} to wait for completion. +Then use background_output for each task_id to collect individual results.` + }, + }) +} diff --git a/src/tools/athena-council/types.ts b/src/tools/athena-council/types.ts new file mode 100644 index 000000000..74207f5f2 --- /dev/null +++ b/src/tools/athena-council/types.ts @@ -0,0 +1,17 @@ +export interface AthenaCouncilToolArgs { + prompt_file: string + members?: string[] +} + +export interface LaunchedMemberInfo { + task_id: string + session_id: string | undefined + member_name: string + model: string +} + +export interface AthenaCouncilResult { + launched: LaunchedMemberInfo[] + failures: Array<{ member_name: string; model: string; error: string }> + total_requested: number +} diff --git a/src/tools/index.ts b/src/tools/index.ts index b723a288d..220d2649c 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -48,6 +48,7 @@ export { export { createHashlineEditTool } from "./hashline-edit" export { createPrepareCouncilPromptTool } from "./prepare-council-prompt" export { createCouncilFinalize } from "./council-archive" +export { createAthenaCouncilTool } from "./athena-council" export function createBackgroundTools(manager: BackgroundManager, client: OpencodeClient): Record { const outputManager: BackgroundOutputManager = manager