feat(07-01): add optional council member filtering

- add optional members arg support to athena_council tool

- filter selected members case-insensitively with clear unknown-member errors

- add tests for default-all and member selection behavior
This commit is contained in:
ismeth 2026-02-12 18:21:33 +01:00 committed by YeonGyu-Kim
parent d76c2bd8fa
commit f0f518f9cd
3 changed files with 155 additions and 3 deletions

View File

@ -3,7 +3,7 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import type { BackgroundManager } from "../../features/background-agent" import type { BackgroundManager } from "../../features/background-agent"
import { ATHENA_COUNCIL_TOOL_DESCRIPTION } from "./constants" import { ATHENA_COUNCIL_TOOL_DESCRIPTION } from "./constants"
import { createAthenaCouncilTool } from "./tools" import { createAthenaCouncilTool, filterCouncilMembers } from "./tools"
const mockManager = { const mockManager = {
getTask: () => undefined, getTask: () => undefined,
@ -19,6 +19,78 @@ const mockToolContext = {
abort: new AbortController().signal, abort: new AbortController().signal,
} }
const configuredMembers = [
{ name: "Claude", model: "anthropic/claude-sonnet-4-5" },
{ name: "GPT", model: "openai/gpt-5.3-codex" },
{ model: "google/gemini-3-pro" },
]
describe("filterCouncilMembers", () => {
test("returns all members when selection is undefined", () => {
// #given
const selectedMembers = undefined
// #when
const result = filterCouncilMembers(configuredMembers, selectedMembers)
// #then
expect(result.members).toEqual(configuredMembers)
expect(result.error).toBeUndefined()
})
test("returns all members when selection is empty", () => {
// #given
const selectedMembers: string[] = []
// #when
const result = filterCouncilMembers(configuredMembers, selectedMembers)
// #then
expect(result.members).toEqual(configuredMembers)
expect(result.error).toBeUndefined()
})
test("filters members using case-insensitive name and model matching", () => {
// #given
const selectedMembers = ["gpt", "GOOGLE/GEMINI-3-PRO"]
// #when
const result = filterCouncilMembers(configuredMembers, selectedMembers)
// #then
expect(result.members).toEqual([configuredMembers[1], configuredMembers[2]])
expect(result.error).toBeUndefined()
})
test("returns helpful error when selected members are not configured", () => {
// #given
const selectedMembers = ["mistral", "xai/grok-3"]
// #when
const result = filterCouncilMembers(configuredMembers, selectedMembers)
// #then
expect(result.members).toEqual([])
expect(result.error).toBe(
"Unknown council members: mistral, xai/grok-3. Available members: Claude, GPT, google/gemini-3-pro."
)
})
test("returns error listing only unmatched names when partially matched", () => {
// #given
const selectedMembers = ["claude", "non-existent"]
// #when
const result = filterCouncilMembers(configuredMembers, selectedMembers)
// #then
expect(result.members).toEqual([])
expect(result.error).toBe(
"Unknown council members: non-existent. Available members: Claude, GPT, google/gemini-3-pro."
)
})
})
describe("createAthenaCouncilTool", () => { describe("createAthenaCouncilTool", () => {
test("returns error when councilConfig is undefined", async () => { test("returns error when councilConfig is undefined", async () => {
// #given // #given
@ -58,5 +130,24 @@ describe("createAthenaCouncilTool", () => {
// #then // #then
expect(athenaCouncilTool.description).toBe(ATHENA_COUNCIL_TOOL_DESCRIPTION) expect(athenaCouncilTool.description).toBe(ATHENA_COUNCIL_TOOL_DESCRIPTION)
expect((athenaCouncilTool as { args: Record<string, unknown> }).args.question).toBeDefined() expect((athenaCouncilTool as { args: Record<string, unknown> }).args.question).toBeDefined()
expect((athenaCouncilTool as { args: Record<string, unknown> }).args.members).toBeDefined()
})
test("returns helpful error when members contains invalid names", async () => {
// #given
const athenaCouncilTool = createAthenaCouncilTool({
backgroundManager: mockManager,
councilConfig: { members: configuredMembers },
})
const toolArgs = {
question: "Who should investigate this?",
members: ["unknown-model"],
}
// #when
const result = await athenaCouncilTool.execute(toolArgs, mockToolContext)
// #then
expect(result).toBe("Unknown council members: unknown-model. Available members: Claude, GPT, google/gemini-3-pro.")
}) })
}) })

View File

@ -1,6 +1,6 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin" import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import { executeCouncil } from "../../agents/athena/council-orchestrator" import { executeCouncil } from "../../agents/athena/council-orchestrator"
import type { CouncilConfig, CouncilMemberResponse } from "../../agents/athena/types" import type { CouncilConfig, CouncilMemberConfig, CouncilMemberResponse } from "../../agents/athena/types"
import type { BackgroundManager, BackgroundTask, BackgroundTaskStatus } from "../../features/background-agent" import type { BackgroundManager, BackgroundTask, BackgroundTaskStatus } from "../../features/background-agent"
import { ATHENA_COUNCIL_TOOL_DESCRIPTION } from "./constants" import { ATHENA_COUNCIL_TOOL_DESCRIPTION } from "./constants"
import { createCouncilLauncher } from "./council-launcher" import { createCouncilLauncher } from "./council-launcher"
@ -101,6 +101,57 @@ function formatCouncilOutput(responses: CouncilMemberResponse[], totalMembers: n
return `${completedCount}/${totalMembers} council members completed.\n\n${lines.join("\n\n")}` return `${completedCount}/${totalMembers} council members completed.\n\n${lines.join("\n\n")}`
} }
interface FilterCouncilMembersResult {
members: CouncilMemberConfig[]
error?: string
}
export function filterCouncilMembers(
members: CouncilMemberConfig[],
selectedNames: string[] | undefined
): FilterCouncilMembersResult {
if (!selectedNames || selectedNames.length === 0) {
return { members }
}
const memberLookup = new Map<string, CouncilMemberConfig>()
members.forEach((member) => {
const key = (member.name ?? member.model).toLowerCase()
memberLookup.set(key, member)
})
const unresolved: string[] = []
const filteredMembers: CouncilMemberConfig[] = []
const includedMemberKeys = new Set<string>()
selectedNames.forEach((selectedName) => {
const selectedKey = selectedName.toLowerCase()
const matchedMember = memberLookup.get(selectedKey)
if (!matchedMember) {
unresolved.push(selectedName)
return
}
const memberKey = matchedMember.model
if (includedMemberKeys.has(memberKey)) {
return
}
includedMemberKeys.add(memberKey)
filteredMembers.push(matchedMember)
})
if (unresolved.length > 0) {
const availableNames = members.map((member) => member.name ?? member.model).join(", ")
return {
members: [],
error: `Unknown council members: ${unresolved.join(", ")}. Available members: ${availableNames}.`,
}
}
return { members: filteredMembers }
}
export function createAthenaCouncilTool(args: { export function createAthenaCouncilTool(args: {
backgroundManager: BackgroundManager backgroundManager: BackgroundManager
councilConfig: CouncilConfig | undefined councilConfig: CouncilConfig | undefined
@ -111,12 +162,21 @@ export function createAthenaCouncilTool(args: {
description: ATHENA_COUNCIL_TOOL_DESCRIPTION, description: ATHENA_COUNCIL_TOOL_DESCRIPTION,
args: { args: {
question: tool.schema.string().describe("The question to send to all council members"), question: tool.schema.string().describe("The question to send to all council members"),
members: tool.schema
.array(tool.schema.string())
.optional()
.describe("Optional list of council member names or models to consult. Defaults to all configured members."),
}, },
async execute(toolArgs: AthenaCouncilToolArgs, toolContext) { async execute(toolArgs: AthenaCouncilToolArgs, toolContext) {
if (!isCouncilConfigured(councilConfig)) { if (!isCouncilConfigured(councilConfig)) {
return "Athena council not configured. Add agents.athena.council.members to your config." return "Athena council not configured. Add agents.athena.council.members to your config."
} }
const filteredMembers = filterCouncilMembers(councilConfig.members, toolArgs.members)
if (filteredMembers.error) {
return filteredMembers.error
}
if (activeCouncilSessions.has(toolContext.sessionID)) { if (activeCouncilSessions.has(toolContext.sessionID)) {
return "Council is already running for this session. Wait for the current council execution to complete." return "Council is already running for this session. Wait for the current council execution to complete."
} }
@ -125,7 +185,7 @@ export function createAthenaCouncilTool(args: {
try { try {
const execution = await executeCouncil({ const execution = await executeCouncil({
question: toolArgs.question, question: toolArgs.question,
council: councilConfig, council: { members: filteredMembers.members },
launcher: createCouncilLauncher(backgroundManager), launcher: createCouncilLauncher(backgroundManager),
parentSessionID: toolContext.sessionID, parentSessionID: toolContext.sessionID,
parentMessageID: toolContext.messageID, parentMessageID: toolContext.messageID,

View File

@ -1,3 +1,4 @@
export interface AthenaCouncilToolArgs { export interface AthenaCouncilToolArgs {
question: string question: string
members?: string[]
} }