fix(tools): name the two bound SDK names and escape NEL
The static-stub sentence over-generalized: `tools` and `ToolCallError` ARE bound at run time, and a model reading "everything below is a stub" could stop catching `ToolCallError`. State the boundary and pin both halves in the fixed-instruction assertions. UNPRINTABLE missed U+0085: it is Cc but not ECMAScript whitespace, so it survived the collapse and reached the docstring raw and invisible. Add it and scope the docstring to Cc, since the `\xNN` escape cannot address the Cf formatting characters that pass through by design. Record the backend PR's two runtime contracts -- inject only `tools` and `ToolCallError`, and bind the assembly-time language to the request -- in the Agent Note and at requireCodeRuntime.
This commit is contained in:
@@ -836,6 +836,14 @@ export class ToolRegistry extends Service {
|
||||
* behind it — hostage to a code runtime existing even under `mode:
|
||||
* 'native'` (the loop's optional-backend idiom, same as
|
||||
* `sessionPersistence`).
|
||||
*
|
||||
* Assembly and `run_code` execution read separately, so the language is not
|
||||
* bound to a request. Harmless while one published backend exists — both
|
||||
* reads return the same flavor — but a reload that swapped in a second
|
||||
* language between them would hand a program written against one SDK to the
|
||||
* other. Binding it belongs to the PR that publishes that backend, which is
|
||||
* also the first point it can be tested; recorded in the
|
||||
* [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md).
|
||||
*/
|
||||
private requireCodeRuntime(): CodeRuntime {
|
||||
const runtime = this.ctx.get('codeRuntime')
|
||||
|
||||
@@ -70,15 +70,25 @@ interface RenderState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Control characters that survive the whitespace collapse in {@link describe}
|
||||
* and have no printable form. CPython rejects source containing a NUL outright
|
||||
* The `Cc` code points that survive the whitespace collapse in {@link describe}
|
||||
* and have no printable form: the C0 controls, DEL, and NEL. U+0009 to U+000D
|
||||
* are absent because ECMAScript `\s` already collapsed them; U+0085 is `Cc` but
|
||||
* NOT in `\s` (TAB/VT/FF/SP/NBSP/ZWNBSP/Zs plus LF/CR/LS/PS), so it survives and
|
||||
* is escaped here. CPython rejects source containing a NUL outright
|
||||
* (`SyntaxError: source code string cannot contain null bytes`), whether it
|
||||
* sits in a docstring or in a comment, so one such byte anywhere in a schema
|
||||
* description would make the whole generated SDK unparseable — the model's only
|
||||
* declaration of the tools. The rest are legal but invisible; escaping them
|
||||
* with the same rule keeps the emitted text readable and the treatment uniform.
|
||||
*
|
||||
* The set stops at `Cc` because the escape is `\xNN`, which addresses exactly
|
||||
* U+0000 to U+00FF. The invisible `Cf` formatting characters (U+00AD soft
|
||||
* hyphen, U+200B ZWSP, U+200E/U+200F bidi marks, U+2060 word joiner) pass
|
||||
* through by design: covering them would need a second `\uNNNN` escape form,
|
||||
* and they are legal in both consumers — only LF and CR terminate a Python
|
||||
* string literal or a `#` comment.
|
||||
*/
|
||||
const UNPRINTABLE = /[\u0000-\u0008\u000e-\u001f\u007f]/g
|
||||
const UNPRINTABLE = /[\u0000-\u0008\u000e-\u001f\u007f\u0085]/g
|
||||
|
||||
/**
|
||||
* The collapsed one-line `description` of a schema node (byte-stable across
|
||||
@@ -519,7 +529,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). Everything declared below is a STATIC STUB describing shapes: the \`TypedDict\` classes are NOT bound 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:
|
||||
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 shapes — 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.
|
||||
|
||||
@@ -106,6 +106,12 @@ describe('renderToolsSdkPy', () => {
|
||||
expect(text).toContain('# tools["class"](args: dict[str, Any]) -> str')
|
||||
// Fixed instruction lines the model relies on.
|
||||
expect(text).toContain('top-level `await`')
|
||||
// The binding boundary: `tools`/`ToolCallError` are bound, the TypedDicts
|
||||
// are not. Both halves are pinned — dropping either one turns a correct
|
||||
// contract into a wrong one (a model that reads only "STATIC STUB" would
|
||||
// stop catching `ToolCallError`).
|
||||
expect(text).toContain('exactly two of the names declared below are bound: `tools` and `ToolCallError`')
|
||||
expect(text).toContain('never `FooArgs(field=1)`, which raises `NameError`')
|
||||
expect(text).toContain('ToolCallError')
|
||||
expect(text).toContain('class ToolCallError(Exception):')
|
||||
expect(text).toContain('MAY overlap under `asyncio.gather`')
|
||||
@@ -732,5 +738,17 @@ describe('renderToolsSdkPy', () => {
|
||||
const others = renderToolsSdkPy([make('bell\u0007esc\u001bdel\u007f')])
|
||||
expect(others).toContain(String.raw`bell\x07esc\x1bdel\x7f`)
|
||||
expect(renderToolsSdkPy([make('tab\tnewline\ncr\r')])).toContain('"""tab newline cr"""')
|
||||
// NEL is the one `Cc` code point the collapse does NOT fold: ECMAScript
|
||||
// whitespace is TAB/VT/FF/SP/NBSP/ZWNBSP/Zs plus LF/CR/LS/PS, and U+0085 is
|
||||
// in none of them, so without the escape it would reach the docstring raw
|
||||
// and be invisible there. NBSP, which IS whitespace, folds instead.
|
||||
const nel = renderToolsSdkPy([make('a\u0085b')])
|
||||
expect(nel).not.toContain('\u0085')
|
||||
expect(nel).toContain(String.raw`# a\x85b`)
|
||||
expect(renderToolsSdkPy([make('nb\u00a0sp')])).toContain('"""nb sp"""')
|
||||
// `Cf` formatting characters pass through by design: `\xNN` cannot address
|
||||
// them, and they terminate neither a Python string literal nor a `#`
|
||||
// comment, so the block stays parseable with the code point intact.
|
||||
expect(renderToolsSdkPy([make('zero\u200bwidth')])).toContain('"""zero\u200bwidth"""')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user