fix: the parity gaps the ECS matrices found, closed — Gemini's block is a refusal, a truncated reasoning-only turn commits nothing, images ride the regrouped stream - #2510
Merged
Conversation
…is a refusal, a truncated reasoning-only turn commits nothing, images ride the regrouped stream (#2509) * fix(gemini): a blocked prompt is a refusal on the report Gemini's SAFETY / BLOCKLIST / PROHIBITED_CONTENT block was CompletionError::ProviderError(message): kind provider, refusal false, no code. ProviderResponseError gains refusal (with_refusal; on the wire form, default false), every ErrorReport conversion that carries a reply reads it, and the block is now the provider's reply without a status, code the block reason, refusal true. A refusal is never retryable, whatever status or transport verdict travels with it: the provider judged the content, and the same call gets the same verdict. OTHER and unknown reasons stay transient and are not refusals. A refusal the model says (OpenAI's refusal deltas) is an answer, as before. CONTRACT §4 gains the row, naming the REST wire: the gRPC Gemini wire does not read prompt_feedback yet. The Gemini refusal cell and the stream-fault blocked cells pin it. * fix(agent): a truncated reasoning-only turn commits nothing rig-agent pushed a reasoning-only turn into the run's new messages before the turn_delivered_no_answer check failed the run; rig-ecs keeps nothing (CONTRACT §4). The check now runs first, so the failed run commits nothing on either runtime. a_truncated_reasoning_only_turn_commits_nothing pins the run state; the CONTRACT row loses its parenthesis. The #2322 streaming test pinned the old ordering in its name and doc (reasoning_survives_into_history_when_the_truncated_turn_errors): its assertion was always about the streamed reasoning reaching the consumer, which is the surface that keeps a truncated thinking turn debuggable, so it is renamed partial_reasoning_reaches_the_consumer_when_the_truncated_ turn_errors and its doc says what it pins. * fix(agent): the streamed assembler carries image parts A regrouped streamed turn (a call or reasoning present) rebuilt the turn from the calls, the reasoning and the text; an image the provider delivered beside a call was dropped. The trailing items are now the calls in arrival order, then the images in theirs; rig-core's canonical_streamed_choice groups the same way, and its doc no longer describes a divergence. finish_carries_images_after_the_calls_when_regrouping asserts the assembler equals rig-core's rule. * test(agent): the model-turn hook reads the reconciled finish reason A wire that reports stop on a turn that carried a tool call reaches the hook as ToolCalls, the value the response and the record carry (FinishReason::reconcile_with_output at the compat normaliser and in the streaming accumulator). Pinned with a mock; no runtime change. The two Venice termination tool-turn cells stay ignored for the reason that is true: their cassettes were removed as orphans in #2501 and this task records none; the attribute names the scenarios to record, after which the cells pass as written. * docs(ecs): a typed tool error is a status: error result Not a gap: ErasedTool::execute renders every tool's Err — a typed Tool::Error or a ToolExecutionError — into the status: error result before the adapter answers; the contract's "any other Err report" row is about handler failures. CONTRACT §8.1 says so in one sentence.
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.
Re-targets #2509 at
main: that PR merged intoci/main-gate, so its change never reachedmain. This is its squash (0b7b215) cherry-picked ontomainwith no conflicts; the description below is #2509's.Summary
The matrices of #2489–#2503 pinned five divergences "as they are" and ledgered them. This closes them in the runtime, one commit each, with the existing cell (or a new unit test where no wire can show it) as the regression. Stacked on #2508.
SAFETY/BLOCKLIST/PROHIBITED_CONTENTblock was a plain provider error (kind: provider,refusal: false, no code).ProviderResponseErrorgainsrefusal(on the wire form, default false, read by everyErrorReportconversion that carries a reply); the block is now the provider's reply without a status,codethe block reason,refusal: true. A refusal is never retryable, whatever status or transport verdict travels with it (is_retryableshort-circuits; pinned).OTHERand unknown reasons stay transient and are not refusals. The row names the REST wire:rig-gemini-grpcdoes not readprompt_feedbackyet, so a block on the gRPC wire is neither an error nor a refusal — deferred. A refusal the model says (OpenAI'srefusaldeltas) is an answer, as before. CONTRACT §4 gains the row;gemini::cassette::ecs_faults::refusaland the Gemini stream-fault blocked cells pin it.turn_delivered_no_answercheck failed the run; rig-ecs keeps nothing. The check now runs first on both runtimes (CONTRACT §4);a_truncated_reasoning_only_turn_commits_nothingpins the run state. The bug: Gemini injects a hardcoded maxOutputTokens=4096 and temperature=1.0 into structured-output and image requests #2322 streaming test that pinned the old ordering in its name and doc asserted only that the streamed reasoning reaches the consumer, which is what keeps a truncated thinking turn debuggable; it is renamedpartial_reasoning_reaches_the_consumer_when_the_truncated_turn_errors.canonical_streamed_choiceto its own inputs (its reasoning, the text items it reports, the calls it accepted, the images the provider delivered), so the two runtimes share one grouping rule: reasoning, text, the calls, then the images, each in arrival order.stopbeside a call isToolCalls); a unit test pins it. The two Venice termination tool-turn cells stay ignored for what is now the real reason: their cassettes were removed as orphans in test(ecs): the ECS contract on five more wires — OpenAI Chat, OpenAI Responses, Gemini, DeepSeek, Doubleword, Venice #2501 and this task records none; the attribute names the two scenarios to record, after which the cells pass as written (every test(ecs): the ECS contract on five more wires — OpenAI Chat, OpenAI Responses, Gemini, DeepSeek, Doubleword, Venice #2501/test(ecs): the ECS contract under faults on six wires — setup, retryable statuses, truncated and error-bearing streams, refusals, failing tools, stops, scenes #2503 Venice tool turn reportstool_calls).status: errorresult — not a gap.ErasedTool::executerenders every tool'sErrinto the result before the adapter answers; CONTRACT §8.1 says so in one sentence (an errormap_errormarks a refusal isstatus: refusedinstead).No golden changed.
Testing
cargo xtask verify --pr --base origin/mainon this head: fmt, source-guards, tooling, clippy and default-check pass; default-tests ran 2530 tests with 2529 passed and one failure,llamacpp::cassette::loaders::loaders_smoke, which fails in this/private/tmpclone regardless of branch (the scrubber redacts only home-directory paths) and passes in CI. dependency-floors is outside the PR selection.ci/main-gatehead that fix: the parity gaps the ECS matrices found, closed — Gemini's block is a refusal, a truncated reasoning-only turn commits nothing, images ride the regrouped stream #2509 merged into (git diffis empty), so fix: the parity gaps the ECS matrices found, closed — Gemini's block is a refusal, a truncated reasoning-only turn commits nothing, images ride the regrouped stream #2509's own verification applies as written.Changelog
ProviderResponseError::refusal,ErrorReport::refusalfrom a provider's reply, the block reason as the code.rig_core::completion::message::canonical_streamed_choice.status: errorresult.Migration
ErrorReport::refusalis nowtruefor a provider's refusal of the prompt (Gemini's content blocks), which arrive askind: provider_responsewith the block reason ascodeinstead ofkind: provider; a caller matching Gemini's block onErrorKind::ProvidermatchesErrorKind::ProviderResponsewithrefusalinstead.ProviderResponseErrorgains arefusalfield (set throughwith_refusal);is_retryableisfalsefor a refusal regardless of status.