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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
9887d0a93d
commit
1413c24886
@ -28,6 +28,8 @@ import { maybeCreateSisyphusConfig } from "./builtin-agents/sisyphus-agent"
|
|||||||
import { maybeCreateHephaestusConfig } from "./builtin-agents/hephaestus-agent"
|
import { maybeCreateHephaestusConfig } from "./builtin-agents/hephaestus-agent"
|
||||||
import { maybeCreateAtlasConfig } from "./builtin-agents/atlas-agent"
|
import { maybeCreateAtlasConfig } from "./builtin-agents/atlas-agent"
|
||||||
import { buildCustomAgentMetadata, parseRegisteredAgentSummaries } from "./custom-agent-summaries"
|
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
|
type AgentSource = AgentFactory | AgentConfig
|
||||||
|
|
||||||
@ -75,7 +77,8 @@ export async function createBuiltinAgents(
|
|||||||
uiSelectedModel?: string,
|
uiSelectedModel?: string,
|
||||||
disabledSkills?: Set<string>,
|
disabledSkills?: Set<string>,
|
||||||
useTaskSystem = false,
|
useTaskSystem = false,
|
||||||
disableOmoEnv = false
|
disableOmoEnv = false,
|
||||||
|
councilConfig?: CouncilConfig
|
||||||
): Promise<Record<string, AgentConfig>> {
|
): Promise<Record<string, AgentConfig>> {
|
||||||
|
|
||||||
const connectedProviders = readConnectedProvidersCache()
|
const connectedProviders = readConnectedProvidersCache()
|
||||||
@ -198,5 +201,21 @@ export async function createBuiltinAgents(
|
|||||||
result["atlas"] = atlasConfig
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/agents/builtin-agents/council-member-agents.ts
Normal file
91
src/agents/builtin-agents/council-member-agents.ts
Normal file
@ -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: <name>").
|
||||||
|
* Returns a record of agent keys to configs and the list of registered keys.
|
||||||
|
*/
|
||||||
|
export function registerCouncilMemberAgents(
|
||||||
|
councilConfig: CouncilConfig
|
||||||
|
): { agents: Record<string, AgentConfig>; registeredKeys: string[] } {
|
||||||
|
const agents: Record<string, AgentConfig> = {}
|
||||||
|
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 }
|
||||||
|
}
|
||||||
@ -78,6 +78,7 @@ export async function applyAgentConfig(params: {
|
|||||||
const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false;
|
const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false;
|
||||||
const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false;
|
const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false;
|
||||||
|
|
||||||
|
const athenaCouncilConfig = params.pluginConfig.agents?.athena?.council
|
||||||
const builtinAgents = await createBuiltinAgents(
|
const builtinAgents = await createBuiltinAgents(
|
||||||
migratedDisabledAgents,
|
migratedDisabledAgents,
|
||||||
params.pluginConfig.agents,
|
params.pluginConfig.agents,
|
||||||
@ -92,6 +93,7 @@ export async function applyAgentConfig(params: {
|
|||||||
disabledSkills,
|
disabledSkills,
|
||||||
useTaskSystem,
|
useTaskSystem,
|
||||||
disableOmoEnv,
|
disableOmoEnv,
|
||||||
|
athenaCouncilConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const includeClaudeAgents = params.pluginConfig.claude_code?.agents ?? true;
|
const includeClaudeAgents = params.pluginConfig.claude_code?.agents ?? true;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user