Merge remote-tracking branch 'origin/master' into codex/enforce-tool-cancellation

# Conflicts:
#	docs/cookbook/adding-a-tool.i18n.yaml
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	packages/bash/tool-bash/src/index.ts
#	packages/core/agent-loop/README.md
#	packages/core/tools/README.md
#	packages/core/tools/tests/scoped.spec.ts
#	packages/fs/tool-fs-search/tests/tools.spec.ts
#	website/zh-CN/api/harness/events.md
#	website/zh-CN/api/harness/tools.md
This commit is contained in:
Tianyi Cui
2026-07-20 23:00:21 +08:00
736 changed files with 22158 additions and 13229 deletions

View File

@@ -7,6 +7,7 @@ import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-too
import { type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import TaskService from '@deepseek-ai/dsh-tasks'
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
import * as mock from './scripted-provider.ts'
@@ -24,7 +25,7 @@ const testToolSignal = new AbortController().signal
* shipping code path.
*/
/** A minimal parent Agent — the tool reads `agent.id` for `parent`. */
/** A minimal parent Agent passed through to the provider request. */
function fakeAgent(id = 'parent-1'): Agent {
return { id: SessionId(id) } as unknown as Agent
}
@@ -88,7 +89,7 @@ describe('dsh-tool-subagent', () => {
// Schema omission is advertising, not enforcement: the arg validator
// allows undeclared keys, so the opt-out must also hold in execute().
const ctx = await setup({ provider: 'mock', enableRunInBackground: false })
const parent = { id: SessionId('sess-off'), inject: () => {}, session: { header: { version: 0, id: 'sess-off', createdAt: 0 } } } as unknown as Agent
const parent = { id: SessionId('sess-off'), inject: () => {}, options: {}, session: { header: { version: 0, id: 'sess-off', createdAt: 0 } } } as unknown as Agent
const forced = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true }, { agent: parent })
expect(forced.isError).toBe(true)
@@ -167,7 +168,7 @@ describe('dsh-tool-subagent', () => {
dispose: async () => {},
}),
})
await ctx.plugin(tool, { provider: 'weird' })
await ctx.plugin(tool, { provider: 'weird', maxDepth: 'provider-managed' })
const result = await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(result.isError).toBe(true)
@@ -196,7 +197,7 @@ describe('dsh-tool-subagent', () => {
}
},
})
await ctx.plugin(tool, { provider: 'capture', agentOptions: { model: 'child-model' } })
await ctx.plugin(tool, { provider: 'capture', agentOptions: { model: 'child-model' }, maxDepth: 'provider-managed' })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(seen?.agentOptions).toEqual({ model: 'child-model' })
@@ -353,7 +354,7 @@ describe('dsh-tool-subagent', () => {
dispose: async () => void disposed(),
}),
})
await ctx.plugin(tool, { provider: 'spy' })
await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(disposed).toHaveBeenCalledTimes(1)
@@ -376,7 +377,7 @@ describe('dsh-tool-subagent', () => {
dispose: async () => void disposed(),
}),
})
await ctx.plugin(tool, { provider: 'spy' })
await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' })
const result = await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(result.isError).toBe(true)
@@ -409,7 +410,7 @@ describe('dsh-tool-subagent', () => {
}
},
})
await ctx.plugin(tool, { provider: 'spy' })
await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' })
const controller = new AbortController()
const pending = callSubagent(ctx, { description: 'd', prompt: 'p' }, { signal: controller.signal })
@@ -437,7 +438,7 @@ describe('dsh-tool-subagent', () => {
throw new Error('start aborted')
},
})
await ctx.plugin(tool, { provider: 'spy' })
await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' })
const controller = new AbortController()
controller.abort() // already aborted BEFORE the tool runs
@@ -517,7 +518,6 @@ describe('dsh-tool-subagent', () => {
})
it.each([
{ label: 'null', value: null as unknown as number },
{ label: 'a string', value: '1' as unknown as number },
{ label: 'NaN', value: Number.NaN },
{ label: 'positive infinity', value: Number.POSITIVE_INFINITY },
@@ -561,7 +561,7 @@ describe('dsh-tool-subagent', () => {
}
},
})
await ctx.plugin(tool, { provider: 'capture3', toolFilter: { deny: ['subagent'] } })
await ctx.plugin(tool, { provider: 'capture3', toolFilter: { deny: ['subagent'] }, maxDepth: 'provider-managed' })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(seen?.toolFilter).toEqual({ deny: ['subagent'] })
expect(seen?.toolFilter).not.toHaveProperty('allow')
@@ -591,7 +591,7 @@ describe('dsh-tool-subagent', () => {
}
},
})
await ctx.plugin(tool, { provider: 'capture4' })
await ctx.plugin(tool, { provider: 'capture4', maxDepth: 'provider-managed' })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(seen).toBeDefined()
expect(seen).not.toHaveProperty('agentOptions')
@@ -622,6 +622,7 @@ describe('dsh-tool-subagent background mode', () => {
id,
ctx: scopeFiber.ctx,
inject,
options: {},
session: { id, header: { version: 0, id, createdAt: 0 } },
} as unknown as Agent
ctx.agents.register(agent)
@@ -860,6 +861,7 @@ describe('background preflight failure (no orphaned child, by construction)', ()
id,
ctx: scopeFiber.ctx,
inject: () => {},
options: {},
session: { id, header: { version: 0, id, createdAt: 0 } },
} as unknown as Agent
ctx.agents.register(parent)
@@ -894,3 +896,85 @@ describe('background preflight failure (no orphaned child, by construction)', ()
expect(starts).toBe(0)
})
})
describe('depth budget configuration', () => {
/** Mount the tool over a request-capturing provider with full capabilities. */
async function captureSetup(config: Omit<tool.Config, 'provider'> = {}) {
const requests: SubagentStartRequest[] = []
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
ctx.subagents.registerProvider({
name: 'capture',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
inheritsParentContext: false,
start: async (request) => {
requests.push(request)
return {
id: SessionId(`capture-child-${requests.length}`),
localAgent: undefined,
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => {},
}
},
})
await ctx.plugin(tool, { provider: 'capture', ...config })
return { ctx, requests }
}
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(3)
expect(requests[0]?.toolFilter).toBeUndefined()
})
it('forwards an explicit tool filter unchanged instead of encoding the depth policy into it', async () => {
const { ctx, requests } = await captureSetup({ toolFilter: { deny: ['dangerous'] }, maxDepth: 0 })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(requests[0]?.maxDepth).toBe(0)
expect(requests[0]?.toolFilter).toEqual({ deny: ['dangerous'] })
})
it('rejects a numeric maxDepth on a provider without the depthLimit capability at mount', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
ctx.subagents.registerProvider({
name: 'no-depth',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: async () => { throw new Error('unreachable') },
})
await expect(ctx.plugin(tool, { provider: 'no-depth' }))
.rejects.toThrow(/provider-managed/)
})
it("'provider-managed' omits the cap so a capability-less provider mounts and starts", async () => {
const requests: SubagentStartRequest[] = []
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
ctx.subagents.registerProvider({
name: 'external',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: async (request) => {
requests.push(request)
return {
id: SessionId('external-child'),
localAgent: undefined,
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => {},
}
},
})
await ctx.plugin(tool, { provider: 'external', maxDepth: 'provider-managed' })
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(requests[0]?.maxDepth).toBeUndefined()
expect(requests[0]?.toolFilter).toBeUndefined()
})
})