feat: return typed values from Code Mode

This commit is contained in:
Tianyi Cui
2026-07-21 04:34:14 +08:00
parent 66c36e7325
commit c1d7b0df81
50 changed files with 2155 additions and 580 deletions

View File

@@ -6,7 +6,7 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
import type { Config, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
@@ -68,13 +68,17 @@ async function mintAgentScope(ctx: Context, name = 'scoped'): Promise<{ scope: S
/** Register a trivial echo tool; returns the calls it received. */
function registerEcho(ctx: Context, name = 'echo'): unknown[] {
const calls: unknown[] = []
ctx.tools.register(defineContentToolFixture({
ctx.tools.register(defineTool({
name,
description: `Echo tool ${name}.`,
parameters: { value: { type: 'string', required: true } },
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute(args) {
calls.push(args)
return Promise.resolve([{ type: 'text' as const, text: `${name}:${args.value}` }])
return Promise.resolve(`${name}:${args.value}`)
},
}))
return calls
@@ -119,8 +123,8 @@ describe('mode-aware wire contribution', () => {
expect(assembly.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')
expect(sdk?.text).toContain('declare const tools: {')
expect(sdk?.text).toContain('echo(args:')
expect(sdk?.text).not.toContain('run_code(args:')
expect(sdk?.text).toContain('echo: {')
expect(sdk?.text).not.toContain('run_code:')
})
it.each(['code', 'both'] as const)('treats expert assembly output as authoritative in mode %s', async (mode) => {
@@ -172,8 +176,8 @@ describe('mode-aware wire contribution', () => {
? [RUN_CODE_NAME]
: ['echo', RUN_CODE_NAME])
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text
expect(sdk).toContain('echo(args:')
expect(sdk).not.toContain('hidden(args:')
expect(sdk).toContain('echo: {')
expect(sdk).not.toContain('hidden:')
runtime.behavior = request => Promise.resolve({
logs: [],
@@ -202,8 +206,8 @@ describe('mode-aware wire contribution', () => {
? [RUN_CODE_NAME]
: ['kept', RUN_CODE_NAME])
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text
expect(sdk).not.toContain('denied(args:')
expect(sdk).toContain('kept(args:')
expect(sdk).not.toContain('denied:')
expect(sdk).toContain('kept: {')
runtime.behavior = request => Promise.resolve({
logs: [],
@@ -241,7 +245,7 @@ describe('mode-aware wire contribution', () => {
expect(transports).toHaveLength(1)
expect(transports[0]?.description).toContain('Execute a TypeScript program')
expect(assembly.sections.find(section => section.name === 'scoped-note')?.text).toBe('safe note')
expect(assembly.sections.find(section => section.name === 'tools:sdk')?.text).toContain('scoped_safe(args:')
expect(assembly.sections.find(section => section.name === 'tools:sdk')?.text).toContain('scoped_safe:')
expect(ctx.tools.get(RUN_CODE_NAME, agent)).toBe(ctx.tools.get(RUN_CODE_NAME))
const result = await runCode(ctx, 'return 1', { agent })
expect(result.content).toEqual([{ type: 'text', text: '(run_code completed with no output)' }])
@@ -328,7 +332,8 @@ describe('the run_code dispatch bridge', () => {
const tools = request.bindings[0]!.functions
const first = await tools.echo!({ value: 'one' })
const second = await tools.echo!({ value: 'two' })
return { logs: [`saw ${String(first)}`], value: second }
if (typeof first !== 'string' || typeof second !== 'string') throw new Error('echo returned a non-string')
return { logs: [`saw ${first}`], value: second }
}
const result = await runCode(ctx, 'const …: string = …', { agent })
expect(result.isError).toBe(false)
@@ -376,10 +381,14 @@ describe('the run_code dispatch bridge', () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const intervals: [string, string][] = []
let active = 0
ctx.tools.register(defineContentToolFixture({
ctx.tools.register(defineTool({
name: 'probe',
description: 'Records execution overlap.',
parameters: { id: { type: 'string', required: true } },
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
active++
expect(active, 'probe executions overlapped').toBe(1)
@@ -387,12 +396,13 @@ describe('the run_code dispatch bridge', () => {
await new Promise(resolve => setTimeout(resolve, 20))
intervals.push(['exit', args.id])
active--
return [{ type: 'text' as const, text: args.id }]
return args.id
},
}))
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const values = await Promise.all([tools.probe!({ id: 'a' }), tools.probe!({ id: 'b' }), tools.probe!({ id: 'c' })])
if (!values.every(value => typeof value === 'string')) throw new Error('probe returned a non-string')
return { logs: [], value: values.join(',') }
}
const result = await runCode(ctx, 'program')
@@ -422,7 +432,7 @@ describe('the run_code dispatch bridge', () => {
}
}
const result = await runCode(ctx, 'program')
expect(result.content[0]).toEqual({ type: 'text', text: 'caught: Error: deliberate failure' })
expect(result.content[0]).toEqual({ type: 'text', text: 'caught: deliberate failure' })
})
it('a tools/pre-execute deny reaches the program as a binding rejection', async () => {
@@ -445,7 +455,7 @@ describe('the run_code dispatch bridge', () => {
expect((result.content[0] as { text: string }).text).toContain('not on my watch')
})
it('rejects a binding argument that does not survive JSON normalization, dispatching nothing', async () => {
it('rejects a binding argument that is not lossless JSON, dispatching nothing', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)
const { agent, events } = fakeAgent()
@@ -458,25 +468,24 @@ describe('the run_code dispatch bridge', () => {
}
}
const result = await runCode(ctx, 'program', { agent })
expect((result.content[0] as { text: string }).text).toContain('JSON-serializable')
expect((result.content[0] as { text: string }).text).toContain('lossless JSON')
expect(calls).toEqual([])
expect(events.filter(event => event.type === 'tool/code-dispatch')).toEqual([])
})
it('dispatches the JSON-normalized value: what the tool sees is what the event logs', async () => {
it('dispatches and logs independent snapshots of the same lossless JSON value', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
// A Date survives structured clone but is not JSON; the bridge
// normalizes it to its JSON form (an ISO string) BEFORE dispatch.
await request.bindings[0]!.functions.echo!({ value: 'x', when: new Date(0) }).catch(() => undefined)
const args = Object.assign(Object.create(null) as Record<string, unknown>, { value: 'x', nested: ['same'] })
await request.bindings[0]!.functions.echo!(args)
return { logs: [] }
}
await runCode(ctx, 'program', { agent })
expect(calls).toEqual([{ value: 'x', when: '1970-01-01T00:00:00.000Z' }])
expect(calls).toEqual([{ value: 'x', nested: ['same'] }])
const dispatch = events.find(event => event.type === 'tool/code-dispatch')?.data as SessionEventMap['tool/code-dispatch']
expect(dispatch.arguments).toEqual({ value: 'x', when: '1970-01-01T00:00:00.000Z' })
expect(dispatch.arguments).toEqual({ value: 'x', nested: ['same'] })
})
it('defers sub-call additionalContexts onto the outer run_code result', async () => {
@@ -691,15 +700,19 @@ describe('the run_code dispatch bridge', () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const { agent, events } = fakeAgent()
const long = 'x'.repeat(300)
ctx.tools.register(defineContentToolFixture({
ctx.tools.register(defineTool({
name: 'mixed',
description: 'Returns mixed content.',
parameters: {},
output: {
schema: { type: 'string' },
render: () => [
{ type: 'text', text: long },
{ type: 'reasoning', text: 'hidden' },
],
},
execute() {
return Promise.resolve([
{ type: 'text' as const, text: long },
{ type: 'reasoning' as const, text: 'hidden' },
])
return Promise.resolve('mixed-value')
},
}))
runtime.behavior = async (request) => {
@@ -708,7 +721,7 @@ describe('the run_code dispatch bridge', () => {
}
const result = await runCode(ctx, 'program', { agent })
expect(result.isError).toBe(false)
expect((result.content[0] as { text: string }).text).toBe(`${long}\n[reasoning content]`)
expect((result.content[0] as { text: string }).text).toBe('mixed-value')
const dispatch = events.find(event => event.type === 'tool/code-dispatch')?.data as SessionEventMap['tool/code-dispatch']
expect(dispatch.resultSummary.length).toBe(201)
expect(dispatch.resultSummary.endsWith('…')).toBe(true)
@@ -716,13 +729,17 @@ describe('the run_code dispatch bridge', () => {
it('normalizes the session workspace root before bounding durable result summaries', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
ctx.tools.register(defineContentToolFixture({
ctx.tools.register(defineTool({
name: 'workspace_path',
description: 'Return a path beneath the session workspace.',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute(_args, exec) {
const cwd = exec.agent?.session.header.cwd ?? ''
return Promise.resolve([{ type: 'text' as const, text: `<path>${cwd}/nested/task.txt</path>\n${'x'.repeat(240)}` }])
return Promise.resolve(`<path>${cwd}/nested/task.txt</path>\n${'x'.repeat(240)}`)
},
}))
runtime.behavior = async request => ({
@@ -760,7 +777,7 @@ describe('the run_code dispatch bridge', () => {
expect((root.events[0]!.data as SessionEventMap['tool/code-dispatch']).resultSummary).toBe('echo:/workspace/value')
})
it('rejects undefined, JSON-throwing, and JSON-unrepresentable binding arguments BEFORE dispatch', async () => {
it('rejects undefined, getter-throwing, exotic, and unrepresentable binding arguments before dispatch', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)
const { agent, events } = fakeAgent()
@@ -773,8 +790,9 @@ describe('the run_code dispatch bridge', () => {
// Root undefined must reject up front: the event log rejects it as
// data, and nothing may execute unlogged.
await catchMessage(echo(undefined)),
// A toJSON that throws a NON-Error propagates out of JSON.stringify.
await catchMessage(echo({ toJSON() { throw 'raw-throw' } })),
await catchMessage(echo(Object.defineProperty({}, 'bad', { enumerable: true, get() { throw 'raw-throw' } }))),
await catchMessage(echo(Object.defineProperty({}, 'bad', { enumerable: true, get() { throw new Error('error-throw') } }))),
await catchMessage(echo(new Date(0))),
// A bare function is a value JSON cannot represent at all.
await catchMessage(echo(() => 1)),
].join(' | '),
@@ -783,9 +801,10 @@ describe('the run_code dispatch bridge', () => {
const result = await runCode(ctx, 'program', { agent })
const text = (result.content[0] as { text: string }).text
expect(text).toContain('call the tool with an arguments object')
expect(text).toContain('JSON-serializable: raw-throw')
expect(text).toContain('a value JSON cannot represent')
// None of the three dispatched, none logged.
expect(text).toContain('lossless JSON: raw-throw')
expect(text).toContain('lossless JSON: error-throw')
expect(text.match(/tool arguments must be lossless JSON/g)).toHaveLength(5)
// None dispatched or logged.
expect(calls).toEqual([])
expect(events.filter(event => event.type === 'tool/code-dispatch')).toEqual([])
})
@@ -816,11 +835,15 @@ describe('the run_code dispatch bridge', () => {
it('exposes a tool named __proto__ as an ordinary own binding', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
ctx.tools.register(defineContentToolFixture({
ctx.tools.register(defineTool({
name: '__proto__',
description: 'A prototype-colliding tool name.',
parameters: {},
execute() { return Promise.resolve([{ type: 'text' as const, text: 'proto-tool-ok' }]) },
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute() { return Promise.resolve('proto-tool-ok') },
}))
runtime.behavior = async (request) => {
const functions = request.bindings[0]!.functions
@@ -833,11 +856,20 @@ describe('the run_code dispatch bridge', () => {
expect(result.content[0]).toEqual({ type: 'text', text: 'proto-tool-ok' })
})
it('renders a non-string completion value inspect-style', async () => {
it('renders every non-string JSON root as pretty JSON while preserving strings raw', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
runtime.behavior = () => Promise.resolve({ logs: [], value: { n: 42 } })
const result = await runCode(ctx, 'program')
expect((result.content[0] as { text: string }).text).toBe('{ n: 42 }')
expect((await runCode(ctx, 'object')).content[0]).toEqual({ type: 'text', text: '{\n "n": 42\n}' })
runtime.behavior = () => Promise.resolve({ logs: [], value: ['x', 2] })
expect((await runCode(ctx, 'array')).content[0]).toEqual({ type: 'text', text: '[\n "x",\n 2\n]' })
runtime.behavior = () => Promise.resolve({ logs: [], value: null })
expect((await runCode(ctx, 'null')).content[0]).toEqual({ type: 'text', text: 'null' })
runtime.behavior = () => Promise.resolve({ logs: [], value: 'raw' })
expect((await runCode(ctx, 'string')).content[0]).toEqual({ type: 'text', text: 'raw' })
runtime.behavior = () => Promise.resolve({ logs: [] })
const absent = await runCode(ctx, 'undefined')
expect(absent.content[0]).toEqual({ type: 'text', text: '(run_code completed with no output)' })
expect(absent.isError ? undefined : absent.value).toEqual({ logs: [] })
})
it('reports a pre-aborted outer signal as the run failure without dispatching anything', async () => {

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { jsonSchemaToTs, renderToolsSdk } from '@deepseek-ai/dsh-tools/src/ts-types.ts'
import type { ToolSdkSchema } from '@deepseek-ai/dsh-tools/src/ts-types.ts'
import { parameterSchemaSpecToJsonSchema } from '@deepseek-ai/dsh-tools'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
describe('jsonSchemaToTs', () => {
it('maps every unified schema construct', () => {
@@ -96,31 +96,45 @@ describe('jsonSchemaToTs', () => {
})
describe('renderToolsSdk', () => {
const bash: ToolSchema = {
const bash: ToolSdkSchema = {
name: 'bash',
description: 'Run a shell command.',
parameters: parameterSchemaSpecToJsonSchema({ command: { type: 'string', required: true } }) as unknown as Record<string, unknown>,
output: {
type: 'object',
additionalProperties: false,
properties: { exitCode: { type: 'integer' } },
required: ['exitCode'],
},
}
const exotic: ToolSchema = {
const exotic: ToolSdkSchema = {
name: 'my-mcp.tool',
description: 'Exotic name.',
parameters: parameterSchemaSpecToJsonSchema({}) as unknown as Record<string, unknown>,
output: { type: 'array', items: { type: 'string' } },
}
it('declares every tool in lexicographic order with quoted keys for exotic names', () => {
const text = renderToolsSdk([exotic, bash])
expect(text).toContain('interface ToolArgsMap {')
expect(text).toContain('interface ToolOutputMap {')
expect(text).toContain('type ToolName = keyof ToolOutputMap')
expect(text).toContain('declare class ToolCallError extends Error')
expect(text).toContain('readonly toolName: ToolName;')
expect(text).toContain('declare const tools: {')
expect(text).toContain('type JsonValue = null | boolean | number | string')
expect(text.indexOf('bash(args:')).toBeGreaterThan(0)
expect(text).toContain('"my-mcp.tool"(args:')
expect(text.indexOf('bash(args:')).toBeLessThan(text.indexOf('"my-mcp.tool"(args:'))
expect(text).toContain('): Promise<string>;')
expect(text.indexOf('bash: {')).toBeGreaterThan(0)
expect(text).toContain('"my-mcp.tool":')
expect(text.indexOf('bash:')).toBeLessThan(text.indexOf('"my-mcp.tool":'))
expect(text).toContain('exitCode: number;')
expect(text).toContain('"my-mcp.tool": string[];')
expect(text).toContain('[K in ToolName]: (args: ToolArgsMap[K]) => Promise<ToolOutputMap[K]>;')
expect(text).toContain('/** Run a shell command. */')
// The fixed instruction lines the model relies on.
expect(text).toContain('erasable syntax only')
expect(text).toContain('rejects with an `Error`')
expect(text).toContain('rejects with `ToolCallError`')
expect(text).toContain('sequentially, even under `Promise.all`')
expect(text).toContain('JSON-serializable')
expect(text).toContain('lossless JSON')
})
it('is deterministic: same tool set, byte-identical text regardless of input order', () => {
@@ -130,6 +144,8 @@ describe('renderToolsSdk', () => {
})
it('renders an empty declaration for an empty tool set', () => {
expect(renderToolsSdk([])).toContain('declare const tools: {}')
const text = renderToolsSdk([])
expect(text).toContain('interface ToolArgsMap {}')
expect(text).toContain('interface ToolOutputMap {}')
})
})