feat(athena): complete delegation routing fix with mode-aware delegation table

- Re-add ATHENA_PROMPT_METADATA with tight triggers and avoidWhen guardrails
- Update switch_agent tool description with permanent handoff guardrail
- Make buildDelegationTable mode-aware: switch_agent for primary, task() for subagent
- Pass agent mode through general-agents to AvailableAgent interface
- Improve primary agent error messages in subagent-resolver with alternatives
- Add council-first bias to Athena interactive prompt
- Update athena-junior trigger text for clearer task() invocation guidance
- Add tests for ATHENA_PROMPT_METADATA shape, triggers, useWhen, avoidWhen
This commit is contained in:
ismeth
2026-03-03 13:21:40 +01:00
committed by YeonGyu-Kim
parent 7ffdd15b52
commit efc53de531
5 changed files with 69 additions and 46 deletions
@@ -1,7 +1,7 @@
/// <reference types="bun-types" />
import { describe, expect, it } from "bun:test"
import { ATHENA_PROMPT_METADATA, createAthenaAgent } from "./agent"
import { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./agent"
import { ATHENA_JUNIOR_PROMPT_METADATA } from "./athena-junior-agent"
import { ATHENA_NON_INTERACTIVE_PROMPT } from "./non-interactive-prompt"
@@ -134,50 +134,12 @@ describe("Athena prompt config injection placeholders", () => {
})
})
describe("Athena prompt metadata", () => {
describe("#given ATHENA_PROMPT_METADATA", () => {
describe("#when checking triggers", () => {
it("#then does NOT include a Non-interactive council trigger", () => {
const hasNonInteractiveTrigger = ATHENA_PROMPT_METADATA.triggers.some((t) =>
t.domain.includes("Non-interactive"),
)
expect(hasNonInteractiveTrigger).toBe(false)
})
})
describe("#when checking useWhen entries", () => {
it("#then does NOT include an entry mentioning oh-my-opencode run", () => {
const hasCLIEntry = ATHENA_PROMPT_METADATA.useWhen.some((entry) =>
entry.includes("oh-my-opencode run"),
)
expect(hasCLIEntry).toBe(false)
})
it("#then does NOT include an entry mentioning structured council output", () => {
const hasStructuredEntry = ATHENA_PROMPT_METADATA.useWhen.some((entry) =>
entry.includes("structured") || entry.includes("agent-to-agent"),
)
expect(hasStructuredEntry).toBe(false)
})
})
describe("#when checking avoidWhen entries", () => {
it("#then includes an entry referencing athena-junior", () => {
const hasAthenaJuniorRef = ATHENA_PROMPT_METADATA.avoidWhen?.some((entry) =>
entry.includes("athena-junior"),
)
expect(hasAthenaJuniorRef).toBe(true)
})
})
})
})
describe("Athena-Junior prompt metadata", () => {
describe("#given ATHENA_JUNIOR_PROMPT_METADATA", () => {
describe("#when checking triggers", () => {
it("#then includes a Non-interactive council trigger", () => {
it("#then includes a multi-model analysis trigger", () => {
const hasNonInteractiveTrigger = ATHENA_JUNIOR_PROMPT_METADATA.triggers.some((t) =>
t.domain.includes("Non-interactive"),
t.domain.includes("multi-model analysis"),
)
expect(hasNonInteractiveTrigger).toBe(true)
})
@@ -201,6 +163,58 @@ describe("Athena-Junior prompt metadata", () => {
})
})
describe("Athena prompt metadata", () => {
describe("#given ATHENA_PROMPT_METADATA", () => {
describe("#when checking triggers", () => {
it("#then includes a Multi-model council trigger", () => {
const hasCouncilTrigger = ATHENA_PROMPT_METADATA.triggers.some((t) =>
t.domain.includes("Multi-model council"),
)
expect(hasCouncilTrigger).toBe(true)
})
})
describe("#when checking useWhen entries", () => {
it("#then includes an entry mentioning tradeoffs", () => {
const hasTradeoffs = ATHENA_PROMPT_METADATA.useWhen?.some((entry) =>
entry.includes("tradeoffs"),
)
expect(hasTradeoffs).toBe(true)
})
})
describe("#when checking avoidWhen entries", () => {
it("#then includes an entry about implementation tasks", () => {
const hasImplWarning = ATHENA_PROMPT_METADATA.avoidWhen?.some((entry) =>
entry.includes("Implementation tasks"),
)
expect(hasImplWarning).toBe(true)
})
it("#then includes an entry about subtask misuse", () => {
const hasSubtaskWarning = ATHENA_PROMPT_METADATA.avoidWhen?.some((entry) =>
entry.includes("Subtasks"),
)
expect(hasSubtaskWarning).toBe(true)
})
})
describe("#when checking metadata shape", () => {
it("#then has category advisor", () => {
expect(ATHENA_PROMPT_METADATA.category).toBe("advisor")
})
it("#then has cost EXPENSIVE", () => {
expect(ATHENA_PROMPT_METADATA.cost).toBe("EXPENSIVE")
})
it("#then has promptAlias Athena", () => {
expect(ATHENA_PROMPT_METADATA.promptAlias).toBe("Athena")
})
})
})
})
describe("Non-interactive prompt config injection placeholders", () => {
describe("#given the non-interactive prompt", () => {
describe("#when checking for config placeholders", () => {
+2 -2
View File
@@ -10,8 +10,8 @@ export const ATHENA_JUNIOR_PROMPT_METADATA: AgentPromptMetadata = {
cost: "EXPENSIVE",
promptAlias: "Athena-Junior",
triggers: [
{ domain: "Non-interactive council", trigger: "Agent needs multi-model analysis without user interaction" },
{ domain: "Programmatic synthesis", trigger: "Need structured council output for automated processing" },
{ domain: "Agent needs multi-model analysis", trigger: "Use task(subagent_type=\"athena-junior\") when council synthesis is needed without interactive handoff" },
{ domain: "Programmatic synthesis", trigger: "Need structured council output for automated pipelines, retries, or downstream machine processing" },
],
useWhen: [
"CLI invocation via oh-my-opencode run needing structured council output",
+5 -1
View File
@@ -5,6 +5,8 @@ export const ATHENA_INTERACTIVE_PROMPT = `
You are Athena, a smart council orchestrator. You MAY use Read, Grep, Glob, LSP tools to understand questions before deciding how to route them.
Your primary job is to send the user's question to your council of AI models, then synthesize their responses.
**Council-first bias**: When in doubt between answering directly and launching the council, LEAN TOWARD THE COUNCIL. You are not a general-purpose assistant — your unique value IS multi-model synthesis. If you find yourself doing extensive investigation (3+ tool calls), pause and ask: "Would multiple perspectives improve this answer?" If yes, offer the council before presenting conclusions.
You may write synthesis documents and session notes to \`.sisyphus/\`. You CANNOT write files outside \`.sisyphus/\`.
If the user wants output saved elsewhere (e.g., \`docs/\`), delegate via switch_agent to Atlas.
</identity>
@@ -31,7 +33,9 @@ C) **Council-worthy & clear** ("should we", "evaluate", "compare", "review", "an
-> Proceed directly to Step 2. No routing question.
D) **Simple/factual** ("what does X do", "where is Y", "explain Z")
-> Answer directly using your tools, then append: "Want deeper multi-model analysis? I can launch the council."
-> Answer directly using your tools.
-> MANDATORY: After answering, append council offer: "Want deeper multi-model analysis? I can launch the council."
-> If your answer involves design decisions, architecture choices, or multiple valid approaches, ESCALATE to C instead — the council adds real value when there's no single "correct" answer.
E) **Tool/action** ("run this", "call glob", "read this file")
-> Just do it.
+4 -1
View File
@@ -1,5 +1,5 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { BuiltinAgentName, AgentOverrides, AgentPromptMetadata } from "../types"
import type { AgentMode, BuiltinAgentName, AgentOverrides, AgentPromptMetadata } from "../types"
import type { CategoryConfig, GitMasterConfig } from "../../config/schema"
import type { BrowserAutomationProvider } from "../../config/schema"
import type { AvailableAgent } from "../dynamic-agent-prompt-builder"
@@ -110,10 +110,13 @@ export function collectPendingBuiltinAgents(input: {
const metadata = agentMetadata[agentName]
if (metadata) {
const mode = isFactory(source) ? source.mode : (config.mode as AgentMode | undefined)
availableAgents.push({
name: agentName,
description: config.description ?? "",
metadata,
mode,
})
}
}
+3 -1
View File
@@ -6,7 +6,9 @@ import type { SwitchAgentArgs } from "./types"
const DESCRIPTION =
"Switch the active session agent. After calling this tool, the session will transition to the specified agent " +
"with the provided context as its starting prompt. Use this to route work to another agent " +
"(e.g., Atlas for fixes, Prometheus for planning). The switch executes when the current agent's turn completes."
"(e.g., Atlas for fixes, Prometheus for planning). The switch executes when the current agent's turn completes.\n\n" +
"Permanent one-way handoff. Use ONLY when you're the wrong agent for the overall job, NEVER for subtasks (use task()). " +
"Targets: atlas, prometheus, sisyphus, hephaestus, athena (multi-model council)."
const ALLOWED_AGENTS = new Set(["atlas", "prometheus", "sisyphus", "hephaestus", "athena"])