fix(tools): state the code-mode collapse in the prompt and the denial
The executor collapse landed without telling the model it exists. Every tool contributes its own guidance section naming its tool, none of them qualify how that tool is reached, and they all render before the SDK (orders 100-199 against SDK_SECTION_ORDER 150), so the prompt said "Use the read tool" eleven times and never said only run_code is callable. A real session shows the consequence: the model emitted a native call, read `unknown tool "read"` for a tool the same prompt declares, and concluded the deployment was inconsistent rather than routing through run_code. The registry now contributes `tools:code-only` at order 99 -- ahead of the guidance band -- stating the rule, registered wherever `tools:sdk` is and rendering empty outside an effective `code`. `both` renders it empty because its native calls do execute, which is also why both-mode-turn no longer shares code-mode-turn's expected prompt. The denial itself now names the route back, since a bare UNKNOWN_TOOL for a declared tool is what misled the model.
This commit is contained in:
@@ -135,6 +135,32 @@ describe('mode-aware wire contribution', () => {
|
||||
expect(sdk?.text).not.toContain('run_code:')
|
||||
})
|
||||
|
||||
it("mode 'code' states the run_code-only rule BEFORE the per-tool guidance that names each tool", async () => {
|
||||
const { ctx, systemPrompt } = await setup({ mode: 'code' })
|
||||
registerEcho(ctx)
|
||||
// Stand in for a real tool's guidance section, which sits in the 100-199
|
||||
// band and names its tool without saying how it is reached.
|
||||
ctx.systemPrompt.section({ name: 'tool:echo', order: 100, text: 'Use the echo tool.' })
|
||||
|
||||
const assembly = await systemPrompt.assemble()
|
||||
const names = assembly.sections.map(section => section.name)
|
||||
const rule = assembly.sections.find(section => section.name === 'tools:code-only')
|
||||
expect(rule?.text).toContain(`\`${RUN_CODE_NAME}\` is the only tool you can call directly`)
|
||||
// The rule is worthless after the guidance it qualifies.
|
||||
expect(names.indexOf('tools:code-only')).toBeLessThan(names.indexOf('tool:echo'))
|
||||
expect(names.indexOf('tools:code-only')).toBeLessThan(names.indexOf('tools:sdk'))
|
||||
})
|
||||
|
||||
it("mode 'both' omits the run_code-only rule, because native calls do execute there", async () => {
|
||||
const { ctx, systemPrompt } = await setup({ mode: 'both' })
|
||||
registerEcho(ctx)
|
||||
const assembly = await systemPrompt.assemble()
|
||||
// Registered (the deployment is non-native) but empty, so the renderer
|
||||
// drops it: `both` executes the native call the rule would forbid.
|
||||
expect(assembly.sections.find(section => section.name === 'tools:code-only')?.text).toBe('')
|
||||
expect(assembly.tools.map(tool => tool.name)).toContain('echo')
|
||||
})
|
||||
|
||||
it('projects deeply nested output schemas into the Code Mode SDK without structured-clone recursion', async () => {
|
||||
const { ctx, systemPrompt } = await setup({ mode: 'code' })
|
||||
let output: JsonSchemaNode = { type: 'string' }
|
||||
@@ -1574,6 +1600,11 @@ describe('the run_code dispatch bridge', () => {
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error?.info).toEqual({ name: 'ToolNotFoundError', code: 'UNKNOWN_TOOL' })
|
||||
// The name IS declared to this model, so a bare `unknown tool` reads as a
|
||||
// broken deployment. The denial carries the route instead.
|
||||
expect(result.error?.message).toBe(
|
||||
`unknown tool "write": only \`${RUN_CODE_NAME}\` is callable directly — call \`write\` from inside a \`${RUN_CODE_NAME}\` program instead`,
|
||||
)
|
||||
})
|
||||
|
||||
it('routes a pre-aborted collapsed call through ABORTED_BEFORE_DISPATCH', async () => {
|
||||
|
||||
Reference in New Issue
Block a user