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.
@@ -136,6 +136,16 @@ export function applyToolConfig(params: {
question: questionPermission,
};
}
const athenaJunior = agentByKey(params.agentResult, "athena-junior");
if (athenaJunior) {
athenaJunior.permission = {
...athenaJunior.permission,
task: "allow",
prepare_council_prompt: "allow",
council_finalize: "allow",
question: "deny",
};
}
params.config.permission = {
webfetch: "allow",
+5
View File
@@ -51,6 +51,11 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
call_omo_agent: false,
},
"athena-junior": {
call_omo_agent: false,
question: false,
},
// NOTE: Athena/council tool restrictions are also defined in:
// - src/agents/athena/agent.ts (AgentConfig permission format)
// - src/agents/athena/council-member-agent.ts (AgentConfig permission format — allow-list)
+10
View File
@@ -190,6 +190,16 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
],
},
"athena-junior": {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
{ providers: ["opencode"], model: "glm-4.7-free" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" },
],
},
"council-member": {
fallbackChain: [
{ providers: ["opencode"], model: "gpt-5-nano" },
+1 -1
View File
@@ -6,7 +6,7 @@ export const ALLOWED_AGENTS = [
"metis",
"momus",
"multimodal-looker",
"athena",
"athena-junior",
] as const
export const CALL_OMO_AGENT_DESCRIPTION = `Spawn explore/librarian agent or custom agents. run_in_background REQUIRED (true=async with task_id, false=sync).
+2 -2
View File
@@ -512,7 +512,7 @@ describe("createCallOmoAgent", () => {
expect(result).toContain("background_task.maxDepth=3")
})
test("should accept athena as a valid agent type", async () => {
test("should accept athena-junior as a valid agent type", async () => {
//#given
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
const executeFunc = toolDef.execute as Function
@@ -522,7 +522,7 @@ describe("createCallOmoAgent", () => {
{
description: "Test",
prompt: "Test prompt",
subagent_type: "athena",
subagent_type: "athena-junior",
run_in_background: true,
},
{ sessionID: "test", messageID: "msg", agent: "test", abort: new AbortController().signal }
@@ -11,6 +11,7 @@ import {
resolveCouncilIntent,
COUNCIL_DEFAULTS,
} from "../../agents/athena"
import type { CouncilGuidanceMode } from "../../agents/athena"
import type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types"
export function createCouncilFinalize(
@@ -30,6 +31,10 @@ export function createCouncilFinalize(
.describe(`Classified question intent used for runtime Athena guidance injection. Valid intents: ${getValidCouncilIntents().join(", ")}`),
question: tool.schema.string().optional().describe("Original user question that triggered the council"),
prompt_file: tool.schema.string().optional().describe("Path to the council prompt temp file (will be moved into the archive)"),
mode: tool.schema
.string()
.optional()
.describe('Council guidance mode: "interactive" (default) includes action paths with Question tool, "non-interactive" strips action paths for programmatic use'),
},
async execute(args: CouncilFinalizeArgs, toolContext) {
const resolvedIntent = resolveCouncilIntent(args.intent)
@@ -154,7 +159,8 @@ export function createCouncilFinalize(
members,
}
const guidance = buildAthenaRuntimeGuidance(resolvedIntent)
const resolvedMode = (args.mode === "non-interactive" ? "non-interactive" : "interactive") as CouncilGuidanceMode
const guidance = buildAthenaRuntimeGuidance(resolvedIntent, resolvedMode)
return JSON.stringify(result, null, 2) + "\n\n" + guidance
},
})
+1
View File
@@ -4,6 +4,7 @@ export interface CouncilFinalizeArgs {
intent: string
question?: string
prompt_file?: string
mode?: string
}
export interface CouncilMemberResult {