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" />
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user