Forráskód Böngészése

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
Yichen Jiang 1 hónapja
szülő
commit
63fecf2534
24 módosított fájl, 61 hozzáadás és 32 törlés
  1. 2 2
      .agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml
  2. 2 0
      .agents/notes/implemented/feature/2026-06-15-code-mode.md
  3. 2 0
      .agents/notes/implemented/feature/2026-06-15-code-mode.zh.md
  4. 2 2
      docs/tool-catalog.i18n.yaml
  5. 1 1
      docs/tool-catalog.md
  6. 1 1
      docs/tool-catalog.zh.md
  7. 1 1
      examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md
  8. 1 1
      examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json
  9. 1 1
      examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md
  10. 1 1
      examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json
  11. 1 1
      examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md
  12. 1 1
      examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json
  13. 3 3
      examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
  14. 3 3
      examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
  15. 2 2
      examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl
  16. 2 2
      packages/core/tools/README.i18n.yaml
  17. 1 1
      packages/core/tools/README.md
  18. 1 1
      packages/core/tools/README.zh.md
  19. 8 6
      packages/core/tools/src/code-mode.ts
  20. 1 1
      packages/core/tools/src/py-types.ts
  21. 1 1
      packages/core/tools/src/ts-types.ts
  22. 5 0
      packages/core/tools/tests/code-mode.spec.ts
  23. 9 0
      packages/core/tools/tests/py-types.spec.ts
  24. 9 0
      packages/core/tools/tests/ts-types.spec.ts

+ 2 - 2
.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml

@@ -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

+ 2 - 0
.agents/notes/implemented/feature/2026-06-15-code-mode.md

@@ -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.

+ 2 - 0
.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md

@@ -87,6 +87,8 @@ worker 运行时只能约束程序的运行,而不构成安全边界:模型
 
 SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 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
docs/tool-catalog.i18n.yaml

@@ -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
docs/tool-catalog.md

@@ -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
docs/tool-catalog.zh.md

@@ -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
 {

+ 1 - 1
examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md

@@ -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.

+ 1 - 1
examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json

@@ -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": {

+ 1 - 1
examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md

@@ -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.

+ 1 - 1
examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json

@@ -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": {

+ 1 - 1
examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md

@@ -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.

+ 1 - 1
examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json

@@ -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": {

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 3
examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 3
examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 2 - 2
examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl


+ 2 - 2
packages/core/tools/README.i18n.yaml

@@ -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
packages/core/tools/README.md

@@ -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
packages/core/tools/README.zh.md

@@ -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
packages/core/tools/src/code-mode.ts

@@ -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
packages/core/tools/src/py-types.ts

@@ -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
packages/core/tools/src/ts-types.ts

@@ -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.

+ 5 - 0
packages/core/tools/tests/code-mode.spec.ts

@@ -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.')

+ 9 - 0
packages/core/tools/tests/py-types.spec.ts

@@ -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',

+ 9 - 0
packages/core/tools/tests/ts-types.spec.ts

@@ -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).

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott