fix(tool-subagent): default maxDepth to 3

This commit is contained in:
Tianyi Cui
2026-07-20 17:12:14 +08:00
parent 27f37942df
commit 1a1ff56ab7
6 changed files with 15 additions and 12 deletions

View File

@@ -22,7 +22,7 @@ With `run_in_background: true`, the tool registers the parent-owned task before
| `agentOptions` | Default child options, currently including `model`. |
| `persona` | Per-child persona; requires provider `persona` capability. |
| `toolFilter` | Per-child global-tool restriction; requires `toolFilter` capability. |
| `maxDepth` | Absolute delegation-depth cap, default `1` (`0` forbids delegation); a numeric cap requires the `depthLimit` capability and fails the mount without it. `'provider-managed'` sends no cap — for an out-of-process provider whose budget belongs to the child harness. A child AT the cap also loses this tool from its schema when the provider supports `toolFilter` (prompt-face hiding; the service still rejects on the execution face). |
| `maxDepth` | Absolute delegation-depth cap, default `3` (`0` forbids delegation); a numeric cap requires the `depthLimit` capability and fails the mount without it. `'provider-managed'` sends no cap — for an out-of-process provider whose budget belongs to the child harness. A child AT the cap also loses this tool from its schema when the provider supports `toolFilter` (prompt-face hiding; the service still rejects on the execution face). |
## Concurrency

View File

@@ -54,7 +54,7 @@ export interface Config {
deny?: string[]
}
/**
* Maximum child depth: a non-negative safe integer (default `1`; `0` forbids
* Maximum child depth: a non-negative safe integer (default `3`; `0` forbids
* delegation entirely), or `'provider-managed'` to send no cap. A numeric cap
* requires the provider's `depthLimit` capability (mount fails loud
* otherwise), and a child AT the cap additionally loses this tool from its
@@ -81,7 +81,7 @@ export const Config: z<Config> = z.object({
allow: z.array(z.string()).default(undefined as unknown as string[]),
deny: z.array(z.string()).default(undefined as unknown as string[]),
}).default(undefined as unknown as { allow: string[]; deny: string[] }),
maxDepth: z.union([z.natural().max(Number.MAX_SAFE_INTEGER), z.const('provider-managed' as const)]).default(1),
maxDepth: z.union([z.natural().max(Number.MAX_SAFE_INTEGER), z.const('provider-managed' as const)]).default(3),
})
/**

View File

@@ -912,23 +912,24 @@ describe('depth budget defaults and schema hiding', () => {
return { ctx, requests }
}
it('defaults maxDepth to 1 and forwards it in the start request', async () => {
it('defaults maxDepth to 3 and forwards it in the start request', async () => {
const { ctx, requests } = await captureSetup()
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(requests[0]?.maxDepth).toBe(1)
expect(requests[0]?.maxDepth).toBe(3)
expect(requests[0]?.toolFilter?.deny ?? []).not.toContain('subagent')
})
it('denies its own toolName to a child at the depth cap', async () => {
// The child of a depth-0 parent under maxDepth 1 sits AT the cap: any
// delegation it attempted would be rejected, so the tool must not appear in
// its schema at all (prompt-face hiding; the service still rejects).
const { ctx, requests } = await captureSetup()
const { ctx, requests } = await captureSetup({ maxDepth: 1 })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(requests[0]?.toolFilter?.deny).toContain('subagent')
})
it('merges the cap denial into a configured tool filter', async () => {
const { ctx, requests } = await captureSetup({ toolFilter: { deny: ['dangerous'] } })
const { ctx, requests } = await captureSetup({ toolFilter: { deny: ['dangerous'] }, maxDepth: 1 })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(requests[0]?.toolFilter?.deny).toEqual(expect.arrayContaining(['dangerous', 'subagent']))
})