feat(core): partially-typed tools — free-value frame keeps typed keys constrained (#378) - #381
Conversation
There was a problem hiding this comment.
Code Review
This pull request resolves issue #378 by enabling constraint enforcement for partially-typed tool schemas. Instead of disqualifying a tool when it contains loosely-typed values (such as Any, open objects, or untyped arrays), these values are now compiled into a FreeValue node. Both GemmaToolArgumentConstraint and JsonToolArgumentConstraint are updated with a new FK.Free state to balance arbitrary well-formed JSON values to completion, ensuring that surrounding typed and required properties remain strictly enforced. I have no feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
… constrained (#378) A tool whose argument object contains a loosely-typed value (an Any-typed value with no `type`, an open nested object, or an untyped array) previously disqualified the WHOLE tool: the constraint dropped to nothing, so even the tool's required/typed siblings went unenforced. Now such a value compiles to a shared FreeValue node (JsonSchemaKind.Any) that both constraints accept as any single well-formed value, while the surrounding object structure — declared key names, required-once, typed siblings — stays enforced. A tool is left unconstrained only when its argument OBJECT itself is open (no declared properties). ToolSchemaCompiler: CompileNode degrades a loosely-typed value to FreeValue instead of null; TryCompileObject returns null only for an open root. Both byte-level PDAs gain an FK.Free frame: FrStart/FrBare/FrBalanced balance nested {}/[] in a single stack slot (so deep free nesting can't overflow the frame stack), FrStr/FrBalancedStr handle free strings, and a fastFree shortcut admits pure-content tokens without per-token simulation (mirrors the existing free-string-content fast path). A belt-and-suspenders EOG sweep forbids end-of-generation ids regardless of their bytes so a free value can't be truncated mid-object. MarkValueFirstBytes(Any) marks only value-START bytes and never touches the array frame's ']'/',' markings, so a typed array of free items admits string/object/scalar items (not only numbers) and can still close. Default off → byte-identical to unconstrained decoding. Allocation-free hot path (FreeValue is a shared singleton; the free frames reuse the mask/scratch buffers). NativeAOT-clean. Tests: mock-tokenizer coverage (Gemma + JSON) for free value still enforcing a typed required key, Any/untyped-array degrading to free, and typed-array-of- free-items item shapes; plus a model-gated real-vocab conformance test. GPU A/B (gemma-4-12b-it-Q4_K_M, CUDA, partially-typed get_weather = required string `location` + open-object `context`): grammar OFF (= pre-#378 for this tool): arguments → "{}" (required arg lost) grammar ON (#378): arguments → {"location":"Paris"} Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKvjcWw1gH5HyXAwsK3quP
769888c to
dec573f
Compare
What
Closes #378. Follow-up to #374 (tool-call argument grammar) and #376 (JSON-syntax constraint). Stacked on #376 (
feat/376-json-tool-grammar, PR #380) — review/merge that first.Before this change, a tool whose argument object contained any loosely-typed value — an
Any-typed value with notype, an open nested object, or an untyped array — disqualified the whole tool. The constraint dropped to nothing, so even the tool's required/typed siblings went unenforced. The model could then drop a required argument entirely.Now such a value compiles to a shared
FreeValuenode (JsonSchemaKind.Any) that both byte-level constraints accept as any single well-formed value, while the surrounding object structure — declared key names, required-once, typed siblings — stays enforced. A tool is left fully unconstrained only when its argument object itself is open (no declared properties to enforce).How
ToolSchemaCompiler—CompileNodedegrades a loosely-typed value to the sharedFreeValuesingleton instead of returning null;TryCompileObjectreturns null only for an open root object.GemmaToolArgumentConstraint,JsonToolArgumentConstraint) gain anFK.Freeframe:FrStart/FrBare/FrBalancedbalance nested{}/[]in a single stack slot (deep free nesting can't overflow the frame stack),FrStr/FrBalancedStrhandle free strings.fastFreeshortcut admits pure-content tokens without per-token simulation (mirrors the existing free-string-content fast path).MarkValueFirstBytes(Any)marks only value-start bytes and never touches the array frame's]/,markings, so a typed array of free items admits string/object/scalar items (not only numbers) and can still close.Invariants
FreeValueis a shared singleton; the free frames reuse the existing mask/scratch buffers.Tests
Mock-tokenizer coverage (Gemma + JSON) for: a free value still enforcing a typed required key;
Any/untyped-array degrading to free; typed-array-of-free-items item shapes (string/object/scalar). Plus a model-gated real-vocab conformance test. FullTests.Coregreen (215/215).GPU A/B
gemma-4-12b-it-Q4_K_M, CUDA, partially-typedget_weather= required stringlocation+ open-objectcontext, identical request:argumentsemitted{}— required arg lost{"location":"Paris"}— required arg enforced🤖 Generated with Claude Code