fix(athena): resolve 4 compatibility and correctness issues

- Use case-insensitive casing in duplicate name test to verify actual logic
- Align permission type with SDK AgentConfig pattern (as AgentConfig["permission"])
- Move duplicate-name validation from schema to runtime for graceful fallback
- Place skipped members details before 'end your turn' in council guard prompt
This commit is contained in:
ismeth 2026-02-20 20:13:02 +01:00 committed by YeonGyu-Kim
parent 2eb8f5741a
commit f6cdba07ec
5 changed files with 20 additions and 21 deletions

View File

@ -1,6 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode, AgentPromptMetadata } from "../types" import type { AgentMode, AgentPromptMetadata } from "../types"
import { createAgentToolRestrictions, type PermissionValue } from "../../shared/permission-compat" import { createAgentToolRestrictions } from "../../shared/permission-compat"
import { applyModelThinkingConfig } from "./model-thinking-config" import { applyModelThinkingConfig } from "./model-thinking-config"
const MODE: AgentMode = "primary" const MODE: AgentMode = "primary"
@ -211,10 +211,10 @@ The switch_agent tool switches the active agent. After you call it, end your res
export function createAthenaAgent(model: string): AgentConfig { export function createAthenaAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions(["write", "edit", "call_omo_agent"]) const restrictions = createAgentToolRestrictions(["write", "edit", "call_omo_agent"])
const permission: Record<string, PermissionValue> = { const permission = {
...restrictions.permission, ...restrictions.permission,
question: "allow", question: "allow",
} } as AgentConfig["permission"]
const base = { const base = {
description: description:

View File

@ -1,6 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentConfig } from "@opencode-ai/sdk"
const MISSING_COUNCIL_PROMPT = ` const MISSING_COUNCIL_PROMPT_HEADER = `
## CRITICAL: No Council Members Configured ## CRITICAL: No Council Members Configured
@ -32,7 +32,9 @@ You have no council members registered. This means the Athena council config is
} }
\`\`\` \`\`\`
Each member requires \`model\` (\`"provider/model-id"\` format) and \`name\` (display name). Minimum 2 members required. Optional fields: \`variant\`, \`temperature\`. Each member requires \`model\` (\`"provider/model-id"\` format) and \`name\` (display name). Minimum 2 members required. Optional fields: \`variant\`, \`temperature\`.`
const MISSING_COUNCIL_PROMPT_FOOTER = `
--- ---
@ -47,12 +49,14 @@ export function appendMissingCouncilPrompt(
athenaConfig: AgentConfig, athenaConfig: AgentConfig,
skippedMembers?: Array<{ name: string; reason: string }>, skippedMembers?: Array<{ name: string; reason: string }>,
): AgentConfig { ): AgentConfig {
let prompt = MISSING_COUNCIL_PROMPT let prompt = MISSING_COUNCIL_PROMPT_HEADER
if (skippedMembers && skippedMembers.length > 0) { if (skippedMembers && skippedMembers.length > 0) {
const skipDetails = skippedMembers.map((m) => `- **${m.name}**: ${m.reason}`).join("\n") const skipDetails = skippedMembers.map((m) => `- **${m.name}**: ${m.reason}`).join("\n")
prompt += `\n\n### Why Council Failed\n\nThe following members were skipped:\n${skipDetails}` prompt += `\n\n### Why Council Failed\n\nThe following members were skipped:\n${skipDetails}`
} }
prompt += MISSING_COUNCIL_PROMPT_FOOTER
return { ...athenaConfig, prompt } return { ...athenaConfig, prompt }
} }

View File

@ -2,12 +2,12 @@ import { describe, expect, test } from "bun:test"
import { registerCouncilMemberAgents } from "./council-member-agents" import { registerCouncilMemberAgents } from "./council-member-agents"
describe("council-member-agents", () => { describe("council-member-agents", () => {
test("skips duplicate names and disables council when below minimum", () => { test("skips case-insensitive duplicate names and disables council when below minimum", () => {
//#given //#given
const config = { const config = {
members: [ members: [
{ model: "openai/gpt-5.3-codex", name: "GPT" }, { model: "openai/gpt-5.3-codex", name: "GPT" },
{ model: "anthropic/claude-opus-4-6", name: "GPT" }, { model: "anthropic/claude-opus-4-6", name: "gpt" },
], ],
} }
//#when //#when

View File

@ -330,8 +330,9 @@ describe("CouncilConfigSchema", () => {
expect(result.success).toBe(false) expect(result.success).toBe(false)
}) })
test("rejects council with duplicate member names", () => { test("accepts council with duplicate member names for graceful runtime handling", () => {
//#given //#given - duplicate detection is handled at runtime by registerCouncilMemberAgents,
// not at schema level, to allow graceful fallback instead of hard parse failure
const config = { const config = {
members: [ members: [
{ model: "anthropic/claude-opus-4-6", name: "analyst" }, { model: "anthropic/claude-opus-4-6", name: "analyst" },
@ -343,11 +344,11 @@ describe("CouncilConfigSchema", () => {
const result = CouncilConfigSchema.safeParse(config) const result = CouncilConfigSchema.safeParse(config)
//#then //#then
expect(result.success).toBe(false) expect(result.success).toBe(true)
}) })
test("rejects council with case-insensitive duplicate names", () => { test("accepts council with case-insensitive duplicate names for graceful runtime handling", () => {
//#given //#given - case-insensitive dedup is handled at runtime by registerCouncilMemberAgents
const config = { const config = {
members: [ members: [
{ model: "anthropic/claude-opus-4-6", name: "Claude" }, { model: "anthropic/claude-opus-4-6", name: "Claude" },
@ -359,7 +360,7 @@ describe("CouncilConfigSchema", () => {
const result = CouncilConfigSchema.safeParse(config) const result = CouncilConfigSchema.safeParse(config)
//#then //#then
expect(result.success).toBe(false) expect(result.success).toBe(true)
}) })
test("accepts council with unique member names", () => { test("accepts council with unique member names", () => {

View File

@ -20,13 +20,7 @@ export const CouncilMemberSchema = z.object({
}).strict() }).strict()
export const CouncilConfigSchema = z.object({ export const CouncilConfigSchema = z.object({
members: z.array(CouncilMemberSchema).min(2).refine( members: z.array(CouncilMemberSchema).min(2),
(members) => {
const names = members.map(m => m.name.toLowerCase())
return new Set(names).size === names.length
},
{ message: "Council member names must be unique (case-insensitive)" },
),
}).strict() }).strict()
export type CouncilMemberConfig = z.infer<typeof CouncilMemberSchema> export type CouncilMemberConfig = z.infer<typeof CouncilMemberSchema>