Streaming partials for @Extractable - #4
Merged
Merged
Conversation
Add Extract.stream that yields ExtractionUpdate.partial snapshots as the model produces JSON, then a terminal .final matching Extract.detailed. Partial is an associatedtype on Extractable (default Self) so hand-written conformances stay intact; the macro generates a nested Partial with every field optional. Half-tokens are withheld — numbers need a delimiter, strings a closing quote — trading latency for truthfulness. Chunked documents emit only .final; repair retries are not streamed; signals/tables attach to the final result only.
Replace vacuous if-let guards with exact expected snapshots so half-token regressions cannot pass by emitting nothing. Cover the realistic completed-field-then-incomplete-field shape.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last open feature on the roadmap. A caller can now show fields filling in as the model produces them instead of waiting on a spinner.
The rule this feature is built around
A partial never shows a value that a later snapshot contradicts within the same token. While the model is writing
473.00, the caller does not see47— a wrong total on screen is worse than an empty field. A scalar surfaces only once its JSON token is provably complete: closing quote for strings, a delimiter after a number, the full keyword fortrue/false/null.This trades latency for truthfulness deliberately, and
DECISIONS.mdsays so explicitly so it does not get "optimised" later.What the assembler actually emits mid-stream, now locked down by exact assertions:
{"total":473{}{"total":473.00{}{"total":473.00}{"total":473.00}{"name":"Ada","total":47{"name":"Ada"}{"lineItems":[{"sku":"A","qty":1{"lineItems":[]}{"lineItems":[{"sku":"A","qty":1}{"lineItems":[{"sku":"A","qty":1}]}Arrays grow element by element as each element completes.
Honest limits, documented rather than papered over
.final. Per-chunk snapshots are incoherent before the deterministic merge, and we have measurements showing what that merge is worth..finalonly. A partial is a UI preview, not an evidenced result..final, or throws the same errors asdetailed.Compatibility
Additive throughout.
Extractablegainsassociatedtype Partial: Decodable & Sendable = Self, so hand-written conformances keep compiling; the macro generates a realPartialfor@Extractabletypes. TheExtractionGeneratingseam gains a streaming method with a default implementation that calls the existinggenerateonce, so every existing test conformance andMockLanguageModelare untouched.Verification
swift build,swift test— 190 tests; the pre-existing 177 pass unchangedswift format lint --strict— silentA first pass of the regression tests guarded their assertions behind
if let snap { … }, which would have passed vacuously had the parser returnednil— including if it drifted toward emitting nothing at all. Those are the tests protecting the rule above, so they were rewritten to assert exact snapshots in both directions, and a missing case was added: a completed field alongside an incomplete one, which is the realistic mid-stream shape neither original test covered.