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:
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import { describe, expect, it } from "bun:test"
|
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_JUNIOR_PROMPT_METADATA } from "./athena-junior-agent"
|
||||||
import { ATHENA_NON_INTERACTIVE_PROMPT } from "./non-interactive-prompt"
|
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("Athena-Junior prompt metadata", () => {
|
||||||
describe("#given ATHENA_JUNIOR_PROMPT_METADATA", () => {
|
describe("#given ATHENA_JUNIOR_PROMPT_METADATA", () => {
|
||||||
describe("#when checking triggers", () => {
|
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) =>
|
const hasNonInteractiveTrigger = ATHENA_JUNIOR_PROMPT_METADATA.triggers.some((t) =>
|
||||||
t.domain.includes("Non-interactive"),
|
t.domain.includes("multi-model analysis"),
|
||||||
)
|
)
|
||||||
expect(hasNonInteractiveTrigger).toBe(true)
|
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("Non-interactive prompt config injection placeholders", () => {
|
||||||
describe("#given the non-interactive prompt", () => {
|
describe("#given the non-interactive prompt", () => {
|
||||||
describe("#when checking for config placeholders", () => {
|
describe("#when checking for config placeholders", () => {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export const ATHENA_JUNIOR_PROMPT_METADATA: AgentPromptMetadata = {
|
|||||||
cost: "EXPENSIVE",
|
cost: "EXPENSIVE",
|
||||||
promptAlias: "Athena-Junior",
|
promptAlias: "Athena-Junior",
|
||||||
triggers: [
|
triggers: [
|
||||||
{ domain: "Non-interactive council", trigger: "Agent needs multi-model analysis without user interaction" },
|
{ 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 processing" },
|
{ domain: "Programmatic synthesis", trigger: "Need structured council output for automated pipelines, retries, or downstream machine processing" },
|
||||||
],
|
],
|
||||||
useWhen: [
|
useWhen: [
|
||||||
"CLI invocation via oh-my-opencode run needing structured council output",
|
"CLI invocation via oh-my-opencode run needing structured council output",
|
||||||
|
|||||||
@@ -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.
|
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.
|
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/\`.
|
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.
|
If the user wants output saved elsewhere (e.g., \`docs/\`), delegate via switch_agent to Atlas.
|
||||||
</identity>
|
</identity>
|
||||||
@@ -31,7 +33,9 @@ C) **Council-worthy & clear** ("should we", "evaluate", "compare", "review", "an
|
|||||||
-> Proceed directly to Step 2. No routing question.
|
-> Proceed directly to Step 2. No routing question.
|
||||||
|
|
||||||
D) **Simple/factual** ("what does X do", "where is Y", "explain Z")
|
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")
|
E) **Tool/action** ("run this", "call glob", "read this file")
|
||||||
-> Just do it.
|
-> Just do it.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
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 { CategoryConfig, GitMasterConfig } from "../../config/schema"
|
||||||
import type { BrowserAutomationProvider } from "../../config/schema"
|
import type { BrowserAutomationProvider } from "../../config/schema"
|
||||||
import type { AvailableAgent } from "../dynamic-agent-prompt-builder"
|
import type { AvailableAgent } from "../dynamic-agent-prompt-builder"
|
||||||
@@ -110,10 +110,13 @@ export function collectPendingBuiltinAgents(input: {
|
|||||||
|
|
||||||
const metadata = agentMetadata[agentName]
|
const metadata = agentMetadata[agentName]
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
|
const mode = isFactory(source) ? source.mode : (config.mode as AgentMode | undefined)
|
||||||
|
|
||||||
availableAgents.push({
|
availableAgents.push({
|
||||||
name: agentName,
|
name: agentName,
|
||||||
description: config.description ?? "",
|
description: config.description ?? "",
|
||||||
metadata,
|
metadata,
|
||||||
|
mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import type { SwitchAgentArgs } from "./types"
|
|||||||
const DESCRIPTION =
|
const DESCRIPTION =
|
||||||
"Switch the active session agent. After calling this tool, the session will transition to the specified agent " +
|
"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 " +
|
"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"])
|
const ALLOWED_AGENTS = new Set(["atlas", "prometheus", "sisyphus", "hephaestus", "athena"])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user