From f3c8695fd61ae949c90f0a3ee4c6d454493782ec Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 5 Aug 2026 19:07:10 +0800 Subject: [PATCH] test(tools): pin the argument-annotation nesting cap, the worst of the three sites The 182 the cap is chosen against had no direct case: the existing tests cover the root chain and the TypedDict field, both of which start one bracket lower. An array-rooted parameters schema reaches it from a plain ToolSdkSchema literal, no raw register() needed. Exactly 180 arrays over a const scalar is the worst case itself -- the root frame starts at listDepth 0, so every list[ still emits and the innermost Literal[ is reached rather than degraded; one deeper is where the item degrades. Name the subscript tool-name comment in pyScalar's docstring: it quotes through the same JSON.stringify call and inherits the same escapes and the same pass-throughs. --- packages/core/tools/src/py-types.ts | 4 +++- packages/core/tools/tests/py-types.spec.ts | 26 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index c879e04a72..243a1ce13f 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -277,7 +277,9 @@ function childClassName(base: string, segment: string): string { * escape denoting the same character, so the emitted `Literal[...]` both * parses and decodes back to the value the schema declared. DEL and the C1 * controls do reach it raw — legal but invisible, byte-for-byte as in the TS - * flavor; escaping them is a both-flavors change. + * flavor; escaping them is a both-flavors change. The subscript tool-name + * comment quotes its name through the same call and inherits both halves, + * escapes and pass-throughs alike. */ function pyScalar(value: JsonSchemaScalar): string { if (value === true) return 'True' diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index 35d5449bfe..b63edffd2b 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -571,6 +571,32 @@ describe('renderToolsSdkPy', () => { expect(renderToolsSdkPy([tool])).toContain(` rows: ${'list['.repeat(179)}str${']'.repeat(179)}`) }) + it('caps the argument annotation, the site whose enclosing paren stays open', () => { + // The worst of the three emission sites: the parameter list's `(` is still + // open around this annotation, so 180 `list[` plus the innermost bracket + // plus that paren is 182 of CPython's 200. Only a raw `register()` reaches + // it — `defineTool` compiles an object root, whose annotation is a bare + // TypedDict name that opens nothing. + const rooted = (depth: number): ToolSdkSchema => { + let schema: Record = { type: 'string', const: 'x' } + for (let i = 0; i < depth; i++) schema = { type: 'array', items: schema } + return { name: 'rooted', description: 'Array-rooted parameters.', parameters: schema, output: { type: 'string' } } + } + // Exactly at the cap with a scalar underneath is the worst case itself: the + // chain's root frame starts at `listDepth: 0` here, so all 180 `list[` + // still emit and the innermost `Literal[` is reached rather than degraded. + const worst = renderToolsSdkPy([rooted(180)]) + expect(worst).toContain(`async def rooted(self, args: ${'list['.repeat(180)}Literal["x"]${']'.repeat(180)}) -> str:`) + const annotation = worst.split('async def rooted(self, args: ')[1]!.split(') -> str:')[0]! + // 181 brackets on the annotation plus the still-open parameter-list paren, + // the 182 the cap is chosen against. + expect(annotation.split('[').length - 1).toBe(181) + // One array deeper is where the degradation lands, and it lands on the item + // rather than on another `list[`, so the count cannot grow past that. + expect(renderToolsSdkPy([rooted(181)])) + .toContain(`async def rooted(self, args: ${'list['.repeat(180)}Any${']'.repeat(180)}) -> str:`) + }) + it('renders a deeply nested oneOf chain in linear time (no per-level re-materialization)', () => { // Each level is a two-branch oneOf whose first branch recurses; joining the // accumulated union string at every level would be Theta(depth^2). At this