fix(tools): collapse code-mode executor to run_code for model-direct calls

wireSchemas() already advertised only run_code under mode: 'code', but the
executor resolved every call through get(), which returns the full visible
map plus the reserved transport. A model could name a native tool directly
and bypass run_code entirely. Route the execution-path lookups through a
new private resolveExecution() that applies the mode collapse at the
operation boundary: model-direct calls under 'code' may only name run_code
(UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep
every visible tool. get()/schemas() public semantics are unchanged.

The denial happens at createExecution, before the extensible policy
pipeline — pre-execute listeners, approval ask, and guards never observe
a call that is deterministically denied. A collapsed call honors the
pre-dispatch cancellation contract, routes aborted results through the
visible tool's finalizeContent, and captures the finalizer before
argument materialization.

Under code mode, a system-prompt/assemble listener filters out tool:*
guidance sections that told the model to call native tools directly.
The tools:sdk section and SDK types remain so programs can still use
all tools through run_code.

Fixes #1815
This commit is contained in:
Chinesezjc
2026-08-10 23:13:28 +08:00
parent 564a853a04
commit 4806fdabab
9 changed files with 232 additions and 54 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { Context } from 'cordis'
import { createUserMessage, CallId, HarnessError, type ContentBlock } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -907,6 +907,25 @@ describe('ToolRegistry', () => {
})
})
it('keeps the normalized content when the final content transform returns undefined', async () => {
const ctx = await setup()
let finalized = 0
ctx.tools.register({
...echoTool,
name: 'identity-finalizer',
finalizeContent() {
finalized += 1
return undefined
},
})
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('identity-finalizer'), name: 'identity-finalizer', arguments: {} })
expect(result.isError).toBe(false)
expect(result.content).toEqual([{ type: 'text', text: '' }])
expect(finalized).toBe(1)
})
it('a block decision can ALSO attach additionalContexts', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)