diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index 4b5cb1fa31..aa4a1f027a 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -300,10 +300,10 @@ export interface RunCodeBridgeOptions { /** Resolves `ctx.codeRuntime` or throws the loud misconfiguration error (shared with the registry's assembly-time checks). */ requireRuntime: () => CodeRuntime /** - * Reads `ctx.codeRuntime` without throwing: `undefined` when none is - * mounted. Lets schema emission tell "no runtime" (degrade to TS; the - * readers that reach it are {@link resolveFlavor}'s) apart from "unknown - * language" (fail loud). + * Reads `ctx.codeRuntime` without throwing: `undefined` when none is mounted. + * Lets schema emission tell "no runtime" (degrade to TS; the readers that + * reach it are {@link resolveFlavor}'s) apart from "unknown language" (fail + * loud). */ peekRuntime: () => CodeRuntime | undefined /** The run's overlap cap for parallel-classified sub-calls (the registry passes its validated `maxParallelSubCalls`). */ diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index 69aa63fb2d..d1358124c3 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -42,10 +42,11 @@ const IDENTIFIER = /^[\p{XID_Start}_]\p{XID_Continue}*$/u * take the subscript path, which carries their exact bytes. * * `IDENTIFIER`'s equivalence to `str.isidentifier()` was measured across 21 - * samples with zero divergence, on Node 22.23.1 against CPython 3.9.6. The - * predicate as a whole is deliberately stricter than `isidentifier()`, which - * does not test NFKC stability: `'field'.isidentifier()` is True and this - * returns false. + * samples with zero divergence, on Node 22.23.1 against CPython 3.9.6 — every + * sample sits inside the two versions' shared tables, and the skew characters + * below are exactly where that pair diverges. The predicate as a whole is + * deliberately stricter than `isidentifier()`, which does not test NFKC + * stability: `'field'.isidentifier()` is True and this returns false. * * Both conditions are evaluated against the ENGINE's Unicode tables, and the * two sides are versioned independently — `\p{XID_Start}`/`\p{XID_Continue}` diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index 8004a330e7..7a3a573349 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -466,6 +466,35 @@ describe('renderToolsSdkPy', () => { expect(text).not.toContain('field:') }) + it('keeps U+200C in a name tail while rejecting it at a name head, per the two XID properties', () => { + // ZWNJ carries `XID_Continue` and not `XID_Start`, so the predicate splits + // on position: bare in a tail, subscripted at a head. Both verdicts are + // stable across the supported engines — the property arrives in Unicode + // 15.1 and the floor (Node 22.19.0, Unicode 16.0) is past it. + // + // The interpreter side is where this one skews, and it is the same skew the + // docstring's four other characters record, reached in a tail position + // instead of at a head: CPython reads XID_Continue out of the + // `DerivedCoreProperties.txt` of the UCD it was built against (13.0.0 on + // 3.9.6 and 15.0.0 on 3.12.13 both lack the row, and `'a‌b'.isidentifier()` + // is False on both, measured), so the field emitted bare here needs an + // interpreter with 15.1 tables or newer. + const of = (name: string): ToolSdkSchema => ({ + name, + description: `Tool ${name}.`, + parameters: { type: 'object', additionalProperties: false, properties: { 'a‌b': { type: 'string' } } }, + output: { type: 'string' }, + }) + const text = renderToolsSdkPy([of('ping'), of('‌b')]) + expect(text).toContain('async def ping(self, args: PingArgs) -> str:') + expect(text).toContain(' a‌b: NotRequired[str]') + // A head that is XID_Continue but not XID_Start takes the subscript path, + // and `camelCase` prefixes `Tool` to make the class name start legally. + expect(text).toContain('# tools["‌b"](args: Tool‌bArgs) -> str') + expect(text).toContain('class Tool‌bArgs(TypedDict):') + expect(text).not.toContain('async def ‌b') + }) + it('subscripts a tool name that NFKC-normalizes to something else, while declaring a plain Unicode one', () => { // Same split at the tool-name site: `路径` becomes an `async def`, the // ligature name cannot, because `async def find` would define `find`. The