import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin" import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants" import type { AllowedAgentType, CallOmoAgentArgs, ToolContextWithMetadata } from "./types" import type { BackgroundManager } from "../../features/background-agent" import type { CategoriesConfig, AgentOverrides } from "../../config/schema" import type { DelegatedModelConfig } from "../../shared/model-resolution-types" import type { FallbackEntry } from "../../shared/model-requirements" import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements" import { getAgentConfigKey } from "../../shared/agent-display-names" import { normalizeModelFormat } from "../../shared/model-format-normalizer" import { normalizeFallbackModels } from "../../shared/model-resolver" import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models" import { log } from "../../shared" import { executeBackground } from "./background-executor" import { executeSync } from "./sync-executor" function resolveModelAndFallbackChain(args: { subagentType: string agentOverrides?: AgentOverrides userCategories?: CategoriesConfig }): { model: DelegatedModelConfig | undefined; fallbackChain: FallbackEntry[] | undefined } { const { subagentType, agentOverrides, userCategories } = args const agentConfigKey = getAgentConfigKey(subagentType) const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentConfigKey] const agentOverride = agentOverrides?.[agentConfigKey as keyof AgentOverrides] ?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentConfigKey)?.[1] : undefined) const agentCategoryModel = agentOverride?.category ? userCategories?.[agentOverride.category]?.model : undefined const agentCategoryVariant = agentOverride?.category ? userCategories?.[agentOverride.category]?.variant : undefined let model: DelegatedModelConfig | undefined if (agentOverride?.model) { const normalized = normalizeModelFormat(agentOverride.model) if (normalized) { model = agentOverride.variant ? { ...normalized, variant: agentOverride.variant } : normalized log("[call_omo_agent] Resolved model override from agent config", { agent: subagentType, model: agentOverride.model, variant: agentOverride.variant, }) } } else if (agentCategoryModel) { const normalized = normalizeModelFormat(agentCategoryModel) if (normalized) { const variantToUse = agentOverride?.variant ?? agentCategoryVariant model = variantToUse ? { ...normalized, variant: variantToUse } : normalized log("[call_omo_agent] Resolved model override from agent category", { agent: subagentType, category: agentOverride?.category, model: agentCategoryModel, variant: variantToUse, }) } } const normalizedFallbackModels = normalizeFallbackModels( agentOverride?.fallback_models ?? (agentOverride?.category ? userCategories?.[agentOverride.category]?.fallback_models : undefined) ) const defaultProviderID = model?.providerID ?? agentRequirement?.fallbackChain?.[0]?.providers?.[0] ?? "opencode" const configuredFallbackChain = buildFallbackChainFromModels(normalizedFallbackModels, defaultProviderID) return { model, fallbackChain: configuredFallbackChain ?? agentRequirement?.fallbackChain, } } export function createCallOmoAgent( ctx: PluginInput, backgroundManager: BackgroundManager, disabledAgents: string[] = [], agentOverrides?: AgentOverrides, userCategories?: CategoriesConfig, ): ToolDefinition { const agentDescriptions = ALLOWED_AGENTS.map( (name) => `- ${name}: Specialized agent for ${name} tasks` ).join("\n") const description = CALL_OMO_AGENT_DESCRIPTION.replace("{agents}", agentDescriptions) return tool({ description, args: { description: tool.schema.string().describe("A short (3-5 words) description of the task"), prompt: tool.schema.string().describe("The task for the agent to perform"), subagent_type: tool.schema .string() .describe("The type of specialized agent to use for this task (explore or librarian only)"), run_in_background: tool.schema .boolean() .describe("REQUIRED. true: run asynchronously (use background_output to get results), false: run synchronously and wait for completion"), session_id: tool.schema.string().describe("Existing Task session to continue").optional(), }, async execute(args: CallOmoAgentArgs, toolContext) { const toolCtx = toolContext as ToolContextWithMetadata log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`) // Case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc. if ( !ALLOWED_AGENTS.some( (name) => name.toLowerCase() === args.subagent_type.toLowerCase(), ) ) { return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.` } const normalizedAgent = args.subagent_type.toLowerCase() as AllowedAgentType args = { ...args, subagent_type: normalizedAgent } // Check if agent is disabled if (disabledAgents.some((disabled) => disabled.toLowerCase() === normalizedAgent)) { return `Error: Agent "${normalizedAgent}" is disabled via disabled_agents configuration. Remove it from disabled_agents in your oh-my-opencode.json to use it.` } const { model: resolvedModel, fallbackChain } = resolveModelAndFallbackChain({ subagentType: args.subagent_type, agentOverrides, userCategories, }) if (args.run_in_background) { if (args.session_id) { return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.` } return await executeBackground(args, toolCtx, backgroundManager, ctx.client, fallbackChain, resolvedModel) } if (!args.session_id) { let spawnReservation: Awaited> | undefined try { spawnReservation = await backgroundManager.reserveSubagentSpawn(toolCtx.sessionID) return await executeSync(args, toolCtx, ctx, undefined, fallbackChain, spawnReservation, resolvedModel) } catch (error) { spawnReservation?.rollback() return `Error: ${error instanceof Error ? error.message : String(error)}` } } return await executeSync(args, toolCtx, ctx, undefined, fallbackChain, undefined, resolvedModel) }, }) }