fix: name run_code's required description argument in its model-facing prose

The transport schema requires both `code` and `description`, but the tool
description and both SDK instruction flavors described the call as passing a
program. `description` was reachable only through the parameter schema, so a
model following the prose emitted `{code}` alone and lost the whole written
program to an INVALID_ARGS rejection.

The length and format guidance stays in RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
alone, so the schema and the prompt cannot drift.

Fixes #2426
This commit is contained in:
Yichen Jiang
2026-08-12 23:30:51 +08:00
parent c4e24071d6
commit 63fecf2534
24 changed files with 77 additions and 48 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-15-code-mode.md
2026-06-15-code-mode.md: f9bde7a85d9cf26d3d6670744c955380fa807e3b
2026-06-15-code-mode.zh.md: 84fc5da63d6049e3533f753cf495aad37b075479
2026-06-15-code-mode.md: 343ad4cf8191542016b492d6a16cdbf6c26b385c
2026-06-15-code-mode.zh.md: 668def6cf7e74288c3e16253c4ebefb4781db633
@@ -87,6 +87,8 @@ The worker runtime provides containment, not a security boundary: model code can
The SDK instructs the model to write an async body in the loaded runtime's language (an erasable-TypeScript body by default; a Python `async` body under a Python runtime — see the [language-dispatch note](2026-07-31-code-mode-language-dispatch.md)), call tools through `await tools.name(args)`, catch rejected tool calls when needed, and return or log only the output that should re-enter context. Both flavors state the same contract in their own primitive: independent read-only calls MAY overlap under `Promise.all` (TypeScript) or `asyncio.gather` (Python), mutating calls run alone in submission order, and dependent work sequences with `await`. The declaration prefix can be as large as native schemas, especially in `'both'`, but remains stable for provider caching.
The transport's own `description` and both SDK instruction flavors open by naming `code` and `description` as the call's two required arguments. Prose that describes the call as passing a program leaves the second argument discoverable only through the parameter schema, and a model that emits `{code}` alone loses the whole written program to an `INVALID_ARGS` rejection.
## Consequences
Deployments switching to `'code'` must update any native-only `toolOrder`. Assembly listeners own the integrity of any rewritten protocol messages. Sub-dispatch starts in submission order under a bounded overlap pool, while per-call contexts retain their source, envelope, and metadata through the outer result.
@@ -87,6 +87,8 @@ worker 运行时只能约束程序的运行,而不构成安全边界:模型
SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScriptPython 运行时下为 Python `async` 函数体——见[语言分发 note](2026-07-31-code-mode-language-dispatch.md)),通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一约定:相互独立的只读调用可以(MAY)在 `Promise.all`TypeScript)或 `asyncio.gather`(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 `await` 排序。声明前缀可能与原生 schema 一样大,尤其在 `'both'` 下,但对提供方缓存保持稳定。
传输自身的 `description` 与两种 flavor 的 SDK 说明都以点名 `code``description` 这两个必填参数开头。把该调用描述成「传入一个程序」的散文会让第二个参数只能从参数 schema 中发现,而只发出 `{code}` 的模型会因 `INVALID_ARGS` 被拒,连同已写好的整个程序一起丢失。
## 后果
切换到 `'code'` 的部署必须更新任何仅限 native 的 `toolOrder`。组装监听器有责任维护任何被重写的协议消息的完整性。子分发在有界的重叠池下按提交顺序启动,而每次调用的上下文会通过外层结果保留其 source、信封与元数据。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/tool-catalog.md
tool-catalog.md: 898c5700eddfe49083b2ce0e3e04761298b28bbb
tool-catalog.zh.md: 3b17cf4e3b1b74b0735783cfe899c9c693146c38
tool-catalog.md: b90a17c7a028b8212fbbcabe2490029de0e44d77
tool-catalog.zh.md: fff2af027415ca0031926bdf79ce41f006d80a50
+1 -1
View File
@@ -116,7 +116,7 @@ ask_user_question pauses the tool call until the active UI provider returns a hu
### `run_code`
Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.
Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.
```json
{
+1 -1
View File
@@ -118,7 +118,7 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
### `run_code`
针对可用工具执行 TypeScript 程序。请编写异步函数的**函数体**(仅使用可擦除语法;支持顶层 `await``return`,并根据系统提示词中的声明,以 `await tools.name(args)` 形式调用工具。只有打印或返回的内容会传回,请谨慎筛选。
针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,即异步函数的**函数体**(仅使用可擦除语法;支持顶层 `await``return`;以及 `description`,简要说明该程序做什么。请根据系统提示词中的声明,以 `await tools.name(args)` 形式调用工具。只有打印或返回的内容会传回,请谨慎筛选。
```json
{
@@ -25,7 +25,7 @@ Use subagent in the background by default. Start independent delegations togethe
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
@@ -251,7 +251,7 @@
},
{
"name": "run_code",
"description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.",
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.",
"parameters": {
"type": "object",
"properties": {
@@ -25,7 +25,7 @@ Use subagent in the background by default. Start independent delegations togethe
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
@@ -194,7 +194,7 @@
},
{
"name": "run_code",
"description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.",
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.",
"parameters": {
"type": "object",
"properties": {
@@ -27,7 +27,7 @@ Use subagent in the background by default. Start independent delegations togethe
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
@@ -2,7 +2,7 @@
"initial": [
{
"name": "run_code",
"description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.",
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.",
"parameters": {
"type": "object",
"properties": {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/tools/README.md
README.md: 44eb25b79436a75f08406102fc1e3734e59b1001
README.zh.md: a47e7c54b0cd3fc3c5146a9a8be4d1406f9ef8aa
README.md: 3dddeb51a9f5b98a01a5e0170dacde471be67094
README.zh.md: deb01899fa909e5d430415fa98ea49bd543b2749
+1 -1
View File
@@ -155,7 +155,7 @@ Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.m
```markdown
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
+1 -1
View File
@@ -155,7 +155,7 @@ Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.md#d
```markdown
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
+8 -6
View File
@@ -45,10 +45,11 @@ interface RunCodeFlavor {
*/
const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
description:
'Execute a TypeScript program against the available tools. Write the BODY of an '
+ 'async function (erasable syntax only; top-level `await` and `return` work) and '
+ 'call tools as `await tools.name(args)` per the declarations in the system prompt. '
+ 'Only what you print or return comes back — curate it.',
'Execute a TypeScript program against the available tools. Takes two required '
+ 'arguments: `code`, the BODY of an async function (erasable syntax only; top-level '
+ '`await` and `return` work), and `description`, a short summary of what the program '
+ 'does. Call tools as `await tools.name(args)` per the declarations in the system '
+ 'prompt. Only what you print or return comes back — curate it.',
codeDescription: 'The program: the body of an async TypeScript function.',
}
@@ -59,8 +60,9 @@ const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
*/
const PYTHON_FLAVOR: RunCodeFlavor = {
description:
'Execute a Python program against the available tools. Write the BODY of an '
+ 'async function (top-level `await` and `return` work) and call tools as '
'Execute a Python program against the available tools. Takes two required '
+ 'arguments: `code`, the BODY of an async function (top-level `await` and `return` '
+ 'work), and `description`, a short summary of what the program does. Call tools as '
+ '`await tools.name(args)` per the declarations in the system prompt. Answer '
+ 'with `print(...)` and/or `return <value>` — only that comes back, so curate it.',
codeDescription: 'The program: the body of an async Python function.',
+1 -1
View File
@@ -733,7 +733,7 @@ export function jsonSchemaToPy(schema: unknown): string {
/** The fixed model-facing usage contract rendered above the declarations. */
const SDK_INSTRUCTIONS = `## Writing code for run_code
Pass \`run_code\` the body of an async Python function (top-level \`await\` and \`return\` both work). At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
\`run_code\` takes two required arguments: \`code\` the body of an async Python function (top-level \`await\` and \`return\` both work) — and \`description\`, a short summary of what the program does. At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
- Call tools as \`await tools.name(args)\` — subscript access for exotic, reserved, or underscore-leading names: \`await tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value (each method's return type below). Tool arguments must be lossless JSON.
- A FAILED tool call raises \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose message is human-readable — wrap in \`try/except\` to handle and continue.
+1 -1
View File
@@ -249,7 +249,7 @@ export function jsonSchemaToTs(schema: unknown, indent = 0): string {
/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */
const SDK_INSTRUCTIONS = `## Writing code for run_code
Pass \`run_code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
\`run_code\` takes two required arguments: \`code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. Inside the program:
- Call tools as \`await tools.name(args)\` — quoted access for exotic names: \`tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose \`message\` is human-readable — \`try/catch\` it to handle and continue.
@@ -399,6 +399,10 @@ describe('mode-aware wire contribution', () => {
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a TypeScript program')
expect(runCodeSchema?.description).toContain('BODY of an')
// Both required arguments are named here, not only in the parameter
// schema: prose that describes the call as "pass the program" is what
// leads a model to emit `{code}` alone and fail INVALID_ARGS.
expect(runCodeSchema?.description).toContain('`description`')
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
expect(codeParam.description).toBe('The program: the body of an async TypeScript function.')
})
@@ -410,6 +414,7 @@ describe('mode-aware wire contribution', () => {
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a Python program')
expect(runCodeSchema?.description).toContain('`return <value>`')
expect(runCodeSchema?.description).toContain('`description`')
expect(runCodeSchema?.description).not.toContain('TypeScript')
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
expect(codeParam.description).toBe('The program: the body of an async Python function.')
@@ -166,6 +166,15 @@ describe('renderToolsSdkPy', () => {
expect(text).toContain('tools: Tools')
})
it('names both required call arguments, not just the program', () => {
// The schema requires `code` AND `description`; instructions that mention
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
const text = renderToolsSdkPy([bash])
expect(text).toContain('`code`')
expect(text).toContain('`description`')
expect(text).toContain('two required arguments')
})
it('renders required as plain fields and optional as NotRequired, with per-field description comments', () => {
const tool: ToolSdkSchema = {
name: 'search',
@@ -148,6 +148,15 @@ describe('renderToolsSdk', () => {
expect(text).toContain('lossless JSON')
})
it('names both required call arguments, not just the program', () => {
// The schema requires `code` AND `description`; instructions that mention
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
const text = renderToolsSdk([bash])
expect(text).toContain('`code`')
expect(text).toContain('`description`')
expect(text).toContain('two required arguments')
})
it('is deterministic: same tool set, byte-identical text regardless of input order', () => {
expect(renderToolsSdk([bash, exotic])).toBe(renderToolsSdk([exotic, bash]))
// Equal names sort stably (the comparator's equal arm).