From efc53de531920a754f57d37b66dbca5e049213ba Mon Sep 17 00:00:00 2001 From: ismeth Date: Tue, 3 Mar 2026 13:21:40 +0100 Subject: [PATCH] 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 --- .../athena/athena-config-injection.test.ts | 96 +++++++++++-------- src/agents/athena/athena-junior-agent.ts | 4 +- src/agents/athena/interactive-prompt.ts | 6 +- src/agents/builtin-agents/general-agents.ts | 5 +- src/tools/switch-agent/tools.ts | 4 +- 5 files changed, 69 insertions(+), 46 deletions(-) diff --git a/src/agents/athena/athena-config-injection.test.ts b/src/agents/athena/athena-config-injection.test.ts index d6b362572..2c5606cdb 100644 --- a/src/agents/athena/athena-config-injection.test.ts +++ b/src/agents/athena/athena-config-injection.test.ts @@ -1,7 +1,7 @@ /// 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", () => { diff --git a/src/agents/athena/athena-junior-agent.ts b/src/agents/athena/athena-junior-agent.ts index 020bb2593..7bf4f5a39 100644 --- a/src/agents/athena/athena-junior-agent.ts +++ b/src/agents/athena/athena-junior-agent.ts @@ -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", diff --git a/src/agents/athena/interactive-prompt.ts b/src/agents/athena/interactive-prompt.ts index 0332d5486..befc7c950 100644 --- a/src/agents/athena/interactive-prompt.ts +++ b/src/agents/athena/interactive-prompt.ts @@ -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. @@ -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. diff --git a/src/agents/builtin-agents/general-agents.ts b/src/agents/builtin-agents/general-agents.ts index 9541d23c2..556e9d4af 100644 --- a/src/agents/builtin-agents/general-agents.ts +++ b/src/agents/builtin-agents/general-agents.ts @@ -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, }) } } diff --git a/src/tools/switch-agent/tools.ts b/src/tools/switch-agent/tools.ts index 608b808d9..f4630d714 100644 --- a/src/tools/switch-agent/tools.ts +++ b/src/tools/switch-agent/tools.ts @@ -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"])