From 757e565c23f83b3ce3eab64aa6e35cf6ffbde880 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:29:08 -0500 Subject: [PATCH] fix(core): ignore blank subagent options (#50977) Co-authored-by: rekram1-node --- packages/core/src/tool/plugin/subagent.ts | 13 +++++++- packages/core/test/lib/tool.ts | 3 +- packages/core/test/tool-subagent.test.ts | 37 ++++++++++++++++++++--- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/core/src/tool/plugin/subagent.ts b/packages/core/src/tool/plugin/subagent.ts index fe3d07dd7e1..91512015ec8 100644 --- a/packages/core/src/tool/plugin/subagent.ts +++ b/packages/core/src/tool/plugin/subagent.ts @@ -3,7 +3,7 @@ export * as SubagentTool from "./subagent.js" import { ToolFailure } from "@opencode/ai" import type { Context } from "@opencode/plugin/effect/plugin" import type { SessionHooks } from "@opencode/plugin/effect/session" -import { Effect, Schema } from "effect" +import { Effect, Predicate, Schema } from "effect" import { Agent } from "../../agent.js" import { Config } from "../../config.js" import { Job } from "../../job.js" @@ -268,6 +268,17 @@ export const Plugin = { ) .pipe(Effect.orDie) + yield* ctx.tool.hook("execute.before", (event) => + Effect.sync(() => { + if (event.tool !== name || !Predicate.isObject(event.input)) return + if (event.input.model !== "" && event.input.sessionID !== "") return + const input = { ...event.input } + if (input.model === "") delete input.model + if (input.sessionID === "") delete input.sessionID + event.input = input + }), + ) + const hook = (event: SessionHooks["context"]) => Effect.gen(function* () { const tool = event.tools[name] diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index 27e8a78aa09..23f38424a3c 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -60,6 +60,7 @@ export const registerToolPlugin = ( readonly effect: (context: Context) => Effect.Effect }, overrides: Parameters[0] = {}, + hook?: Context["tool"]["hook"], ): Effect.Effect => Effect.gen(function* () { const tools = yield* Tool.Service @@ -73,7 +74,7 @@ export const registerToolPlugin = ( transform: tools.transform, reload: tools.reload, list: tools.list, - hook: () => Effect.die("registerToolPlugin does not support tool hooks"), + hook: hook ?? (() => Effect.die("registerToolPlugin does not support tool hooks")), }, }) yield* plugin.effect(context) diff --git a/packages/core/test/tool-subagent.test.ts b/packages/core/test/tool-subagent.test.ts index 0e36b151c91..3d2c5f514e9 100644 --- a/packages/core/test/tool-subagent.test.ts +++ b/packages/core/test/tool-subagent.test.ts @@ -30,6 +30,7 @@ import { SessionMessage } from "@opencode/core/session/message" import { SessionRunnerModel } from "@opencode/core/session/runner/model" import { SessionStore } from "@opencode/core/session/store" import { Plugin } from "@opencode/core/plugin" +import { PluginHooks } from "@opencode/core/plugin/hooks" import { PluginSupervisor } from "@opencode/core/plugin/supervisor" import { Permission } from "@opencode/core/permission" import { SubagentTool } from "@opencode/core/tool/plugin/subagent" @@ -112,8 +113,13 @@ const executionNode = makeGlobalNode({ const subagentPluginSupervisor = makeLocationNode({ name: "test/subagent-plugins", - layer: Layer.effectDiscard(registerToolPlugin(SubagentTool.Plugin)), - deps: [Agent.node, Config.node, Model.node, Permission.node, Session.node, Job.node, Tool.node], + layer: Layer.effectDiscard( + Effect.gen(function* () { + const hooks = yield* PluginHooks.Service + yield* registerToolPlugin(SubagentTool.Plugin, {}, (name, callback) => hooks.register("tool", name, callback)) + }), + ), + deps: [Agent.node, Config.node, Model.node, Permission.node, Session.node, Job.node, Tool.node, PluginHooks.node], }) const nodes = LayerNode.group([ @@ -407,7 +413,7 @@ describe("SubagentTool", () => { type: "tool-call", id: "call-subagent", name: SubagentTool.name, - input: { agent: "reviewer", description: "review", prompt: "review this" }, + input: { agent: "reviewer", description: "review", prompt: "review this", model: "", sessionID: "" }, }, }) @@ -484,6 +490,7 @@ describe("SubagentTool", () => { description: "follow up", prompt: "continue this", sessionID: childID, + model: "", }, }, }) @@ -600,6 +607,28 @@ describe("SubagentTool", () => { message: `Subagent session not found: ${missing}`, }, }) + expect( + yield* executeTool(registry, { + sessionID: parent.id, + ...toolIdentity, + call: { + type: "tool-call", + id: "call-malformed-child-id", + name: SubagentTool.name, + input: { agent: "reviewer", description: "follow up", prompt: "continue", sessionID: "placeholder" }, + }, + }), + ).toMatchObject({ + status: "error", + error: { message: expect.stringContaining('sessionID: Expected a string starting with "ses"') }, + }) + expect(yield* call(parent.id, "call-parent-id")).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: `Session ${parent.id} is not a child of the current session`, + }, + }) expect(yield* call(unrelated.id, "call-unrelated-child")).toEqual({ status: "error", error: { @@ -655,7 +684,7 @@ describe("SubagentTool", () => { }) // The requested model beats the agent's configured model. - const spawned = yield* call("call-override", { model: "test/override#fast" }) + const spawned = yield* call("call-override", { model: "test/override#fast", sessionID: "" }) expect(spawned).toMatchObject({ status: "completed", metadata: { status: "completed" } }) const child = yield* sessions.get(outputSessionID(spawned.metadata)) expect(child).toMatchObject({ agent: "reviewer", model: overrideModel })