From 1413c248864b42ec4a23c1e53aea1cec1d522744 Mon Sep 17 00:00:00 2001 From: ismeth Date: Thu, 19 Feb 2026 02:20:51 +0100 Subject: [PATCH] feat(athena): register council members as task-callable subagents Each council member from config is now registered as a named agent (e.g. 'Council: Claude Opus 4.6') via registerCouncilMemberAgents(). Adds humanizeModelId() to derive friendly display names from model IDs. Athena's prompt gets the member list appended so it can call task(subagent_type=...) for each. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/agents/builtin-agents.ts | 21 ++++- .../builtin-agents/council-member-agents.ts | 91 +++++++++++++++++++ src/plugin-handlers/agent-config-handler.ts | 2 + 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/agents/builtin-agents/council-member-agents.ts diff --git a/src/agents/builtin-agents.ts b/src/agents/builtin-agents.ts index 0193d482..95f360b3 100644 --- a/src/agents/builtin-agents.ts +++ b/src/agents/builtin-agents.ts @@ -28,6 +28,8 @@ import { maybeCreateSisyphusConfig } from "./builtin-agents/sisyphus-agent" import { maybeCreateHephaestusConfig } from "./builtin-agents/hephaestus-agent" import { maybeCreateAtlasConfig } from "./builtin-agents/atlas-agent" import { buildCustomAgentMetadata, parseRegisteredAgentSummaries } from "./custom-agent-summaries" +import { registerCouncilMemberAgents } from "./builtin-agents/council-member-agents" +import type { CouncilConfig } from "./athena/types" type AgentSource = AgentFactory | AgentConfig @@ -75,7 +77,8 @@ export async function createBuiltinAgents( uiSelectedModel?: string, disabledSkills?: Set, useTaskSystem = false, - disableOmoEnv = false + disableOmoEnv = false, + councilConfig?: CouncilConfig ): Promise> { const connectedProviders = readConnectedProvidersCache() @@ -198,5 +201,21 @@ export async function createBuiltinAgents( result["atlas"] = atlasConfig } + if (councilConfig && councilConfig.members.length >= 2) { + const { agents: councilAgents, registeredKeys } = registerCouncilMemberAgents(councilConfig) + for (const [key, config] of Object.entries(councilAgents)) { + result[key] = config + } + + if (result["athena"] && registeredKeys.length > 0) { + const memberList = registeredKeys.map((key) => `- "${key}"`).join("\n") + const councilTaskInstructions = `\n\n## Registered Council Members (use these as subagent_type in task calls)\n\n${memberList}` + result["athena"] = { + ...result["athena"], + prompt: (result["athena"].prompt ?? "") + councilTaskInstructions, + } + } + } + return result } diff --git a/src/agents/builtin-agents/council-member-agents.ts b/src/agents/builtin-agents/council-member-agents.ts new file mode 100644 index 00000000..ff4c3b39 --- /dev/null +++ b/src/agents/builtin-agents/council-member-agents.ts @@ -0,0 +1,91 @@ +import type { AgentConfig } from "@opencode-ai/sdk" +import type { CouncilConfig, CouncilMemberConfig } from "../athena/types" +import { createCouncilMemberAgent } from "../athena/council-member-agent" +import { parseModelString } from "../athena/model-parser" +import { log } from "../../shared/logger" + +/** Prefix used for all dynamically-registered council member agent keys. */ +export const COUNCIL_MEMBER_KEY_PREFIX = "Council: " + +const UPPERCASE_TOKENS = new Set(["gpt", "llm", "ai", "api"]) + +/** + * Derives a human-friendly display name from a model string. + * "anthropic/claude-opus-4-6" → "Claude Opus 4.6" + * "openai/gpt-5.3-codex" → "GPT 5.3 Codex" + */ +function humanizeModelId(model: string): string { + const modelId = model.includes("/") ? model.split("/").pop() ?? model : model + const parts = modelId.split("-") + const result: string[] = [] + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + if (/^\d+$/.test(part)) { + const versionParts = [part] + while (i + 1 < parts.length && /^\d+$/.test(parts[i + 1])) { + i++ + versionParts.push(parts[i]) + } + result.push(versionParts.join(".")) + } else if (UPPERCASE_TOKENS.has(part.toLowerCase())) { + result.push(part.toUpperCase()) + } else { + result.push(part.charAt(0).toUpperCase() + part.slice(1)) + } + } + + return result.join(" ") +} + +/** + * Generates a stable agent registration key from a council member config. + * Uses the member's name if present, otherwise derives a friendly name from the model ID. + */ +export function getCouncilMemberAgentKey(member: CouncilMemberConfig): string { + const displayName = member.name ?? humanizeModelId(member.model) + return `${COUNCIL_MEMBER_KEY_PREFIX}${displayName}` +} + +/** + * Registers council members as individual subagent entries. + * Each member becomes a separate agent callable via task(subagent_type="Council: "). + * Returns a record of agent keys to configs and the list of registered keys. + */ +export function registerCouncilMemberAgents( + councilConfig: CouncilConfig +): { agents: Record; registeredKeys: string[] } { + const agents: Record = {} + const registeredKeys: string[] = [] + + for (const member of councilConfig.members) { + const parsed = parseModelString(member.model) + if (!parsed) { + log("[council-member-agents] Skipping member with invalid model", { model: member.model }) + continue + } + + const key = getCouncilMemberAgentKey(member) + const config = createCouncilMemberAgent(member.model) + + const friendlyName = member.name ?? humanizeModelId(member.model) + const description = `Council member: ${friendlyName} (${member.model}). Independent read-only code analyst for Athena council. (OhMyOpenCode)` + + agents[key] = { + ...config, + description, + model: member.model, + ...(member.variant ? { variant: member.variant } : {}), + } + + registeredKeys.push(key) + + log("[council-member-agents] Registered council member agent", { + key, + model: member.model, + variant: member.variant, + }) + } + + return { agents, registeredKeys } +} diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index c5d59e14..86c9a68c 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -78,6 +78,7 @@ export async function applyAgentConfig(params: { const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false; const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false; + const athenaCouncilConfig = params.pluginConfig.agents?.athena?.council const builtinAgents = await createBuiltinAgents( migratedDisabledAgents, params.pluginConfig.agents, @@ -92,6 +93,7 @@ export async function applyAgentConfig(params: { disabledSkills, useTaskSystem, disableOmoEnv, + athenaCouncilConfig, ); const includeClaudeAgents = params.pluginConfig.claude_code?.agents ?? true;