fix(core): ignore blank subagent options (#50977)

Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-09-23 16:29:08 -05:00
committed by GitHub
parent f53cc41657
commit 757e565c23
3 changed files with 47 additions and 6 deletions
+12 -1
View File
@@ -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]
+2 -1
View File
@@ -60,6 +60,7 @@ export const registerToolPlugin = <R>(
readonly effect: (context: Context) => Effect.Effect<void, never, R>
},
overrides: Parameters<typeof host>[0] = {},
hook?: Context["tool"]["hook"],
): Effect.Effect<void, never, R | Tool.Service | Scope.Scope> =>
Effect.gen(function* () {
const tools = yield* Tool.Service
@@ -73,7 +74,7 @@ export const registerToolPlugin = <R>(
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)
+33 -4
View File
@@ -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 })