fix(core): limit v2 subagent nesting depth (#37291)

This commit is contained in:
opencode-agent[bot] 2026-07-16 09:28:15 -04:00 committed by GitHub
commit d01dfa57b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 116 additions and 0 deletions

View file

@ -283,6 +283,12 @@ describe("Config", () => {
}),
)
it.effect("migrates the v1 experimental subagent depth", () =>
Effect.sync(() => {
expect(ConfigMigrateV1.migrate({ experimental: { subagent_depth: 2 } }).experimental?.subagent_depth).toBe(2)
}),
)
it.effect("migrates v1 provider setup options into AISDK settings", () =>
Effect.sync(() => {
const migrated = ConfigMigrateV1.migrate({

View file

@ -1,5 +1,6 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Fiber, Layer, Schema, Stream } from "effect"
import path from "path"
import { Money } from "@opencode-ai/schema/money"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@ -164,6 +165,77 @@ describe("SubagentTool", () => {
),
)
it.live("prevents subagents from launching subagents by default", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
const sessions = yield* SessionV2.Service
const root = yield* sessions.create({ location })
const parent = yield* sessions.create({ parentID: root.id, title: "parent" })
yield* withSubagent(parent.location)
const locations = yield* LocationServiceMap.Service
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
yield* waitForTool(registry, SubagentTool.name)
expect(
yield* executeTool(registry, {
sessionID: parent.id,
...toolIdentity,
call: {
type: "tool-call",
id: "call-nested-subagent",
name: SubagentTool.name,
input: { agent: "reviewer", description: "nested", prompt: "should fail" },
},
}),
).toEqual({ type: "error", value: expect.stringContaining("Subagent depth limit reached (1)") })
expect((yield* sessions.list({ parentID: parent.id })).data).toHaveLength(0)
}),
),
),
)
it.live("allows nested subagents up to the configured depth", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
yield* Effect.promise(() =>
Bun.write(path.join(dir.path, "opencode.json"), JSON.stringify({ experimental: { subagent_depth: 2 } })),
)
const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
const sessions = yield* SessionV2.Service
const root = yield* sessions.create({ location })
const parent = yield* sessions.create({ parentID: root.id, title: "parent", model: parentModel })
yield* withSubagent(parent.location)
const locations = yield* LocationServiceMap.Service
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
yield* waitForTool(registry, SubagentTool.name)
const settled = yield* settleTool(registry, {
sessionID: parent.id,
...toolIdentity,
call: {
type: "tool-call",
id: "call-configured-nested-subagent",
name: SubagentTool.name,
input: { agent: "reviewer", description: "nested", prompt: "should run" },
},
})
expect(settled.output?.structured).toMatchObject({ status: "completed", output: childText })
expect((yield* sessions.get(outputSessionID(settled.output?.structured))).parentID).toBe(parent.id)
}),
),
),
)
it.live("runs a foreground child session and returns the final assistant text", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),