Skip to content

JsonSchemaOutputConstraint: enforce declared property order (blocks streaming an early field before a later one) #425

Description

@pekkah

Summary

JsonSchemaOutputConstraint (whole-body mode via JsonToolArgumentConstraint.ForWholeBody) enforces which keys appear and that required keys appear, but not the order they appear in. For a streaming consumer that wants to act on an early field before a later, larger field finishes, unordered keys defeat the latency win.

Where

src/SharpInference.Core/Grammar/JsonToolArgumentConstraint.cs, StepObject / OExpectKeyOrClose (~L622-640): when a key opens, the candidate set is every not-yet-emitted key, so any declared key is legal in any position. CompiledObject already preserves declaration order (ToolSchemaCompiler), so the information is present — it's just not enforced.

Motivating use case

An assistant reply schema:

{ "type": "object",
  "properties": { "say": {"type":"string"}, "show": {"type":"string"} },
  "required": ["say"] }

say is a short spoken line streamed to TTS; show is a large markdown block for the screen. To start speaking as early as possible we want say emitted first. Today the model may emit show first (potentially thousands of tokens), so speech can't start until the whole show value has streamed — the constraint permits it.

Ask

An opt-in ordered mode for JsonSchemaOutputConstraint (e.g. a ctor flag or a ToolSchemaObject.OrderedProperties marker) where declared properties must appear in declaration order (optional properties skippable, but never reordered). Ordered decoding is also generally cheaper (the key candidate set is a single expected key, not a bitmask).

Related limitations (same feature, lower priority — happy to split out)

  1. No minLength / non-empty for required strings. A required string may still be emitted as "". For the say field above that means a structurally-valid but silent reply. A minimal "required string must be non-empty" facet would make the guarantee real.
  2. Structured output is mutually exclusive with tool-calling in a single pass. Whole-body mode forces { as the first token, masking a tool-call opener, so a pass is either "may call a tool" or "must emit schema JSON," never "tool-call OR final structured answer." A top-level toolcall | schema alternation would let a single agentic pass either act or answer-in-schema. (Workaround today: run tool passes unconstrained, then a final constrained answer pass — works, but costs an extra pass.)

Context

Consumer is Ayu — a local voice assistant using SharpInference. It wants a {say, show} structured reply so the spoken and on-screen channels are model-controlled and structurally guaranteed (no more fragile <say>/<show> tag parsing). Ordering is the one thing blocking a clean low-latency streaming implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions