From 73ec0d38d81fad1ff76ddbff5f6074640c7ec67a Mon Sep 17 00:00:00 2001 From: Stranmor Date: Tue, 17 Feb 2026 02:34:00 +0300 Subject: [PATCH 1/3] feat: allow custom subagents to delegate tasks via task() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded `task: false` in all subagent spawning paths with `buildSubagentTools()` — a centralized function that respects AGENT_RESTRICTIONS. Built-in agents (explore, librarian, oracle, metis, momus, sisyphus-junior) keep `task: false` via their AGENT_RESTRICTIONS entries. Custom/user-defined agents (not in AGENT_RESTRICTIONS) now get `task: true`, enabling multi-level agent delegation (e.g. a critic agent spawning a breaker agent). This unblocks workflows where specialized review agents need to orchestrate sub-pipelines without routing through the top-level orchestrator. --- src/features/background-agent/manager.ts | 16 +++------------- src/features/background-agent/spawner.ts | 16 +++------------- .../background-agent/spawner/task-resumer.ts | 9 ++------- .../background-agent/spawner/task-starter.ts | 9 ++------- src/features/background-agent/task-resumer.ts | 9 ++------- src/features/background-agent/task-starter.ts | 9 ++------- src/shared/agent-tool-restrictions.ts | 18 ++++++++++++++++++ .../subagent-session-prompter.ts | 8 ++------ src/tools/call-omo-agent/sync-executor.ts | 8 ++------ src/tools/delegate-task/sync-continuation.ts | 11 ++--------- src/tools/delegate-task/sync-prompt-sender.ts | 11 ++--------- 11 files changed, 40 insertions(+), 84 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 137c6843cc..4b6a7e911b 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -6,7 +6,7 @@ import type { ResumeInput, } from "./types" import { TaskHistory } from "./task-history" -import { log, getAgentToolRestrictions, normalizeSDKResponse, promptWithModelSuggestionRetry } from "../../shared" +import { log, buildSubagentTools, normalizeSDKResponse, promptWithModelSuggestionRetry } from "../../shared" import { setSessionTools } from "../../shared/session-tools-store" import { ConcurrencyManager } from "./concurrency" import type { BackgroundTaskConfig, TmuxConfig } from "../../config/schema" @@ -335,12 +335,7 @@ export class BackgroundManager { ...(launchVariant ? { variant: launchVariant } : {}), system: input.skillContent, tools: (() => { - const tools = { - ...getAgentToolRestrictions(input.agent), - task: false, - call_omo_agent: true, - question: false, - } + const tools = buildSubagentTools(input.agent) setSessionTools(sessionID, tools) return tools })(), @@ -608,12 +603,7 @@ export class BackgroundManager { ...(resumeModel ? { model: resumeModel } : {}), ...(resumeVariant ? { variant: resumeVariant } : {}), tools: (() => { - const tools = { - ...getAgentToolRestrictions(existingTask.agent), - task: false, - call_omo_agent: true, - question: false, - } + const tools = buildSubagentTools(existingTask.agent) setSessionTools(existingTask.sessionID!, tools) return tools })(), diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index e9256eca42..111b497f93 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -1,7 +1,7 @@ import type { BackgroundTask, LaunchInput, ResumeInput } from "./types" import type { OpencodeClient, OnSubagentSessionCreated, QueueItem } from "./constants" import { TMUX_CALLBACK_DELAY_MS } from "./constants" -import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../shared" +import { log, buildSubagentTools, promptWithModelSuggestionRetry } from "../../shared" import { subagentSessions } from "../claude-code-session-state" import { getTaskToastManager } from "../task-toast-manager" import { isInsideTmux } from "../../shared/tmux" @@ -140,12 +140,7 @@ export async function startTask( ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), system: input.skillContent, - tools: { - ...getAgentToolRestrictions(input.agent), - task: false, - call_omo_agent: true, - question: false, - }, + tools: buildSubagentTools(input.agent), parts: [{ type: "text", text: input.prompt }], }, }).catch((error) => { @@ -224,12 +219,7 @@ export async function resumeTask( agent: task.agent, ...(resumeModel ? { model: resumeModel } : {}), ...(resumeVariant ? { variant: resumeVariant } : {}), - tools: { - ...getAgentToolRestrictions(task.agent), - task: false, - call_omo_agent: true, - question: false, - }, + tools: buildSubagentTools(task.agent), parts: [{ type: "text", text: input.prompt }], }, }).catch((error) => { diff --git a/src/features/background-agent/spawner/task-resumer.ts b/src/features/background-agent/spawner/task-resumer.ts index 7c7d5d2ace..c43742b5ec 100644 --- a/src/features/background-agent/spawner/task-resumer.ts +++ b/src/features/background-agent/spawner/task-resumer.ts @@ -1,5 +1,5 @@ import type { BackgroundTask, ResumeInput } from "../types" -import { log, getAgentToolRestrictions } from "../../../shared" +import { log, buildSubagentTools } from "../../../shared" import { setSessionTools } from "../../../shared/session-tools-store" import type { SpawnerContext } from "./spawner-context" import { subagentSessions } from "../../claude-code-session-state" @@ -80,12 +80,7 @@ export async function resumeTask( ...(resumeModel ? { model: resumeModel } : {}), ...(resumeVariant ? { variant: resumeVariant } : {}), tools: (() => { - const tools = { - ...getAgentToolRestrictions(task.agent), - task: false, - call_omo_agent: true, - question: false, - } + const tools = buildSubagentTools(task.agent) setSessionTools(task.sessionID!, tools) return tools })(), diff --git a/src/features/background-agent/spawner/task-starter.ts b/src/features/background-agent/spawner/task-starter.ts index a904a20b8f..7451cb9073 100644 --- a/src/features/background-agent/spawner/task-starter.ts +++ b/src/features/background-agent/spawner/task-starter.ts @@ -1,5 +1,5 @@ import type { QueueItem } from "../constants" -import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../../shared" +import { log, buildSubagentTools, promptWithModelSuggestionRetry } from "../../../shared" import { setSessionTools } from "../../../shared/session-tools-store" import { subagentSessions } from "../../claude-code-session-state" import { getTaskToastManager } from "../../task-toast-manager" @@ -81,12 +81,7 @@ export async function startTask(item: QueueItem, ctx: SpawnerContext): Promise { - const tools = { - ...getAgentToolRestrictions(input.agent), - task: false, - call_omo_agent: true, - question: false, - } + const tools = buildSubagentTools(input.agent) setSessionTools(sessionID, tools) return tools })(), diff --git a/src/features/background-agent/task-resumer.ts b/src/features/background-agent/task-resumer.ts index be88eb4a7f..73cc06f864 100644 --- a/src/features/background-agent/task-resumer.ts +++ b/src/features/background-agent/task-resumer.ts @@ -1,4 +1,4 @@ -import { log, getAgentToolRestrictions } from "../../shared" +import { log, buildSubagentTools } from "../../shared" import { subagentSessions } from "../claude-code-session-state" import { getTaskToastManager } from "../task-toast-manager" @@ -111,12 +111,7 @@ export async function resumeBackgroundTask(args: { agent: existingTask.agent, ...(resumeModel ? { model: resumeModel } : {}), ...(resumeVariant ? { variant: resumeVariant } : {}), - tools: { - ...getAgentToolRestrictions(existingTask.agent), - task: false, - call_omo_agent: true, - question: false, - }, + tools: buildSubagentTools(existingTask.agent), parts: [{ type: "text", text: input.prompt }], }, }).catch((error) => { diff --git a/src/features/background-agent/task-starter.ts b/src/features/background-agent/task-starter.ts index 9af87bdd00..ac1dec17c8 100644 --- a/src/features/background-agent/task-starter.ts +++ b/src/features/background-agent/task-starter.ts @@ -1,4 +1,4 @@ -import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../shared" +import { log, buildSubagentTools, promptWithModelSuggestionRetry } from "../../shared" import { isInsideTmux } from "../../shared/tmux" import { subagentSessions } from "../claude-code-session-state" @@ -150,12 +150,7 @@ export async function startQueuedTask(args: { ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), system: input.skillContent, - tools: { - ...getAgentToolRestrictions(input.agent), - task: false, - call_omo_agent: true, - question: false, - }, + tools: buildSubagentTools(input.agent), parts: [{ type: "text", text: input.prompt }], }, }).catch((error) => { diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index 865251d6fc..2947fcf8a9 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -55,3 +55,21 @@ export function hasAgentToolRestrictions(agentName: string): boolean { ?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1] return restrictions !== undefined && Object.keys(restrictions).length > 0 } + +/** + * Build the tools restriction object for a subagent. + * + * - Gets base restrictions from AGENT_RESTRICTIONS via getAgentToolRestrictions(). + * - If restrictions already define `task`, uses that value (built-in agents stay restricted). + * - If restrictions do NOT define `task` (custom/unknown agents), defaults to `task: true` (allow). + * - Always sets `call_omo_agent: true` and `question: false`. + */ +export function buildSubagentTools(agentName: string): Record { + const restrictions = getAgentToolRestrictions(agentName) + return { + ...restrictions, + ...("task" in restrictions ? {} : { task: true }), + call_omo_agent: true, + question: false, + } +} diff --git a/src/tools/call-omo-agent/subagent-session-prompter.ts b/src/tools/call-omo-agent/subagent-session-prompter.ts index 286bfc999e..8c82e08223 100644 --- a/src/tools/call-omo-agent/subagent-session-prompter.ts +++ b/src/tools/call-omo-agent/subagent-session-prompter.ts @@ -1,5 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { log, getAgentToolRestrictions } from "../../shared" +import { log, buildSubagentTools } from "../../shared" export async function promptSubagentSession( ctx: PluginInput, @@ -10,11 +10,7 @@ export async function promptSubagentSession( path: { id: options.sessionID }, body: { agent: options.agent, - tools: { - ...getAgentToolRestrictions(options.agent), - task: false, - question: false, - }, + tools: buildSubagentTools(options.agent), parts: [{ type: "text", text: options.prompt }], }, }) diff --git a/src/tools/call-omo-agent/sync-executor.ts b/src/tools/call-omo-agent/sync-executor.ts index d1310f9208..1665d01ab9 100644 --- a/src/tools/call-omo-agent/sync-executor.ts +++ b/src/tools/call-omo-agent/sync-executor.ts @@ -1,7 +1,7 @@ import type { CallOmoAgentArgs } from "./types" import type { PluginInput } from "@opencode-ai/plugin" import { log } from "../../shared" -import { getAgentToolRestrictions } from "../../shared" +import { buildSubagentTools } from "../../shared" import { createOrGetSession } from "./session-creator" import { waitForCompletion } from "./completion-poller" import { processMessages } from "./message-processor" @@ -45,11 +45,7 @@ export async function executeSync( path: { id: sessionID }, body: { agent: args.subagent_type, - tools: { - ...getAgentToolRestrictions(args.subagent_type), - task: false, - question: false, - }, + tools: buildSubagentTools(args.subagent_type), parts: [{ type: "text", text: args.prompt }], }, }) diff --git a/src/tools/delegate-task/sync-continuation.ts b/src/tools/delegate-task/sync-continuation.ts index b31e19508a..73cab0e3dd 100644 --- a/src/tools/delegate-task/sync-continuation.ts +++ b/src/tools/delegate-task/sync-continuation.ts @@ -1,9 +1,8 @@ import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" import type { ExecutorContext, SessionMessage } from "./executor-types" -import { isPlanFamily } from "./constants" import { storeToolMetadata } from "../../features/tool-metadata-store" import { getTaskToastManager } from "../../features/task-toast-manager" -import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions" +import { buildSubagentTools } from "../../shared/agent-tool-restrictions" import { getMessageDir } from "../../shared" import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry" import { findNearestMessageWithFields } from "../../features/hook-message-injector" @@ -78,13 +77,7 @@ export async function executeSyncContinuation( resumeVariant = resumeMessage?.model?.variant } - const allowTask = isPlanFamily(resumeAgent) - const tools = { - ...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}), - task: allowTask, - call_omo_agent: true, - question: false, - } + const tools = buildSubagentTools(resumeAgent ?? "") setSessionTools(args.session_id!, tools) await promptWithModelSuggestionRetry(client, { diff --git a/src/tools/delegate-task/sync-prompt-sender.ts b/src/tools/delegate-task/sync-prompt-sender.ts index e7aa20dc0b..3cc09dccf2 100644 --- a/src/tools/delegate-task/sync-prompt-sender.ts +++ b/src/tools/delegate-task/sync-prompt-sender.ts @@ -1,11 +1,10 @@ import type { DelegateTaskArgs, OpencodeClient } from "./types" -import { isPlanFamily } from "./constants" import { promptSyncWithModelSuggestionRetry, promptWithModelSuggestionRetry, } from "../../shared/model-suggestion-retry" import { formatDetailedError } from "./error-formatting" -import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions" +import { buildSubagentTools } from "../../shared/agent-tool-restrictions" import { setSessionTools } from "../../shared/session-tools-store" type SendSyncPromptDeps = { @@ -41,13 +40,7 @@ export async function sendSyncPrompt( }, deps: SendSyncPromptDeps = sendSyncPromptDeps ): Promise { - const allowTask = isPlanFamily(input.agentToUse) - const tools = { - task: allowTask, - call_omo_agent: true, - question: false, - ...getAgentToolRestrictions(input.agentToUse), - } + const tools = buildSubagentTools(input.agentToUse) setSessionTools(input.sessionID, tools) const promptArgs = { From d15a05c2192976f6007d1ccc5c52195b86c6dcf0 Mon Sep 17 00:00:00 2001 From: Stranmor Date: Tue, 17 Feb 2026 03:37:32 +0300 Subject: [PATCH 2/3] feat: register custom agents from config and grant them task permission Custom agents defined in oh-my-opencode.json (e.g. breaker, type-sentinel) were silently dropped because they were passed as overrides to createBuiltinAgents() which only processes known builtin names. Additionally, the global config.permission.task = 'deny' blocked custom agents from calling task() since they were never whitelisted. Changes: - applyAgentConfig: add custom agents from pluginConfig.agents that aren't already registered (both sisyphus-enabled and disabled paths) - applyToolConfig: grant task='allow' to all agents not explicitly blocked by AGENT_RESTRICTIONS --- src/plugin-handlers/agent-config-handler.ts | 21 +++++++++++++++++++++ src/plugin-handlers/tool-config-handler.ts | 10 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index 91cbf2a963..82af3e0b5f 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -28,6 +28,23 @@ function hasConfiguredDefaultAgent(config: Record): boolean { return typeof defaultAgent === "string" && defaultAgent.trim().length > 0; } +function addCustomAgentsFromConfig(params: { + config: Record; + pluginConfig: OhMyOpenCodeConfig; +}): void { + const agents = params.pluginConfig.agents; + if (!agents) return; + + const configAgent = params.config.agent as Record | undefined; + if (!configAgent) return; + + for (const [name, agentCfg] of Object.entries(agents)) { + if (agentCfg && !configAgent[name]) { + configAgent[name] = migrateAgentConfig(agentCfg as Record); + } + } +} + export async function applyAgentConfig(params: { config: Record; pluginConfig: OhMyOpenCodeConfig; @@ -192,6 +209,8 @@ export async function applyAgentConfig(params: { build: { ...migratedBuild, mode: "subagent", hidden: true }, ...(planDemoteConfig ? { plan: planDemoteConfig } : {}), }; + + addCustomAgentsFromConfig(params); } else { params.config.agent = { ...builtinAgents, @@ -200,6 +219,8 @@ export async function applyAgentConfig(params: { ...pluginAgents, ...configAgent, }; + + addCustomAgentsFromConfig(params); } if (params.config.agent) { diff --git a/src/plugin-handlers/tool-config-handler.ts b/src/plugin-handlers/tool-config-handler.ts index e488d2da97..6692c1e7bf 100644 --- a/src/plugin-handlers/tool-config-handler.ts +++ b/src/plugin-handlers/tool-config-handler.ts @@ -1,5 +1,6 @@ import type { OhMyOpenCodeConfig } from "../config"; import { getAgentDisplayName } from "../shared/agent-display-names"; +import { getAgentToolRestrictions } from "../shared/agent-tool-restrictions"; type AgentWithPermission = { permission?: Record }; @@ -98,6 +99,15 @@ export function applyToolConfig(params: { }; } + for (const customAgent of Object.keys(params.agentResult)) { + const agent = params.agentResult[customAgent] as AgentWithPermission | undefined; + if (!agent) continue; + const restrictions = getAgentToolRestrictions(customAgent); + if (!("task" in restrictions) && !agent.permission?.task) { + agent.permission = { ...agent.permission, task: "allow" }; + } + } + params.config.permission = { ...(params.config.permission as Record), webfetch: "allow", From c78aab8598dd9f742c6903fddba0dafdb661a004 Mon Sep 17 00:00:00 2001 From: Stranmor Date: Tue, 17 Feb 2026 03:51:44 +0300 Subject: [PATCH 3/3] fix: allow custom agent names in AgentOverridesSchema via catchall AgentOverridesSchema used strict z.object() with only hardcoded builtin agent names. Zod strips unknown keys during safeParse, silently dropping any custom agents (breaker, critic, type-sentinel, etc.) from the parsed config. Adding .catchall(AgentOverrideConfigSchema) preserves unknown agent keys while still validating them against the same schema. --- src/config/schema/agent-overrides.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/schema/agent-overrides.ts b/src/config/schema/agent-overrides.ts index 876560ec66..32604c6d28 100644 --- a/src/config/schema/agent-overrides.ts +++ b/src/config/schema/agent-overrides.ts @@ -55,7 +55,7 @@ export const AgentOverridesSchema = z.object({ explore: AgentOverrideConfigSchema.optional(), "multimodal-looker": AgentOverrideConfigSchema.optional(), atlas: AgentOverrideConfigSchema.optional(), -}) +}).catchall(AgentOverrideConfigSchema) export type AgentOverrideConfig = z.infer export type AgentOverrides = z.infer