From cb53dbe24a8180645321ce78fbefeffc217ddfb8 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 5 Aug 2026 17:01:51 +0800 Subject: [PATCH] docs(tools): correct the bracket-count sites and the underscore routing rationale --- packages/core/tools/src/py-types.ts | 37 +++++++++++++++------- packages/core/tools/tests/py-types.spec.ts | 12 ++++--- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index 0c088e708c..5ebfc516fb 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -31,8 +31,10 @@ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/ * ABSENT: they are only special in statement position, so ``match: str`` as a * field and ``async def match(...)`` as a method are both legal, and including * them would needlessly degrade common search/regex tool fields to - * ``dict[str, Any]``. Underscore-leading names are handled separately (dunders - * name-mangle or resolve on ``object`` before the proxy hook), not here. + * ``dict[str, Any]``. Underscore-leading names are handled separately, not + * here: a non-dunder ``__token`` name-mangles, a dunder present on + * ``object``/``type`` resolves before the proxy hook, and implicit + * special-method lookup bypasses the hook. */ const RESERVED = new Set([ 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', @@ -137,12 +139,20 @@ const MAX_CLASS_NAME_BASE = 120 * block that is not valid Python at all — the same failure the docstring * escaping in {@link docLines} exists to prevent. 180 leaves headroom for the * few brackets an annotation can add around the chain, all of which count - * toward the same limit: a `Literal[…]` item, plus exactly one of `NotRequired[…]` - * (a chain in a TypedDict field, whose class-body line has no other open - * bracket) or the `def` parameter list still open around a chain in a method's - * RETURN annotation — the two are mutually exclusive, so the worst case is 182. - * An argument annotation is always a bare TypedDict class name and opens - * nothing. + * toward the same limit. Per emission site, counting brackets open at the + * chain's innermost point: + * + * - Return annotation, `async def f(self, args: X) -> chain:` — 180 `list[` + * plus an innermost `Literal[`. The parameter list's `(` closed at the `)` + * before the `->`, so it is NOT open here: 181. + * - TypedDict field, `field: NotRequired[chain]` — a class-body line with no + * other open bracket, and its children start at `listDepth: 1` to reserve + * the `NotRequired[`, so 179 `list[` plus `Literal[`: 181. + * - Argument annotation, `async def f(self, args: chain) -> Y:` — the `(` IS + * still open around it: 180 `list[` plus `Literal[` plus the paren, 182, the + * worst case. Reachable only through a raw `register()` whose `parameters` + * is array-rooted; `defineTool` compiles an object root, so the annotation + * is a bare TypedDict class name that opens nothing. * * A CPython grammar limit, not a deployment choice, so it is fixed rather than * configurable. The sibling `ts-types` renderer needs no counterpart: nothing @@ -564,10 +574,13 @@ export function renderToolsSdkPy(schemas: ToolSdkSchema[]): string { // Not reachable as ``tools.name`` — the model reaches it via // ``tools[name]``. Exotic names and hard keywords are not legal // attributes at all; an underscore-leading name (``_foo``) IS a legal - // attribute and is routed here anyway, so one rule covers every - // underscore form rather than singling out the dunders that would - // name-mangle or resolve on ``object`` ahead of the proxy hook (see - // {@link RESERVED}). The stub lists it as a subscript comment + // attribute and is routed here anyway, because the forms that break + // split three ways — a non-dunder ``__token`` name-mangles at the CALL + // site, a dunder that exists on ``object``/``type`` (``__class__``, + // ``__doc__``) resolves before ``__getattr__`` ever runs, and implicit + // special-method lookup skips the hook entirely — and one rule over the + // whole family costs nothing while a per-form rule would have to + // enumerate them (see {@link RESERVED}). The stub lists it as a subscript comment // (referencing the named TypedDicts too) so a reader sees what is // accessible; runtime resolution goes through the proxy's __getitem__. members.push(`${pad(1)}# tools[${JSON.stringify(schema.name)}](args: ${argType}) -> ${outputType}`) diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index b60ee07e5e..cc7b55c4c1 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -662,11 +662,13 @@ describe('renderToolsSdkPy', () => { }) it('routes every underscore-leading tool name to subscript access', () => { - // `_foo` is a legal Python attribute, unlike an exotic name or a hard - // keyword, but the whole underscore family goes to `tools[name]` under one - // rule: `__meta__` resolves on `object` before the proxy's __getattr__ ever - // runs, and `__token` name-mangles at the CALL SITE inside the model's own - // class. `_foo` follows them so the rule needs no per-form exception. + // `_foo` and `__meta__` are both legal Python attributes, unlike an exotic + // name or a hard keyword, yet the whole underscore family goes to + // `tools[name]` under one rule. Only some forms actually break — `__token` + // name-mangles at the CALL SITE inside the model's own class, and a dunder + // that exists on `object` (`__class__`) resolves before the proxy's + // __getattr__ runs — so the family rule is what routes `_foo` and + // `__meta__`, not a defect in those two names. const make = (name: string): ToolSdkSchema => ({ name, description: 'Leading underscore.',