test(ecs): the ECS contract on five more wires — OpenAI Chat, OpenAI Responses, Gemini, DeepSeek, Doubleword, Venice - #2501
Merged
Conversation
…, the Responses wire replays a turn's text parts as one message item Found by the ECS contract matrix: gpt-5-mini on the Chat wire answered 400 to an output tool degraded to native output under `tool_choice: none` (no tool advertised, a choice sent), and on the Responses wire to a reprompted turn the model had answered in two output_text parts (two message items under one id, "Duplicate item found"). Each has a unit test beside it; the matrix cells are the cassette regressions. The Moonshot tool-choice tests advertise the tool their choice names.
…nswer that already is the structured output Two parity divergences the matrix found between rig-ecs's materialise and rig-agent on the same bytes: Gemini 3's thought signature rides the last chunk, so the world committed [text, reasoning] where the agent commits [reasoning, text] (rig-agent's ordered_assistant_content is hoisted to rig_core::message with canonical_streamed_choice; the fold orders a streamed turn canonically when it lands); and a JSON text answer where the output tool was due was reprompted by the world and accepted by the agent (policy::text_satisfies_schema; CONTRACT §4 says so).
…rpus as their shape tests/common/ecs_matrix: the cells as data over the rig-verify corpus's Program table (included by path, as the Anthropic causal cells include its nesting systems), a rig-agent producer driver, a world driver (the corpus's world_hooks as systems, real adapters, the record/graph/cut/despawn assertions) and the world-only rows (batch holds, error facts, minted ids, despawn on an in-flight stream). Program is Copy, the world interpreter's pieces are pub, Nesting learns no_temperature for a wire whose model takes only its default; the parity helper dumps a diverging pair under RIG_ECS_PARITY_DUMP; world_replay expects the grown corpus.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
…olicy hash (#2500) #2500 (724dce2) added `provider_retries` to the effective policy `rig_ecs::replay::spec_json` hashes into a run's program identity. #2501 was based and CI-checked before it, so its 491 `ecs_parity` goldens carry the hash without the field and every `ecs_matrix*` world cell diverges on `main` at 258377b (a clean checkout fails them; the ledger names five). Regenerated: the `policy` line is the only change in every file.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
…ers hoisted `tests/common/ecs_matrix/faults.rs`: every way a run ends badly as a `Cell` over the same `Program` table plus the `Fault` it drives and the `Scene` it saves. The world driver parks a tool or a stream at a cut the happy paths have no hook for, asserts the failure's facts beside the record, saves and loads (or is refused) where CONTRACT §13 says; the producer runs to a failing ending; `run_scripted` runs both over the same scripted frames and compares them by the golden comparison's own normalisation (`ecs_goldens::assert_parity`); a cell over another cell's recording compares to that cell's golden (`compare_to_original`). The #2490 stream-fault helpers become `SseShape` in `tests/common/stream_faults.rs` (each wire family's terminal, deltas and in-band error frame; CRLF-delimited recordings read like LF ones) and the OpenAI and Gemini cells call them. Row 11's cancel-at-cut driver (`extra::cancel_at`) generalises the #2501 first-delta cell.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's #2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's #2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's #2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's #2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's #2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons.
pull Bot
pushed a commit
to appelgriebsch/rig
that referenced
this pull request
Sep 13, 2026
…etryable (0xPlaygrounds#2502) * fix(core): restore observe::scrub_diagnostic and diagnostic_url_secrets for hosts that persist diagnostics 0xPlaygrounds#2499 removed both as having no callers. The caller is rigcoder, the out-of-tree consumer of rig-ecs: its failure records and checkpoint artifacts scrub their own diagnostics with the adapter's rules, and the scrub module is private, so nothing else reaches it. Restored as thin wrappers with a test. * fix(core): a stream cut before its terminal record is retryable A truncation is a transport fault, not a provider verdict: the same request is served whole on the next call, as a reset connection is. The report from serve::stream_truncated is now retryable, so a run with budget re-issues the completion (rig-ecs CONTRACT §5) instead of ending Failed(Provider) on a dropped connection. Pinned by a rig-ecs test whose first stream closes without a terminal record and whose retry answers. Found moving rigcoder's pin: its session used to retry truncations by matching the message text; with the run-level retry the classification has to be typed. * test(gemini): the truncation failure test declines the provider-retry budget A truncation is retryable now, so under the default budget the run re-issues the completion; this test pins the failure's classification and the kept prefix, and rig-ecs pins the retry. * test(openai): the two truncation failure tests decline the provider-retry budget Same as the gemini one: a truncation is retryable now, so under the default budget the run re-issues the completion; these pin the failure and the kept prefix, and rig-ecs pins the retry. * test(rig-verify): regenerate the five-wire goldens for the provider-retry policy identity 0xPlaygrounds#2501 added the OpenAI Chat, OpenAI Responses, Gemini, DeepSeek, Doubleword and Venice ECS goldens with a policy hash from before 0xPlaygrounds#2500 put ProviderRetries into the identity, so main's own stable/test fails on them. Regenerated in replay mode: 491 files, only the hash in each.
pull Bot
pushed a commit
to appelgriebsch/rig
that referenced
this pull request
Sep 13, 2026
…ble statuses, truncated and error-bearing streams, refusals, failing tools, stops, scenes (0xPlaygrounds#2503) * fix(cassette): placeholder OAuth token fields at record time `access_token`, `id_token` and `refresh_token` join the sensitive string keys the scrubber placeholders on record, beside the generated ids and the server-tool locators. No committed cassette records a token exchange (the ChatGPT and Copilot recordings hold completion paths only); the keys are in the table so one never can. Found by the pydantic-ai harness comparison; the other three practices it named (unplayed interactions fail the test, record-time header scrubbing, path normalisation) were already enforced or moot, see the ledger. * fix(core): the error report's code is the body's own `ErrorReport.code` was the transport's machine code alone (a gRPC status name, an AWS exception type), so every HTTP wire's 4xx reported `None` while the body named `model_not_found`, `NOT_FOUND` or `authentication_error`. `ProviderResponseError::machine_code` is the transport's code when it gave one apart from the body, else the string the body names under `error.code`, `error.status` or `error.type` (`provider_response::body_code`; a numeric `code` is the status said again and is skipped; a prose envelope names none). Every report conversion that carries a provider reply (completion, embedding, rerank) reads it. CONTRACT §5 gains the row. Goldens regenerated: the four `anthropic_outcome_model_error{,_streamed}` (root and ecs_parity), `code: null` → `"authentication_error"`. The six `ErrorProbe` cells pin each wire's code (Venice's envelope is prose: `None`). * test(ecs): the 0xPlaygrounds#2501 native goldens carry the retry budget in their policy hash (0xPlaygrounds#2500) 0xPlaygrounds#2500 (724dce2) added `provider_retries` to the effective policy `rig_ecs::replay::spec_json` hashes into a run's program identity. 0xPlaygrounds#2501 was based and CI-checked before it, so its 491 `ecs_parity` goldens carry the hash without the field and every `ecs_matrix*` world cell diverges on `main` at 258377b (a clean checkout fails them; the ledger names five). Regenerated: the `policy` line is the only change in every file. * fix(ecs): an answerless turn the provider cut short fails the run as rig-agent does A turn with no text, no call and no image that stopped with a truncating finish reason (`Length`, `ContentFilter`) settled the world's run with an empty answer; rig-agent fails it (rig#2322). `turn_delivered_no_answer` moves from rig-agent's transcript to `rig_core::completion::message` (rig-agent imports it there), `FinishReason::no_answer_message` carries the one wording, and `Materialise` fails the run `Provider(report)` with `kind: response` before reading such a turn. CONTRACT §4 gains the row; every `ecs_faults*::filtered_empty` cell is the regression. The four DeepSeek `ecs_truncation` cells accept the failure their runner twins already accept (an error that is not the provider's reply). * test(ecs): the failure rows' cells and drivers, the stream-fault helpers hoisted `tests/common/ecs_matrix/faults.rs`: every way a run ends badly as a `Cell` over the same `Program` table plus the `Fault` it drives and the `Scene` it saves. The world driver parks a tool or a stream at a cut the happy paths have no hook for, asserts the failure's facts beside the record, saves and loads (or is refused) where CONTRACT §13 says; the producer runs to a failing ending; `run_scripted` runs both over the same scripted frames and compares them by the golden comparison's own normalisation (`ecs_goldens::assert_parity`); a cell over another cell's recording compares to that cell's golden (`compare_to_original`). The 0xPlaygrounds#2490 stream-fault helpers become `SseShape` in `tests/common/stream_faults.rs` (each wire family's terminal, deltas and in-band error frame; CRLF-delimited recordings read like LF ones) and the OpenAI and Gemini cells call them. Row 11's cancel-at-cut driver (`extra::cancel_at`) generalises the 0xPlaygrounds#2501 first-delta cell. * test(ecs): the failure rows on the openai wire The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's 0xPlaygrounds#2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons. * test(ecs): the failure rows on the gemini wire The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's 0xPlaygrounds#2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons. * test(ecs): the failure rows on the deepseek wire The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's 0xPlaygrounds#2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons. * test(ecs): the failure rows on the doubleword wire The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's 0xPlaygrounds#2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons. * test(ecs): the failure rows on the venice wire The producers (corpus_faults*.rs: four recordings, the setup 4xx streamed reuse) and the world cells (ecs_faults*.rs: recorded, reused, scripted from this wire's 0xPlaygrounds#2501 recordings, world-only), with the frames' provenance, the wire's models and its #[ignore] reasons. * test(ecs): catalog the failure rows' scenarios 36 recorded producers mapped to their world cells; the scripted and world-only cells are native-only, as the 0xPlaygrounds#2490 cells are; the files inventory gains the sources and goldens. The world-replay corpus count follows the 36 new producer goldens.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
Every native golden carries the hash of the agent's effective policy (`rig_ecs::replay::spec_json`); a field added there changes every native golden at once. #2500 added `provider_retries`, #2501 was based before it and merged green on its own base, and main failed 491 world cells until #2503 regenerated them. The parity README says what a PR that touches `spec_json` does, what a PR based before one does, and why no static check can recompute the hash from a header.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
Every native golden carries the hash of the agent's effective policy (`rig_ecs::replay::spec_json`); a field added there changes every native golden at once. #2500 added `provider_retries`, #2501 was based before it and merged green on its own base, and main failed 491 world cells until #2503 regenerated them. The parity README says what a PR that touches `spec_json` does, what a PR based before one does, and why no static check can recompute the hash from a header.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
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.
gold-silver-copper
added a commit
that referenced
this pull request
Sep 13, 2026
…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.
yermakoffivan
pushed a commit
to yermakoffivan/rig
that referenced
this pull request
Sep 13, 2026
…ens as a named lane (0xPlaygrounds#2508) * ci: run the slow lanes wherever the plan job does not `test-all-features` and `dependency-floors` depend on `plan`, which runs on pull requests only. Their `if` had no status function, so the implicit `success()` failed on a skipped `plan` and both lanes were skipped on the merge queue, on every push to main (cd.yaml's run-slow) and on the nightly schedule: every recorded non-PR run shows them skipped. `!cancelled()` ahead of the explicit result check lets the intended condition decide. * ci: the ECS parity lane `cargo xtask verify --check ecs-parity`: every producer and world cell of the agent/ECS matrices (`corpus_*` / `ecs_*` on the root provider targets under `--features bedrock`), the golden pairing guard, and rig-verify's replay of every golden through a world; a `stable / ecs parity` CI job; selected by every --pr plan and by a fixture change in --changed. A subset of `test` by construction, named so the merge queue can require it and a failure reads as what it is: the runtimes disagree, or a golden is stale. * test(ecs): name the policy-hash trap Every native golden carries the hash of the agent's effective policy (`rig_ecs::replay::spec_json`); a field added there changes every native golden at once. 0xPlaygrounds#2500 added `provider_retries`, 0xPlaygrounds#2501 was based before it and merged green on its own base, and main failed 491 world cells until 0xPlaygrounds#2503 regenerated them. The parity README says what a PR that touches `spec_json` does, what a PR based before one does, and why no static check can recompute the hash from a header. * test(ecs): the despawn cell compares up to the first text delta `despawning_the_stream_at_the_first_delta_records_a_cancel` compared the witnessed and plain runs' whole logs. The despawn lands on the tick that collected the first text delta, and how many deltas that tick collected is scheduling: on a loaded runner the plain run kept one delta more than the witnessed one and the parity lane failed on 0xPlaygrounds#2509 with no divergence. The two records are compared up to the first text delta, which is the fact the cell pins; what a cancelled record kept after it is not one.
yermakoffivan
pushed a commit
to yermakoffivan/rig
that referenced
this pull request
Sep 13, 2026
…is a refusal, a truncated reasoning-only turn commits nothing, images ride the regrouped stream (0xPlaygrounds#2509) (0xPlaygrounds#2510) * 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 0xPlaygrounds#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 0xPlaygrounds#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.
Summary
The ECS contract (
crates/rig-ecs/CONTRACT.md) was pinned on real provider bytes almost only for Anthropic. This lands the same cell families on five more wires — OpenAI Chat, OpenAI Responses, Gemini REST, DeepSeek, Doubleword, Venice — as one rectangular matrix: rows are contract sections, columns are wires, and every cell drives a committed recording throughspawn_runand asserts the world's record equals rig-agent's on the same bytes, then the graph, then (where the section names a cut) a scene saved at the cut and resumed in a fresh world over replayers of the log's tail, thendespawn_run.One shape, six copies. The cells are data (
tests/common/ecs_matrix/cells.rs: the rig-verify corpus'sProgramtable plus the tools, store and bus a live cell needs) over two drivers intests/common/ecs_matrix/:agent.rs(the rig-agent producer, writing the golden) andworld.rs(the world cell: the corpus'sworld_hooksas systems, real adapters, the four assertions). The rig-verify corpus module is included by#[path], as the Anthropic causal cells already did. The per-wire files hold only scenario literals (the cassette census reads them off the call sites), the wire's models and the wire's#[ignore]reasons.The grid as landed
85 golden-backed cells per wire (rows 1–8, 10, 12) plus the world-only rows (11: batch holds by both approval routes; 13: a recorded 4xx through the world with the witness's facts; 14: Gemini's minted ids; 15:
despawn_runon a run cancelled with its stream in flight, and on every settled cell) and row 9 (the termination matrix, ported to DeepSeek, Doubleword and Venice; OpenAI and Gemini already had it).openai_chatgpt-5-mini, routegpt-5-nanoopenai_responsesgpt-5-mini/ reusegpt-4omax_output_tokensfloor, 3 causal (leaf: true)geminigemini-3-flash-preview/ reusegemini-2.5-flashMALFORMED_FUNCTION_CALL)deepseekdeepseek-chat, routedeepseek-reasonerdoublewordQwen/Qwen3.5-397B-A17B-FP8,reasoning_effort: nonevenicemistral-small-3-2-24b-instruct(the route is the same model)Every recording is a short prompt; the whole matrix recorded for well under a dollar per provider. The cassette engine refuses a session that leaves an interaction unplayed, so every distinct consumption pattern is its own recording, as the Anthropic families' are.
Parity defects found and fixed (each with its cell as the regression)
tool_choicebeside an emptytools(400). Dropped when no tool is advertised; unit testtool_choice_is_dropped_when_no_tool_is_advertised.assistant_turn_with_several_text_parts_replays_as_one_message_item.ordered_assistant_contenthoisted torig_core::message(rig-agent re-exports it),canonical_streamed_choicebeside it, and rig-ecs'sfoldorders a streamed turn canonically when it lands.rig_ecs::policy::text_satisfies_schema; CONTRACT §4's reprompt row now says so.Open findings (ledger):
ErrorReport.codeisNoneon every HTTP wire's 4xx (the body's own code stays onprovider_response.body); rig-agent's regrouped streamed branch drops image parts; Venice reportsfinish_reason: stopon a tool-call turn and rig-agent'sModelTurnFinishedreportsStop, notToolCalls(the two termination tool-turn cells are ignored with that reason).Changelog
despawn_run), and the termination matrix on DeepSeek, Doubleword and Venice; 502 new recordings and 969 new goldens (478 rig-agent, 491 world).tool_choicewhen no tool is advertised.rig_core::message::{ordered_assistant_content, canonical_streamed_choice}.Migration
None.
rig_agent::run::streamed::ordered_assistant_contentis now a re-export ofrig_core::message::ordered_assistant_content.