fix(athena): harden council flow and resilience config

This commit is contained in:
ismeth
2026-02-28 20:26:35 +01:00
committed by YeonGyu-Kim
parent e52ccd1b04
commit ed2bb1ead7
8 changed files with 105 additions and 21 deletions
-1
View File
@@ -34,7 +34,6 @@ Agent factories following `createXXXAgent(model) → AgentConfig` pattern. Each
| Multimodal-Looker | ALL except read |
| Atlas | task, call_omo_agent |
| Momus | write, edit, task |
| Athena | write, edit, call_omo_agent |
| Council-Member | ALL except read, grep, glob, lsp_*, ast_grep_search (allow-list) |
## STRUCTURE
@@ -14,6 +14,10 @@ describe("Athena prompt config injection placeholders", () => {
expect(athenaConfig.prompt).toContain("{STUCK_THRESHOLD_SECONDS}")
})
it("#then contains MEMBER_MAX_RUNNING_SECONDS placeholder", () => {
expect(athenaConfig.prompt).toContain("{MEMBER_MAX_RUNNING_SECONDS}")
})
it("#then contains quorum reference", () => {
expect(athenaConfig.prompt).toContain("quorum")
})
@@ -29,6 +33,75 @@ describe("Athena prompt config injection placeholders", () => {
it("#then contains CANCEL_RETRYING_ON_QUORUM placeholder", () => {
expect(athenaConfig.prompt).toContain("{CANCEL_RETRYING_ON_QUORUM}")
})
it("#then avoids session_id continuation retry instruction", () => {
expect(athenaConfig.prompt).not.toContain("Retries: use task(session_id=<member_session_id>")
})
it("#then uses sequential workflow step numbering", () => {
expect(athenaConfig.prompt).toContain("Step 12: Synthesize")
expect(athenaConfig.prompt).toContain("Step 13: Determine the follow-up path")
expect(athenaConfig.prompt).toContain("Step 14: ACTIONABLE findings")
expect(athenaConfig.prompt).toContain("Step 15: INFORMATIONAL findings")
})
it("#then omits legacy mixed step labels", () => {
expect(athenaConfig.prompt).not.toContain("Step 1.5")
expect(athenaConfig.prompt).not.toContain("Step 4.5")
expect(athenaConfig.prompt).not.toContain("Step 7A")
expect(athenaConfig.prompt).not.toContain("Step 7B")
})
it("#then uses tagged prompt sections for consistency", () => {
expect(athenaConfig.prompt).toContain("<identity>")
expect(athenaConfig.prompt).toContain("<workflow>")
expect(athenaConfig.prompt).toContain("<synthesis_rules>")
expect(athenaConfig.prompt).toContain("<action_paths>")
expect(athenaConfig.prompt).toContain("<constraints>")
})
it("#then places council setup after step 1 and removes first-action wording", () => {
const prompt = athenaConfig.prompt ?? ""
expect(prompt).not.toContain("CRITICAL: Council Setup (Your First Action)")
const step1Index = prompt.indexOf("Step 1: Understand the question and decide the route.")
const step2Index = prompt.indexOf("Step 2: Council setup (default flow before launch).")
expect(step1Index).toBeGreaterThan(-1)
expect(step2Index).toBeGreaterThan(step1Index)
})
it("#then keeps edge-case disambiguation inside step 3 classification", () => {
const prompt = athenaConfig.prompt ?? ""
expect(prompt).toContain("Step 3: Classify the question intent.")
expect(prompt).toContain("Classification disambiguation rule:")
expect(prompt).not.toContain("Before selecting a route, apply this interpretation rule")
})
it("#then requires question tool routing in self-answerable path", () => {
expect(athenaConfig.prompt).toContain("Athena MUST ask the user to choose direct answer vs council using the Question tool")
expect(athenaConfig.prompt).toContain("header: \"Routing\"")
expect(athenaConfig.prompt).toContain("label: \"Answer directly\"")
expect(athenaConfig.prompt).toContain("label: \"Consult council\"")
})
it("#then requires question tool clarifications in ambiguous council path", () => {
expect(athenaConfig.prompt).toContain("Use the Question tool (not open-ended free text) for 1-2 targeted clarifications")
expect(athenaConfig.prompt).toContain("Formulate the questions and options dynamically")
expect(athenaConfig.prompt).not.toContain("header: \"Output Type\"")
expect(athenaConfig.prompt).not.toContain("header: \"Scope\"")
})
it("#then keeps intent classification anchored to step 3 wording", () => {
expect(athenaConfig.prompt).toContain("Step 3: Classify the question intent.")
expect(athenaConfig.prompt).toContain("Then proceed to Step 2.")
expect(athenaConfig.prompt).not.toContain("Then classify intent and proceed to Step 2.")
expect(athenaConfig.prompt).not.toContain("Classify intent immediately and proceed to Step 2.")
})
it("#then excludes non-interactive mode branch from runtime prompt", () => {
expect(athenaConfig.prompt).not.toContain("Non-interactive mode (Question tool unavailable)")
})
})
})
})
@@ -63,7 +63,7 @@ You are conducting an **audit** — your goal is to find discrete issues, risks,
- Severity determines priority: critical (blocks/breaks), high (significant risk), medium (should fix), low (nice to fix)
- For each finding, provide the specific location (reference, section, or component where it occurs)
- State your confidence: high (clear evidence), medium (likely but needs verification), low (suspicion, investigate further)
- **This is a broad sweep, not a targeted trace.** If you are starting from a specific symptom and need to find its root cause, that is DIAGNOSE — not AUDIT.
- **This is a broad sweep, not a targeted trace.**.
**Required output fields per finding:**
- Title, Severity (critical/high/medium/low), Location, Confidence (high/medium/low)
+4 -18
View File
@@ -31,23 +31,7 @@ You MUST wrap your final analysis in <COUNCIL_MEMBER_RESPONSE> tags. This is how
- Exploration logs and intermediate reasoning
- Step-by-step search process
**Example structure:**
\`\`\`
<COUNCIL_MEMBER_RESPONSE>
## Point 1: [Title]
- **Observation**: [what you found]
- **Confidence**: high
- **Details**: [supporting evidence or reasoning]
## Point 2: [Title]
...
## Summary
[Overall assessment with confidence levels]
</COUNCIL_MEMBER_RESPONSE>
\`\`\`
If you do not wrap your response in these tags, your analysis will not be included in the synthesis.`
If you do not wrap your response in <COUNCIL_MEMBER_RESPONSE> tags, your analysis will not be included in the synthesis.`
export const COUNCIL_SOLO_ADDENDUM = `
## Solo Analysis Mode
@@ -85,7 +69,9 @@ background_output(task_id="<id>")
- Use \`librarian\` for documentation and external references
- Keep targeted file reads (Read tool) for yourself — delegate broad searches
- Collect results with \`background_output\` after \`background_wait\` returns
- Before generating your final \`<COUNCIL_MEMBER_RESPONSE>\`, cancel any remaining pending tasks with \`background_cancel\``
- Before generating your final \`<COUNCIL_MEMBER_RESPONSE>\`, wait for all the background tasks to finish.
- If you decide to form your final response before background tasks finishes, cancel any remaining pending tasks with \`background_cancel\`
`
export function createCouncilMemberAgent(model: string): AgentConfig {
// Allow-list: only read-only analysis tools + optional delegation.
+3 -1
View File
@@ -206,6 +206,7 @@ export async function createBuiltinAgents(
const retryIfFinished = councilConfig.retry_failed_if_others_finished ?? false
const cancelOnQuorum = councilConfig.cancel_retrying_on_quorum ?? true
const stuckThreshold = councilConfig.stuck_threshold_seconds ?? 120
const memberMaxRunning = councilConfig.member_max_running_seconds ?? 1800
let athenaPrompt = (result["athena"].prompt ?? "") + councilTaskInstructions
athenaPrompt = athenaPrompt
@@ -213,7 +214,8 @@ export async function createBuiltinAgents(
.replace(/\{RETRY_FAILED_IF_OTHERS_FINISHED\}/g, String(retryIfFinished))
.replace(/\{CANCEL_RETRYING_ON_QUORUM\}/g, String(cancelOnQuorum))
.replace(/\{STUCK_THRESHOLD_SECONDS\}/g, String(stuckThreshold))
athenaPrompt += `\n\n## Council Resilience Config\n- retry_on_fail: ${retryOnFail}\n- retry_failed_if_others_finished: ${retryIfFinished}\n- cancel_retrying_on_quorum: ${cancelOnQuorum}\n- stuck_threshold_seconds: ${stuckThreshold}`
.replace(/\{MEMBER_MAX_RUNNING_SECONDS\}/g, String(memberMaxRunning))
athenaPrompt += `\n\n## Council Resilience Config\n- retry_on_fail: ${retryOnFail}\n- retry_failed_if_others_finished: ${retryIfFinished}\n- cancel_retrying_on_quorum: ${cancelOnQuorum}\n- stuck_threshold_seconds: ${stuckThreshold}\n- member_max_running_seconds: ${memberMaxRunning}`
result["athena"] = {
...result["athena"],
@@ -13,6 +13,7 @@ describe("council-member-agents", () => {
retry_failed_if_others_finished: false,
cancel_retrying_on_quorum: true,
stuck_threshold_seconds: 120,
member_max_running_seconds: 1800,
}
//#when
const result = registerCouncilMemberAgents(config)
@@ -32,6 +33,7 @@ describe("council-member-agents", () => {
retry_failed_if_others_finished: false,
cancel_retrying_on_quorum: true,
stuck_threshold_seconds: 120,
member_max_running_seconds: 1800,
}
//#when
const result = registerCouncilMemberAgents(config)
@@ -52,6 +54,7 @@ describe("council-member-agents", () => {
retry_failed_if_others_finished: false,
cancel_retrying_on_quorum: true,
stuck_threshold_seconds: 120,
member_max_running_seconds: 1800,
}
//#when
const result = registerCouncilMemberAgents(config)
@@ -72,6 +75,7 @@ describe("council-member-agents", () => {
retry_failed_if_others_finished: false,
cancel_retrying_on_quorum: true,
stuck_threshold_seconds: 120,
member_max_running_seconds: 1800,
}
//#when
const result = registerCouncilMemberAgents(config)
+19
View File
@@ -469,6 +469,14 @@ describe("CouncilConfigSchema — resilience fields", () => {
expect(result.data.stuck_threshold_seconds).toBe(120)
}
})
it("#then applies default member_max_running_seconds of 1800", () => {
const result = CouncilConfigSchema.safeParse({ members: validMembers })
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.member_max_running_seconds).toBe(1800)
}
})
})
})
@@ -481,6 +489,7 @@ describe("CouncilConfigSchema — resilience fields", () => {
retry_failed_if_others_finished: true,
cancel_retrying_on_quorum: false,
stuck_threshold_seconds: 60,
member_max_running_seconds: 2400,
}
const result = CouncilConfigSchema.safeParse(config)
expect(result.success).toBe(true)
@@ -489,6 +498,7 @@ describe("CouncilConfigSchema — resilience fields", () => {
expect(result.data.retry_failed_if_others_finished).toBe(true)
expect(result.data.cancel_retrying_on_quorum).toBe(false)
expect(result.data.stuck_threshold_seconds).toBe(60)
expect(result.data.member_max_running_seconds).toBe(2400)
}
})
})
@@ -521,6 +531,15 @@ describe("CouncilConfigSchema — resilience fields", () => {
})
})
describe("#given member_max_running_seconds below minimum", () => {
describe("#when parsed with 30", () => {
it("#then fails validation", () => {
const result = CouncilConfigSchema.safeParse({ members: validMembers, member_max_running_seconds: 30 })
expect(result.success).toBe(false)
})
})
})
describe("#given backward-compatible config with only members", () => {
describe("#when parsed", () => {
it("#then succeeds without errors — new fields are optional", () => {
+1
View File
@@ -25,6 +25,7 @@ export const CouncilConfigSchema = z.object({
retry_failed_if_others_finished: z.boolean().default(false),
cancel_retrying_on_quorum: z.boolean().default(true),
stuck_threshold_seconds: z.number().min(30).default(120),
member_max_running_seconds: z.number().min(60).default(1800),
}).strict()
export type CouncilMemberConfig = z.infer<typeof CouncilMemberSchema>