Skip to content

feat(core): partially-typed tools — free-value frame keeps typed keys constrained (#378) - #381

Merged
pekkah merged 1 commit into
masterfrom
feat/378-partial-typed-tools
Jun 24, 2026
Merged

feat(core): partially-typed tools — free-value frame keeps typed keys constrained (#378)#381
pekkah merged 1 commit into
masterfrom
feat/378-partial-typed-tools

Conversation

@pekkah

@pekkah pekkah commented Jun 24, 2026

Copy link
Copy Markdown
Owner

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 no type, 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 FreeValue node (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

  • ToolSchemaCompilerCompileNode degrades a loosely-typed value to the shared FreeValue singleton instead of returning null; TryCompileObject returns null only for an open root object.
  • Both PDAs (GemmaToolArgumentConstraint, JsonToolArgumentConstraint) gain an FK.Free frame:
    • FrStart/FrBare/FrBalanced balance nested {}/[] in a single stack slot (deep free nesting can't overflow the frame stack), FrStr/FrBalancedStr handle free strings.
    • 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 truncate the call 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.

Invariants

  • Default off → byte-identical to unconstrained decoding (constraints only built when the opt-in is on).
  • Allocation-free hot pathFreeValue is a shared singleton; the free frames reuse the existing mask/scratch buffers.
  • NativeAOT-clean, TreatWarningsAsErrors satisfied (0 warnings), InvariantGlobalization-safe.

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. Full Tests.Core green (215/215).

GPU A/B

gemma-4-12b-it-Q4_K_M, CUDA, partially-typed get_weather = required string location + open-object context, identical request:

arguments emitted
grammar OFF (= pre-#378 for this tool: constraint dropped) {} — required arg lost
grammar ON (#378) {"location":"Paris"} — required arg enforced

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pekkah
pekkah changed the base branch from feat/376-json-tool-grammar to master June 24, 2026 08:07
… 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
@pekkah
pekkah force-pushed the feat/378-partial-typed-tools branch from 769888c to dec573f Compare June 24, 2026 08:08
@pekkah
pekkah merged commit c59ca83 into master Jun 24, 2026
1 check passed
@pekkah
pekkah deleted the feat/378-partial-typed-tools branch June 24, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool-arg grammar: constrain partially-typed tools (Any/open-object values) (#374 follow-up)

1 participant