Merge pull request #1873 from deepseek-harness/fix/code-mode-executor-collapse
fix(tools): collapse code-mode executor to run_code for model-direct calls
This commit is contained in:
@@ -13,6 +13,8 @@ import ToolRegistry, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH, T
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from '@deepseek-ai/dsh-agent-loop'
|
||||
import { MockAdapter, textResponse } from './mock-adapter.ts'
|
||||
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
|
||||
async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
|
||||
const ctx = new Context()
|
||||
@@ -687,3 +689,74 @@ describe('tool-call scheduler: failure quiescence', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('code-mode native-tool denial through the agent loop', () => {
|
||||
/** A minimal in-process code runtime for test purposes — never actually runs. */
|
||||
class FakeCodeRuntime extends CodeRuntime {
|
||||
readonly language = 'typescript'
|
||||
readonly isolation = 'fake' as const
|
||||
async run(_request: CodeRunRequest): Promise<CodeRunResult> {
|
||||
return { logs: [] }
|
||||
}
|
||||
}
|
||||
|
||||
async function codeModeHarness(adapter: MockAdapter) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt, { persona: '' })
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FakeCodeRuntime is an internal test helper with an opaque type shape
|
||||
await ctx.plugin(FakeCodeRuntime as any)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
return ctx
|
||||
}
|
||||
|
||||
it('denies a model-direct native-tool call under code mode: tool body never runs and session records UNKNOWN_TOOL', async () => {
|
||||
let toolInvoked = false
|
||||
const tool = defineContentToolFixture({
|
||||
name: 'write',
|
||||
description: 'Write a file.',
|
||||
parameters: {
|
||||
file_path: { type: 'string', required: true },
|
||||
content: { type: 'string', required: true },
|
||||
},
|
||||
async execute(_args, _exec) {
|
||||
toolInvoked = true
|
||||
return [{ type: 'text', text: 'written' }]
|
||||
},
|
||||
})
|
||||
|
||||
// Scripted model emits a native tool call under code mode — the wire
|
||||
// never advertised it, but a non-compliant provider may still emit one.
|
||||
const adapter = new MockAdapter([
|
||||
[
|
||||
...multiCall([{ id: 'call-1', name: 'write', args: { file_path: '/tmp/test', content: 'hello' } }]),
|
||||
...textResponse('ok'),
|
||||
],
|
||||
])
|
||||
|
||||
const ctx = await codeModeHarness(adapter)
|
||||
ctx.tools.register(tool)
|
||||
|
||||
const agent = ctx.agentLoop.create(SessionId('code-native'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'write a file' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The tool body must NOT have executed — the collapse denied the call
|
||||
// at createExecution, before the body could start.
|
||||
expect(toolInvoked).toBe(false)
|
||||
|
||||
// The session must record a tool/result with UNKNOWN_TOOL error so the
|
||||
// transcript faithfully captures that the call was denied.
|
||||
const sessionEvents = events(agent)
|
||||
const toolResult = sessionEvents.find(e => e.type === 'tool/result')
|
||||
expect(toolResult).toBeDefined()
|
||||
expect(toolResult!.data.error).toMatchObject({
|
||||
name: 'ToolNotFoundError',
|
||||
code: 'UNKNOWN_TOOL',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user