fix(athena): address 9 council-audit findings — dead code, bugs, and hardening
Fixes from multi-model council audit (7 members, 19 findings, 9 selected): - Use parseModelString() for cross-provider Anthropic thinking config (#3) - Update stale AGENTS.md athena directory listing (#4) - Replace prompt in appendMissingCouncilPrompt instead of appending (#5) - Extract duplicated session cleanup logic in agent-switch hook (#6) - Surface skipped council members when >=2 valid members exist (#9) - Expand fallback handoff regex with negation guards (#11) - Remove dead council-member agent from agentSources and tests (#12) - Make runtime council member duplicate check case-insensitive (#14) - Fix false-positive schema tests by adding required name field (#18)
This commit is contained in:
@@ -51,11 +51,10 @@ agents/
|
||||
├── momus.ts # Plan review
|
||||
├── atlas/agent.ts # Todo orchestrator
|
||||
├── athena/ # Multi-model council orchestrator
|
||||
│ ├── agent.ts # Athena agent factory
|
||||
│ ├── agent.ts # Athena agent factory + system prompt
|
||||
│ ├── council-member-agent.ts # Council member agent factory
|
||||
│ ├── model-parser.ts # Model string parser
|
||||
│ ├── types.ts # Council types
|
||||
│ └── index.ts # Barrel exports
|
||||
│ ├── model-thinking-config.ts # Per-provider thinking/reasoning config
|
||||
│ └── model-thinking-config.test.ts # Tests for thinking config
|
||||
├── types.ts # AgentFactory, AgentMode
|
||||
├── agent-builder.ts # buildAgent() composition
|
||||
├── utils.ts # Agent utilities
|
||||
|
||||
@@ -52,4 +52,30 @@ describe("applyModelThinkingConfig", () => {
|
||||
expect(result).toBe(BASE_CONFIG)
|
||||
})
|
||||
})
|
||||
|
||||
describe("given a Claude model through a non-Anthropic provider", () => {
|
||||
it("returns thinking config for github-copilot/claude-opus-4-6", () => {
|
||||
const result = applyModelThinkingConfig(BASE_CONFIG, "github-copilot/claude-opus-4-6")
|
||||
expect(result).toEqual({
|
||||
...BASE_CONFIG,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
})
|
||||
})
|
||||
|
||||
it("returns thinking config for opencode/claude-opus-4-6", () => {
|
||||
const result = applyModelThinkingConfig(BASE_CONFIG, "opencode/claude-opus-4-6")
|
||||
expect(result).toEqual({
|
||||
...BASE_CONFIG,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
})
|
||||
})
|
||||
|
||||
it("returns thinking config for opencode/claude-sonnet-4-6", () => {
|
||||
const result = applyModelThinkingConfig(BASE_CONFIG, "opencode/claude-sonnet-4-6")
|
||||
expect(result).toEqual({
|
||||
...BASE_CONFIG,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { parseModelString } from "../../tools/delegate-task/model-string-parser"
|
||||
import { isGptModel } from "../types"
|
||||
|
||||
export function applyModelThinkingConfig(base: AgentConfig, model: string): AgentConfig {
|
||||
@@ -6,10 +7,12 @@ export function applyModelThinkingConfig(base: AgentConfig, model: string): Agen
|
||||
return { ...base, reasoningEffort: "medium" }
|
||||
}
|
||||
|
||||
const slashIndex = model.indexOf("/")
|
||||
const provider = slashIndex > 0 ? model.substring(0, slashIndex).toLowerCase() : ""
|
||||
const parsed = parseModelString(model)
|
||||
if (!parsed) {
|
||||
return base
|
||||
}
|
||||
|
||||
if (provider === "anthropic") {
|
||||
if (parsed.providerID.toLowerCase() === "anthropic" || parsed.modelID.startsWith("claude")) {
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { createMomusAgent, momusPromptMetadata } from "./momus"
|
||||
import { createHephaestusAgent } from "./hephaestus"
|
||||
import { createSisyphusJuniorAgentWithOverrides } from "./sisyphus-junior"
|
||||
import { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./athena/agent"
|
||||
import { createCouncilMemberAgent } from "./athena/council-member-agent"
|
||||
import type { AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||
import {
|
||||
fetchAvailableModels,
|
||||
@@ -45,7 +44,6 @@ const agentSources: Partial<Record<BuiltinAgentName, AgentSource>> = {
|
||||
metis: createMetisAgent,
|
||||
momus: createMomusAgent,
|
||||
athena: createAthenaAgent,
|
||||
"council-member": createCouncilMemberAgent,
|
||||
// Note: Atlas is handled specially in createBuiltinAgents()
|
||||
// because it needs OrchestratorContext, not just a model string
|
||||
atlas: createAtlasAgent as AgentFactory,
|
||||
@@ -196,7 +194,14 @@ export async function createBuiltinAgents(
|
||||
|
||||
if (registeredKeys.length > 0) {
|
||||
const memberList = registeredKeys.map((key) => `- "${key}"`).join("\n")
|
||||
const councilTaskInstructions = `\n\n## Registered Council Members\n\nUse these as subagent_type in task calls:\n\n${memberList}`
|
||||
let councilTaskInstructions = `\n\n## Registered Council Members\n\nUse these as subagent_type in task calls:\n\n${memberList}`
|
||||
|
||||
if (skippedMembers.length > 0) {
|
||||
const skipDetails = skippedMembers.map((m) => `- **${m.name}**: ${m.reason}`).join("\n")
|
||||
councilTaskInstructions += `\n\n> **Note**: Some configured council members were skipped:\n${skipDetails}`
|
||||
log("[builtin-agents] Some council members were skipped during registration", { skippedMembers })
|
||||
}
|
||||
|
||||
result["athena"] = {
|
||||
...result["athena"],
|
||||
prompt: (result["athena"].prompt ?? "") + councilTaskInstructions,
|
||||
|
||||
@@ -39,14 +39,15 @@ Each member requires \`model\` (\`"provider/model-id"\` format) and \`name\` (di
|
||||
After informing the user, **end your turn**. Do NOT try to work around this by using generic agents, the council-member agent, or any other fallback.`
|
||||
|
||||
/**
|
||||
* Replaces Athena's prompt with a guard that tells the user to configure council members.
|
||||
* Replaces Athena's orchestration prompt with a guard that tells the user to configure council members.
|
||||
* The original prompt is discarded to avoid contradictory instructions.
|
||||
* Used when Athena is registered but no valid council config exists.
|
||||
*/
|
||||
export function appendMissingCouncilPrompt(
|
||||
athenaConfig: AgentConfig,
|
||||
skippedMembers?: Array<{ name: string; reason: string }>,
|
||||
): AgentConfig {
|
||||
let prompt = (athenaConfig.prompt ?? "") + MISSING_COUNCIL_PROMPT
|
||||
let prompt = MISSING_COUNCIL_PROMPT
|
||||
|
||||
if (skippedMembers && skippedMembers.length > 0) {
|
||||
const skipDetails = skippedMembers.map((m) => `- **${m.name}**: ${m.reason}`).join("\n")
|
||||
|
||||
@@ -27,6 +27,7 @@ export function registerCouncilMemberAgents(
|
||||
const agents: Record<string, AgentConfig> = {}
|
||||
const registeredKeys: string[] = []
|
||||
const skippedMembers: SkippedMember[] = []
|
||||
const registeredNamesLower = new Set<string>()
|
||||
|
||||
for (const member of councilConfig.members) {
|
||||
const parsed = parseModelString(member.model)
|
||||
@@ -40,16 +41,16 @@ export function registerCouncilMemberAgents(
|
||||
}
|
||||
|
||||
const key = getCouncilMemberAgentKey(member)
|
||||
const nameLower = member.name.toLowerCase()
|
||||
|
||||
if (agents[key]) {
|
||||
if (registeredNamesLower.has(nameLower)) {
|
||||
skippedMembers.push({
|
||||
name: member.name,
|
||||
reason: `Duplicate name: '${member.name}' already registered`,
|
||||
reason: `Duplicate name: '${member.name}' already registered (case-insensitive match)`,
|
||||
})
|
||||
log("[council-member-agents] Skipping duplicate council member name", {
|
||||
name: member.name,
|
||||
model: member.model,
|
||||
existingModel: agents[key].model ?? "unknown",
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -66,6 +67,7 @@ export function registerCouncilMemberAgents(
|
||||
}
|
||||
|
||||
registeredKeys.push(key)
|
||||
registeredNamesLower.add(nameLower)
|
||||
|
||||
log("[council-member-agents] Registered council member agent", {
|
||||
key,
|
||||
|
||||
Reference in New Issue
Block a user