fix(core): limit subagent nesting depth (#37124)
Co-authored-by: Dax <mail@thdxr.com>
This commit is contained in:
co-authored by
Dax
parent
656f299017
commit
285d315b4e
@@ -81,6 +81,9 @@ export const Info = Schema.Struct({
|
|||||||
description:
|
description:
|
||||||
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
|
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
|
||||||
}),
|
}),
|
||||||
|
subagent_depth: Schema.optional(NonNegativeInt).annotate({
|
||||||
|
description: "Maximum subagent nesting depth. Defaults to 1, which prevents subagents from launching subagents.",
|
||||||
|
}),
|
||||||
username: Schema.optional(Schema.String).annotate({
|
username: Schema.optional(Schema.String).annotate({
|
||||||
description: "Custom username to display in conversations instead of system username",
|
description: "Custom username to display in conversations instead of system username",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -101,6 +101,21 @@ export const TaskTool = Tool.define(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parent = yield* sessions.get(ctx.sessionID)
|
||||||
|
let current = parent
|
||||||
|
let depth = 0
|
||||||
|
while (current.parentID) {
|
||||||
|
depth++
|
||||||
|
current = yield* sessions.get(current.parentID)
|
||||||
|
}
|
||||||
|
if (depth >= (cfg.subagent_depth ?? 1)) {
|
||||||
|
return yield* Effect.fail(
|
||||||
|
new Error(
|
||||||
|
`Subagent depth limit reached (${cfg.subagent_depth ?? 1}). Increase "subagent_depth" to allow nested subagents.`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!ctx.extra?.bypassAgentCheck) {
|
if (!ctx.extra?.bypassAgentCheck) {
|
||||||
yield* ctx.ask({
|
yield* ctx.ask({
|
||||||
permission: id,
|
permission: id,
|
||||||
@@ -121,7 +136,6 @@ export const TaskTool = Tool.define(
|
|||||||
const session = params.task_id
|
const session = params.task_id
|
||||||
? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
|
? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
|
||||||
: undefined
|
: undefined
|
||||||
const parent = yield* sessions.get(ctx.sessionID)
|
|
||||||
const childPermission = deriveSubagentSessionPermission({
|
const childPermission = deriveSubagentSessionPermission({
|
||||||
parentSessionPermission: parent.permission ?? [],
|
parentSessionPermission: parent.permission ?? [],
|
||||||
subagent: next,
|
subagent: next,
|
||||||
|
|||||||
@@ -388,6 +388,86 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance("prevents subagents from launching subagents by default", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const sessions = yield* Session.Service
|
||||||
|
const { chat, assistant } = yield* seed()
|
||||||
|
const child = yield* sessions.create({ parentID: chat.id, title: "child" })
|
||||||
|
const nestedAssistant = yield* sessions.updateMessage({
|
||||||
|
...assistant,
|
||||||
|
id: MessageID.ascending(),
|
||||||
|
parentID: MessageID.ascending(),
|
||||||
|
sessionID: child.id,
|
||||||
|
})
|
||||||
|
const tool = yield* TaskTool
|
||||||
|
const def = yield* tool.init()
|
||||||
|
let asked = false
|
||||||
|
|
||||||
|
const exit = yield* def
|
||||||
|
.execute(
|
||||||
|
{
|
||||||
|
description: "inspect bug",
|
||||||
|
prompt: "look into the cache key path",
|
||||||
|
subagent_type: "general",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sessionID: child.id,
|
||||||
|
messageID: nestedAssistant.id,
|
||||||
|
agent: "general",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
extra: { promptOps: stubOps() },
|
||||||
|
messages: [],
|
||||||
|
metadata: () => Effect.void,
|
||||||
|
ask: () => Effect.sync(() => (asked = true)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.pipe(Effect.exit)
|
||||||
|
|
||||||
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
|
expect(asked).toBe(false)
|
||||||
|
expect(yield* sessions.children(child.id)).toHaveLength(0)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"allows nested subagents up to the configured depth",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const sessions = yield* Session.Service
|
||||||
|
const { chat, assistant } = yield* seed()
|
||||||
|
const child = yield* sessions.create({ parentID: chat.id, title: "child" })
|
||||||
|
const nestedAssistant = yield* sessions.updateMessage({
|
||||||
|
...assistant,
|
||||||
|
id: MessageID.ascending(),
|
||||||
|
parentID: MessageID.ascending(),
|
||||||
|
sessionID: child.id,
|
||||||
|
})
|
||||||
|
const tool = yield* TaskTool
|
||||||
|
const def = yield* tool.init()
|
||||||
|
|
||||||
|
const result = yield* def.execute(
|
||||||
|
{
|
||||||
|
description: "inspect bug",
|
||||||
|
prompt: "look into the cache key path",
|
||||||
|
subagent_type: "general",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sessionID: child.id,
|
||||||
|
messageID: nestedAssistant.id,
|
||||||
|
agent: "general",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
extra: { promptOps: stubOps() },
|
||||||
|
messages: [],
|
||||||
|
metadata: () => Effect.void,
|
||||||
|
ask: () => Effect.void,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect((yield* sessions.get(result.metadata.sessionId)).parentID).toBe(child.id)
|
||||||
|
}),
|
||||||
|
{ config: { subagent_depth: 2 } },
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"execute shapes child permissions for task, todowrite, and primary tools",
|
"execute shapes child permissions for task, todowrite, and primary tools",
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -1928,6 +1928,7 @@ export type Config = {
|
|||||||
model?: string
|
model?: string
|
||||||
small_model?: string
|
small_model?: string
|
||||||
default_agent?: string
|
default_agent?: string
|
||||||
|
subagent_depth?: number
|
||||||
username?: string
|
username?: string
|
||||||
mode?: {
|
mode?: {
|
||||||
build?: AgentConfig
|
build?: AgentConfig
|
||||||
|
|||||||
Reference in New Issue
Block a user