fix(code-mode): render deep results iteratively

This commit is contained in:
Tianyi Cui
2026-07-22 19:57:44 +08:00
parent 68c511bf2c
commit 994bb4b07c
11 changed files with 138 additions and 19 deletions

View File

@@ -117,7 +117,7 @@ Under `code` or `both`, the registry exposes the reserved `run_code` transport a
- **The SDK section** (`tools:sdk`, order 150): a lazy prompt section regenerating, at each assembly, `JsonValue`, exact `ToolArgsMap` / `ToolOutputMap`, `ToolName`, the `ToolCallError` declaration, and a mapped `tools` namespace for the calling scope's visible end capabilities (exotic names via quoted keys), plus fixed usage instructions. Deterministic — lexicographic tool order, byte-identical text for an unchanged tool set (prefix-cache-friendly). The codegen (`jsonSchemaToTs`, exported) handles every unified schema construct and degrades unsupported raw constructs to `unknown`, never throwing during prompt assembly.
- **The dispatch bridge** (`run_code`'s execute): every binding call is snapshotted as lossless JSON before dispatch (`undefined`, `BigInt`, cycles, sparse arrays, `-0`, and exotic objects reject that one call), serialized through a per-run queue (even `Promise.all` executes underlying calls one at a time in submission order), given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A success returns the final canonical value after policy; a failure reaches the worker as one message and becomes `ToolCallError(toolName, message)`. Each sub-call is logged as a `tool/code-dispatch` session event with deterministic id `<parent>:code:<n>` and a bounded Native-content summary; `deriveMessages()` does not surface that event or persist the value. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and retaining each source/meta even when the program later fails.
- **Settlement discipline**: the bridge owns a run-scoped abort that follows the outer signal in and fires when the run settles for any reason, so a budget expiry aborts an in-flight sub-tool instead of orphaning it; the bridge then drains its queue BEFORE returning, so every `tool/code-dispatch` lands inside the open turn. A failed run throws `CodeRunFailedError` (`code: 'CODE_RUN_FAILED'`, message = the failure kind + captured logs), which the pipeline converts to a structured `isError` the model self-corrects from.
- **Result boundary**: intermediate binding values cross the worker boundary whole and have no per-binding byte cap. `run_code` returns canonical `{ logs: string[], result?: JsonValue }`; strings render raw, every other present JSON root renders as pretty JSON, `null` remains explicit, and absent `result` means the program returned `undefined`. The worker's configurable `maxOutputBytes` (default 64 MiB) applies only to the combined serialized outer logs, completion, or failure diagnostic; invalid and over-limit completions fail explicitly, and only this outer result is eligible for ordinary spill.
- **Result boundary**: intermediate binding values cross the worker boundary whole and have no per-binding byte cap. `run_code` returns canonical `{ logs: string[], result?: JsonValue }`; strings render raw, every other present JSON root renders through a stack-safe pretty JSON traversal whose total indentation is capped at ten characters (deeper subtrees stay compact), `null` remains explicit, and absent `result` means the program returned `undefined`. The worker's configurable `maxOutputBytes` (default 64 MiB) applies only to the combined serialized outer log-array, completion-value, or failure-message payloads; fixed result-envelope syntax and presentation whitespace are outside that ledger. Invalid and over-limit completions fail explicitly, and only this outer result is eligible for ordinary spill.
### Parallel execution

View File

@@ -102,9 +102,94 @@ function jsonNormalizeArgs(value: unknown): { dispatched: unknown; logged: unkno
return { dispatched: structuredClone(snapshot), logged: structuredClone(snapshot) }
}
/** Two-space JSON presentation, matching the existing shallow `run_code` text contract. */
const JSON_INDENT = ' '
/**
* ECMAScript caps `JSON.stringify`'s `space` string at ten characters. The
* renderer also caps TOTAL indentation there, compacting deeper subtrees, so
* formatted output remains linear in the canonical JSON size.
*/
const MAX_JSON_INDENT_CHARS = 10
/** A pending fragment in the iterative JSON presentation traversal. */
type JsonRenderTask =
| { kind: 'text'; text: string }
| { kind: 'value'; value: JsonValue; depth: number; compact: boolean }
/** Render one non-string JSON root without recursive traversal or unbounded indentation growth. */
function renderJsonValue(value: Exclude<JsonValue, string>): string {
const chunks: string[] = []
const tasks: JsonRenderTask[] = [{ kind: 'value', value, depth: 0, compact: false }]
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
if (task.kind === 'text') {
chunks.push(task.text)
continue
}
const current = task.value
if (current === null || typeof current === 'boolean' || typeof current === 'number') {
chunks.push(String(current))
continue
}
if (typeof current === 'string') {
chunks.push(JSON.stringify(current))
continue
}
const compact = task.compact || (task.depth + 1) * JSON_INDENT.length > MAX_JSON_INDENT_CHARS
const childDepth = task.depth + 1
if (Array.isArray(current)) {
chunks.push('[')
if (current.length === 0) {
chunks.push(']')
continue
}
tasks.push({ kind: 'text', text: compact ? ']' : `\n${JSON_INDENT.repeat(task.depth)}]` })
for (let index = current.length - 1; index >= 0; index--) {
const item = current[index]
/* v8 ignore next -- canonical JsonValue arrays are dense. */
if (item === undefined) throw new Error('cannot render a sparse JSON array')
tasks.push({ kind: 'value', value: item, depth: childDepth, compact })
tasks.push({
kind: 'text',
text: compact
? index === 0 ? '' : ','
: `${index === 0 ? '\n' : ',\n'}${JSON_INDENT.repeat(childDepth)}`,
})
}
continue
}
const keys = Object.keys(current)
chunks.push('{')
if (keys.length === 0) {
chunks.push('}')
continue
}
tasks.push({ kind: 'text', text: compact ? '}' : `\n${JSON_INDENT.repeat(task.depth)}}` })
for (let index = keys.length - 1; index >= 0; index--) {
const key = keys[index]
/* v8 ignore next -- the loop is bounded by the captured key count. */
if (key === undefined) throw new Error('cannot render a missing JSON object key')
const item = current[key]
/* v8 ignore next -- canonical JsonValue records contain no undefined properties. */
if (item === undefined) throw new Error('cannot render an undefined JSON object property')
tasks.push({ kind: 'value', value: item, depth: childDepth, compact })
tasks.push({
kind: 'text',
text: compact
? `${index === 0 ? '' : ','}${JSON.stringify(key)}:`
: `${index === 0 ? '\n' : ',\n'}${JSON_INDENT.repeat(childDepth)}${JSON.stringify(key)}: `,
})
}
}
return chunks.join('')
}
/** Render one present program completion value for the model-facing result text. */
function renderValue(value: JsonValue): string {
return typeof value === 'string' ? value : JSON.stringify(value, null, 2)
return typeof value === 'string' ? value : renderJsonValue(value)
}
/** The run_code result's `meta` payload (JSON-serializable; `presentResult` narrows it back). */

View File

@@ -10,7 +10,7 @@ import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DI
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'
import type { SessionEventMap } from '@deepseek-ai/dsh-session'
import type { JsonValue, SessionEventMap } from '@deepseek-ai/dsh-session'
const testToolSignal = new AbortController().signal
@@ -860,10 +860,17 @@ describe('the run_code dispatch bridge', () => {
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 } })
expect((await runCode(ctx, 'object')).content[0]).toEqual({ type: 'text', text: '{\n "n": 42\n}' })
runtime.behavior = () => Promise.resolve({ logs: [], value: { n: 42, ok: true } })
expect((await runCode(ctx, 'object')).content[0]).toEqual({ type: 'text', text: '{\n "n": 42,\n "ok": true\n}' })
runtime.behavior = () => Promise.resolve({ logs: [], value: {} })
expect((await runCode(ctx, 'empty object')).content[0]).toEqual({ type: 'text', text: '{}' })
const nested = { outer: [{ inner: true }] }
runtime.behavior = () => Promise.resolve({ logs: [], value: nested })
expect((await runCode(ctx, 'nested')).content[0]).toEqual({ type: 'text', text: JSON.stringify(nested, null, 2) })
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: [] })
expect((await runCode(ctx, 'empty array')).content[0]).toEqual({ type: 'text', text: '[]' })
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' })
@@ -874,6 +881,27 @@ describe('the run_code dispatch bridge', () => {
expect(absent.isError ? undefined : absent.value).toEqual({ logs: [] })
})
it('renders deeply nested JSON without recursive traversal or quadratic indentation', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
let value: JsonValue = {
emptyArray: [],
emptyObject: {},
pair: ['leaf', 2],
record: { first: true, second: null },
}
for (let depth = 0; depth < 5_000; depth++) value = [value]
runtime.behavior = () => Promise.resolve({ logs: [], value })
const result = await runCode(ctx, 'deep result')
expect(result.isError).toBe(false)
const text = (result.content[0] as { type: 'text'; text: string }).text
expect(text.startsWith('[\n [\n [')).toBe(true)
expect(text).toContain('"leaf"')
expect(text.endsWith(']')).toBe(true)
expect(text.length).toBeLessThan(11_000)
})
it('short-circuits a pre-aborted outer signal before the code runtime', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)