mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(tool-subagent): default maxDepth to 3
This commit is contained in:
@@ -49,9 +49,11 @@ The global registry remains live. A deny-only filter admits a later global name
|
||||
|
||||
The depth limit bounds recursive delegation independently of tool visibility. A top-level agent has depth zero; an in-process child has its parent's validated depth plus one. `maxDepth` is an absolute non-negative safe integer, and a start rejects before child ownership begins when the derived child depth is greater than the cap.
|
||||
|
||||
Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. Omitting the cap leaves depth unbounded by this mechanism.
|
||||
The effective parent depth is the greater of durable `SessionHeader.delegationDepth` and runtime `AgentOptions.subagentDepth`. An in-process child records its derived depth in the session header, and resume restores that header, so a restart cannot lower the recursion count.
|
||||
|
||||
A deployment can combine depth and filtering. For example, it may keep the delegation tool visible at depth one but set `maxDepth: 1`, or deny the delegation tool entirely in children. Neither choice changes the provider's conversation-history behavior.
|
||||
Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`.
|
||||
|
||||
A deployment can combine depth and filtering. When a numeric tool cap and `toolFilter` are supported, `dsh-tool-subagent` denies its configured tool name in a child whose derived depth is at the cap; the provider's independent depth check still rejects direct or alternate starts beyond it. A deployment may also deny delegation tools entirely in children. Neither choice changes the provider's conversation-history behavior.
|
||||
|
||||
### Capability gating keeps providers honest
|
||||
|
||||
|
||||
@@ -1180,7 +1180,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
|
||||
|
||||
@@ -105,7 +105,7 @@ const SCENARIOS: Scenario[] = [
|
||||
},
|
||||
{ name: 'cancel', hasModelTurn: true, recorded: false, overridden: true },
|
||||
{ name: 'cancel-tool-calls', hasModelTurn: true, recorded: false, overridden: true },
|
||||
// Children sit AT the default depth cap (maxDepth 1), so each child's header
|
||||
// Children sit at this example's configured depth cap (`maxDepth: 1`), so each child's header
|
||||
// legitimately omits the delegation tool that spawned it (schema hiding).
|
||||
{ name: 'subagent-spawn', hasModelTurn: true, recorded: true, childToolOmissions: ['subagent'] },
|
||||
{ name: 'subagent-multi', hasModelTurn: true, recorded: true, childToolOmissions: ['subagent'] },
|
||||
@@ -125,7 +125,7 @@ const SCENARIOS: Scenario[] = [
|
||||
pinsHeader: true,
|
||||
headerClass: 'advanced',
|
||||
configPath: ADVANCED_CONFIG,
|
||||
// The direct spawn child sits AT the default cap and loses `subagent`;
|
||||
// The direct spawn child sits at the configured cap and loses `subagent`;
|
||||
// workflow children bypass tool-subagent and keep the full set.
|
||||
childToolOmissions: ['subagent'],
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -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']))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user