fix(core): limit v2 subagent nesting depth
This commit is contained in:
parent
7a6ce05d09
commit
345bdf6665
4 changed files with 109 additions and 0 deletions
|
|
@ -1044,6 +1044,12 @@ export namespace Config {
|
|||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Tools that should only be available to primary agents."),
|
||||
subagent_depth: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe("Maximum subagent nesting depth. Defaults to 1."),
|
||||
continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
|
||||
mcp_timeout: z
|
||||
.number()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,22 @@ export const TaskTool = Tool.define(
|
|||
const run = Effect.fn("TaskTool.execute")(function* (params: z.infer<typeof parameters>, ctx: Tool.Context) {
|
||||
const cfg = yield* config.get()
|
||||
|
||||
const parent = yield* sessions.get(ctx.sessionID)
|
||||
let current = parent
|
||||
let depth = 0
|
||||
while (current.parentID) {
|
||||
depth++
|
||||
current = yield* sessions.get(current.parentID)
|
||||
}
|
||||
const limit = cfg.experimental?.subagent_depth ?? 1
|
||||
if (depth >= limit) {
|
||||
return yield* Effect.fail(
|
||||
new Error(
|
||||
`Subagent depth limit reached (${limit}). Increase "experimental.subagent_depth" to allow nested subagents.`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (!ctx.extra?.bypassAgentCheck) {
|
||||
yield* ctx.ask({
|
||||
permission: id,
|
||||
|
|
|
|||
|
|
@ -313,6 +313,89 @@ describe("tool.task", () => {
|
|||
),
|
||||
)
|
||||
|
||||
it.live("prevents subagents from launching subagents by default", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const child = yield* sessions.create({ parentID: chat.id, title: "child" })
|
||||
const nested = 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: nested.id,
|
||||
agent: "general",
|
||||
abort: new AbortController().signal,
|
||||
extra: { promptOps: stubOps() },
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
ask: () => Effect.sync(() => (asked = true)),
|
||||
},
|
||||
)
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(exit._tag).toBe("Failure")
|
||||
expect(asked).toBe(false)
|
||||
expect(yield* sessions.children(child.id)).toHaveLength(0)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("allows nested subagents up to the configured depth", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const child = yield* sessions.create({ parentID: chat.id, title: "child" })
|
||||
const nested = 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: nested.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: { experimental: { subagent_depth: 2 } } },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("execute shapes child permissions for task, todowrite, and primary tools", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -1623,6 +1623,10 @@ export type Config = {
|
|||
* Tools that should only be available to primary agents.
|
||||
*/
|
||||
primary_tools?: Array<string>
|
||||
/**
|
||||
* Maximum subagent nesting depth. Defaults to 1.
|
||||
*/
|
||||
subagent_depth?: number
|
||||
/**
|
||||
* Continue the agent loop when a tool call is denied
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue