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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user