feat(athena): split into athena (interactive) and athena-junior (subagent)

Split the Athena council orchestrator into two distinct agents:
- athena: primary mode, interactive prompt with Question tool and switch_agent
- athena-junior: subagent mode, non-interactive prompt with structured JSON output

Key changes:
- Create athena-junior-agent.ts factory (mode=subagent, denies question+call_omo_agent)
- Revert athena agent.ts to primary mode (no env var switching)
- Make buildAthenaRuntimeGuidance mode-aware (strips action_paths for non-interactive)
- Add mode parameter to council_finalize tool
- Register athena-junior in builtin-agents, tool-config, model-requirements
- Replace "athena" with "athena-junior" in call_omo_agent ALLOWED_AGENTS
- Update all tests for new architecture

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
ismeth
2026-03-03 00:04:31 +01:00
committed by YeonGyu-Kim
parent deb2a2754e
commit 149d065d68
14 changed files with 270 additions and 70 deletions
+74 -55
View File
@@ -1,27 +1,14 @@
/// <reference types="bun-types" />
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
import { describe, expect, it } from "bun:test"
import { createAthenaAgent } from "./agent"
import { createAthenaJuniorAgent } from "./athena-junior-agent"
describe("createAthenaAgent", () => {
const originalEnv = process.env.OPENCODE_CLI_RUN_MODE
beforeEach(() => {
delete process.env.OPENCODE_CLI_RUN_MODE
})
afterEach(() => {
if (originalEnv !== undefined) {
process.env.OPENCODE_CLI_RUN_MODE = originalEnv
} else {
delete process.env.OPENCODE_CLI_RUN_MODE
}
})
describe("#given the agent mode", () => {
describe("#when accessing the static mode property", () => {
it("#then equals 'all' to support both primary and subagent contexts", () => {
expect(createAthenaAgent.mode).toBe("all")
it("#then equals 'primary'", () => {
expect(createAthenaAgent.mode).toBe("primary")
})
})
})
@@ -43,23 +30,27 @@ describe("createAthenaAgent", () => {
expect(config.permission).toBeDefined()
})
it("#then includes a description", () => {
it("#then includes a description containing synthesis strategist", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.description).toContain("synthesis strategist")
})
})
})
describe("#given default invocation (no CLI mode)", () => {
describe("#when OPENCODE_CLI_RUN_MODE is not set", () => {
it("#then selects the interactive prompt", () => {
describe("#given the interactive prompt", () => {
describe("#when checking prompt content", () => {
it("#then contains Question tool references", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("Question tool")
})
it("#then contains interactive workflow sections", () => {
it("#then contains Step 2: Council setup section", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("Step 2: Council setup")
})
it("#then contains agent_handoff section", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("Step 2: Council setup (default flow before launch).")
expect(config.prompt).toContain("<agent_handoff>")
})
@@ -67,40 +58,68 @@ describe("createAthenaAgent", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).not.toContain("<athena_council_result>")
})
})
})
describe("#given CLI run mode", () => {
describe("#when OPENCODE_CLI_RUN_MODE is 'true'", () => {
beforeEach(() => {
process.env.OPENCODE_CLI_RUN_MODE = "true"
})
it("#then selects the non-interactive prompt", () => {
it("#then does not contain NEVER use the Question tool constraint", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("<athena_council_result>")
})
it("#then contains non-interactive constraints", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("NEVER use the Question tool")
})
it("#then does not contain interactive agent handoff section", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).not.toContain("<agent_handoff>")
})
})
describe("#when OPENCODE_CLI_RUN_MODE is 'false'", () => {
beforeEach(() => {
process.env.OPENCODE_CLI_RUN_MODE = "false"
})
it("#then selects the interactive prompt", () => {
const config = createAthenaAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("Question tool")
expect(config.prompt).not.toContain("<athena_council_result>")
expect(config.prompt).not.toContain("NEVER use the Question tool")
})
})
})
})
describe("createAthenaJuniorAgent", () => {
describe("#given the agent mode", () => {
describe("#when accessing the static mode property", () => {
it("#then equals 'subagent'", () => {
expect(createAthenaJuniorAgent.mode).toBe("subagent")
})
})
})
describe("#given the agent config", () => {
describe("#when creating the agent with a model", () => {
it("#then returns config with the specified model", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.model).toBe("anthropic/claude-opus-4-6")
})
it("#then sets temperature to 0.1", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.temperature).toBe(0.1)
})
it("#then includes tool restrictions denying call_omo_agent and question", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.permission).toBeDefined()
})
it("#then includes a description containing Non-interactive", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.description).toContain("Non-interactive")
})
})
})
describe("#given the non-interactive prompt", () => {
describe("#when checking prompt content", () => {
it("#then contains athena_council_result output contract", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("<athena_council_result>")
})
it("#then contains NEVER use the Question tool constraint", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.prompt).toContain("NEVER use the Question tool")
})
it("#then does not contain agent_handoff section", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.prompt).not.toContain("<agent_handoff>")
})
it("#then does not contain switch_agent references", () => {
const config = createAthenaJuniorAgent("anthropic/claude-opus-4-6")
expect(config.prompt).not.toContain("switch_agent")
})
})
})
@@ -2,6 +2,7 @@
import { describe, expect, it } from "bun:test"
import { ATHENA_PROMPT_METADATA, createAthenaAgent } from "./agent"
import { ATHENA_JUNIOR_PROMPT_METADATA } from "./athena-junior-agent"
import { ATHENA_NON_INTERACTIVE_PROMPT } from "./non-interactive-prompt"
describe("Athena prompt config injection placeholders", () => {
@@ -136,24 +137,62 @@ describe("Athena prompt config injection placeholders", () => {
describe("Athena prompt metadata", () => {
describe("#given ATHENA_PROMPT_METADATA", () => {
describe("#when checking triggers", () => {
it("#then includes a Non-interactive council trigger", () => {
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", () => {
const hasNonInteractiveTrigger = ATHENA_JUNIOR_PROMPT_METADATA.triggers.some((t) =>
t.domain.includes("Non-interactive"),
)
expect(hasNonInteractiveTrigger).toBe(true)
})
})
describe("#when checking useWhen entries", () => {
it("#then includes an entry mentioning oh-my-opencode run", () => {
const hasCLIEntry = ATHENA_PROMPT_METADATA.useWhen.some((entry) =>
const hasCLIEntry = ATHENA_JUNIOR_PROMPT_METADATA.useWhen.some((entry) =>
entry.includes("oh-my-opencode run"),
)
expect(hasCLIEntry).toBe(true)
})
it("#then includes an entry mentioning structured council output", () => {
const hasStructuredEntry = ATHENA_PROMPT_METADATA.useWhen.some((entry) =>
it("#then includes an entry mentioning structured or agent-to-agent", () => {
const hasStructuredEntry = ATHENA_JUNIOR_PROMPT_METADATA.useWhen.some((entry) =>
entry.includes("structured") || entry.includes("agent-to-agent"),
)
expect(hasStructuredEntry).toBe(true)
+40
View File
@@ -0,0 +1,40 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode, AgentPromptMetadata } from "../types"
import { createAgentToolRestrictions } from "../../shared/permission-compat"
import { ATHENA_NON_INTERACTIVE_PROMPT } from "./non-interactive-prompt"
const MODE: AgentMode = "subagent"
export const ATHENA_JUNIOR_PROMPT_METADATA: AgentPromptMetadata = {
category: "advisor",
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" },
],
useWhen: [
"CLI invocation via oh-my-opencode run needing structured council output",
"Agent-to-agent invocation where structured <athena_council_result> JSON is required",
"Automated pipelines needing multi-model consensus",
],
avoidWhen: [
"Interactive sessions where user can confirm actions (use athena instead)",
"Single-model questions that do not need council synthesis",
],
}
export function createAthenaJuniorAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions(["call_omo_agent", "question"])
return {
description:
"Non-interactive council orchestrator for programmatic multi-model synthesis. Returns structured <athena_council_result> JSON without user interaction. (Athena-Junior - OhMyOpenCode)",
mode: MODE,
model,
temperature: 0.1,
permission: restrictions.permission,
prompt: ATHENA_NON_INTERACTIVE_PROMPT,
color: "#1F8EFA",
}
}
createAthenaJuniorAgent.mode = MODE
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test"
import { resolveCouncilIntent, buildAthenaRuntimeGuidance, getValidCouncilIntents } from "./council-runtime-guidance"
import { resolveCouncilIntent, buildAthenaRuntimeGuidance, getValidCouncilIntents, type CouncilGuidanceMode } from "./council-runtime-guidance"
describe("council-runtime-guidance", () => {
describe("resolveCouncilIntent", () => {
@@ -116,7 +116,7 @@ describe("council-runtime-guidance", () => {
})
})
describe("#when building guidance for each intent", () => {
describe("#when building guidance for each intent in interactive mode (default)", () => {
const allIntents = ["DIAGNOSE", "AUDIT", "PLAN", "EVALUATE", "EXPLAIN", "CREATE", "PERSPECTIVES", "FREEFORM"] as const
for (const intent of allIntents) {
@@ -125,7 +125,7 @@ describe("council-runtime-guidance", () => {
expect(result).toContain("runtime_synthesis_rules")
})
it(`#then ${intent} guidance contains runtime_action_paths`, () => {
it(`#then ${intent} guidance contains runtime_action_paths in interactive mode`, () => {
const result = buildAthenaRuntimeGuidance(intent)
expect(result).toContain("runtime_action_paths")
})
@@ -139,6 +139,62 @@ describe("council-runtime-guidance", () => {
})
})
describe("#given non-interactive mode", () => {
const allIntents = ["DIAGNOSE", "AUDIT", "PLAN", "EVALUATE", "EXPLAIN", "CREATE", "PERSPECTIVES", "FREEFORM"] as const
describe("#when building guidance with non-interactive mode", () => {
for (const intent of allIntents) {
it(`#then ${intent} guidance contains runtime_synthesis_rules`, () => {
const result = buildAthenaRuntimeGuidance(intent, "non-interactive")
expect(result).toContain("runtime_synthesis_rules")
})
it(`#then ${intent} guidance does NOT contain runtime_action_paths`, () => {
const result = buildAthenaRuntimeGuidance(intent, "non-interactive")
expect(result).not.toContain("runtime_action_paths")
})
it(`#then ${intent} guidance contains mode: non-interactive`, () => {
const result = buildAthenaRuntimeGuidance(intent, "non-interactive")
expect(result).toContain("mode: non-interactive")
})
it(`#then ${intent} guidance contains source: council_finalize`, () => {
const result = buildAthenaRuntimeGuidance(intent, "non-interactive")
expect(result).toContain("source: council_finalize")
})
}
})
describe("#when building guidance with explicit interactive mode", () => {
for (const intent of allIntents) {
it(`#then ${intent} guidance contains both synthesis_rules and action_paths`, () => {
const result = buildAthenaRuntimeGuidance(intent, "interactive")
expect(result).toContain("runtime_synthesis_rules")
expect(result).toContain("runtime_action_paths")
})
it(`#then ${intent} guidance contains mode: interactive`, () => {
const result = buildAthenaRuntimeGuidance(intent, "interactive")
expect(result).toContain("mode: interactive")
})
}
})
})
describe("CouncilGuidanceMode type", () => {
describe("#given the type is imported", () => {
describe("#when used as a type annotation", () => {
it("#then accepts interactive and non-interactive values", () => {
const interactive: CouncilGuidanceMode = "interactive"
const nonInteractive: CouncilGuidanceMode = "non-interactive"
expect(interactive).toBe("interactive")
expect(nonInteractive).toBe("non-interactive")
})
})
})
})
describe("getValidCouncilIntents", () => {
describe("#given the function is called", () => {
describe("#when retrieving valid intents", () => {
+15 -2
View File
@@ -50,12 +50,25 @@ export function resolveCouncilIntent(intent?: string): CouncilIntent | null {
: null
}
export function buildAthenaRuntimeGuidance(intent: CouncilIntent): string {
export type CouncilGuidanceMode = "interactive" | "non-interactive"
export function buildAthenaRuntimeGuidance(intent: CouncilIntent, mode: CouncilGuidanceMode = "interactive"): string {
let guidanceContent = RUNTIME_GUIDANCE_BY_INTENT[intent].trim()
if (mode === "non-interactive") {
guidanceContent = stripActionPaths(guidanceContent)
}
return [
"<athena_runtime_guidance>",
"source: council_finalize",
`intent: ${intent}`,
RUNTIME_GUIDANCE_BY_INTENT[intent].trim(),
`mode: ${mode}`,
guidanceContent,
"</athena_runtime_guidance>",
].join("\n\n")
}
function stripActionPaths(guidance: string): string {
return guidance.replace(/<runtime_action_paths>[\s\S]*?<\/runtime_action_paths>/g, "").trim()
}
+2 -1
View File
@@ -1,4 +1,5 @@
export { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./agent"
export { createAthenaJuniorAgent, ATHENA_JUNIOR_PROMPT_METADATA } from "./athena-junior-agent"
export { createCouncilMemberAgent, COUNCIL_MEMBER_PROMPT, COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM } from "./council-member-agent"
export { COUNCIL_INTENT_ADDENDUMS } from "./council-intent-addendums"
export {
@@ -6,7 +7,7 @@ export {
getValidCouncilIntents,
resolveCouncilIntent,
} from "./council-runtime-guidance"
export type { CouncilIntent } from "./council-runtime-guidance"
export type { CouncilIntent, CouncilGuidanceMode } from "./council-runtime-guidance"
export { COUNCIL_DEFAULTS } from "./constants"
export { ATHENA_INTERACTIVE_PROMPT } from "./interactive-prompt"
export { ATHENA_NON_INTERACTIVE_PROMPT } from "./non-interactive-prompt"
+1 -1
View File
@@ -72,7 +72,7 @@ Launch ALL members before collecting results. Track every task_id.
- Repeat until ALL members reach terminal state.
### Step 6: Collect results with council_finalize.
- Call: council_finalize(task_ids=[...], name="{topic-slug}", intent="{intent}", question="{original question}", prompt_file="{path from Step 4.1}")
- Call: council_finalize(task_ids=[...], name="{topic-slug}", intent="{intent}", question="{original question}", prompt_file="{path from Step 4.1}", mode="non-interactive")
- council_finalize extracts responses, writes archives, returns structured JSON with archive_dir and members array.
- Read each member's archive_file using Read tool for synthesis input.