refactor(agent)!: unify PromptResponse and FinalResponse into one type - #2056
Merged
Conversation
Fixes #2046 (option 3, full merge). The blocking and streaming agent surfaces returned two structs carrying the same run result under different names (`PromptResponse` vs `FinalResponse`). Switching `.prompt()` <-> `.stream_prompt()` forced a mechanical accessor rename for no semantic reason. `FinalResponse` is removed. The terminal `MultiTurnStreamItem::FinalResponse` item now carries the unified `PromptResponse`, which both surfaces return. One vocabulary works on either side — `output`/`output()`, `usage`/`usage()`, `messages`/`messages()`, `completion_calls`/`completion_calls()`, and `content`/`content()` — and blocking callers gain the structured final-turn `content` (populated by the sans-IO state machine at both Done sites). Migration: `FinalResponse` -> `PromptResponse`, `.response()` -> `.output()`, `.history()` -> `.messages()`; `.aggregated_usage`/`assistant_content()` are gone (use `.usage()`/`.content()`). The streamed final-response item now serializes snake_case (matching the blocking type and `CompletionCall`).
… data
Responses serialized before `content` existed deserialized to `Text("")`
even when `output` was non-empty, making `output()` and `content()`
inconsistent and breaking the structured-final-turn contract for legacy
data. Deserialize through a shadow struct where `content` is optional and,
when absent, reconstruct it from `output` as a single text part.
Serialization shape is unchanged; adds regression tests covering legacy
JSON without `content` (non-empty and empty output) and that an explicit
`content` survives a round-trip without being clobbered by the fallback.
The previous fix routed only deserialize through `PromptResponseRepr`, so serialize wrote `content` as a bare `OneOrMany` while deserialize expected an `Option<OneOrMany>`. Self-describing formats (JSON) hide the mismatch, but positional / non-self-describing formats would misalign, regressing round-trip that worked before the FinalResponse merge. Route serialize through the shadow too (`into = "PromptResponseRepr"`) so both directions agree on `content`'s wire shape. The shadow carries the field serde attributes, keeping the serialized (JSON) shape byte-identical. Adds a test asserting serialize and deserialize share the shadow shape.
This was referenced Jul 9, 2026
Merged
Closed
Closed
Closed
Merged
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.
Fixes #2046 — option 3 (full merge into a single type), as requested.
Problem
The blocking (
.prompt()) and streaming (.stream_prompt()) surfaces returned two different structs —PromptResponseandFinalResponse— carrying the same run result under different accessor names. Switching a call site between surfaces forced a mechanical rename of every accessor for no semantic reason.PromptResponseFinalResponseoutputresponse()usageaggregated_usage/usage()messageshistory()completion_callscompletion_callscontentChange
FinalResponseis removed. The terminalMultiTurnStreamItem::FinalResponseitem now carries the unifiedPromptResponse, which both surfaces return. One vocabulary works on either side:output/output()usage/usage()messages/messages()completion_calls/completion_calls()content/content()— the structured final assistant turn, now available on the blocking surface tooThe sans-IO state machine (
AgentRun) populatescontentat bothDoneconstruction sites, so blocking callers get it "for free"; the streaming driver keeps its existingfinalize_streamed_choiceshaping. Only the result-type boundary changed — the shared engine (AgentRun+drive_agent) was already unified.Migration (breaking)
FinalResponse→PromptResponse.response()→.output().history()→.messages().aggregated_usage/.assistant_content()removed →.usage()/.content()snake_case(matching the blocking type and the siblingCompletionCall) rather thancamelCase.The
MultiTurnStreamItem::FinalResponsevariant name is kept, soif let MultiTurnStreamItem::FinalResponse(res) = …call sites are unchanged — only the accessors onresmove to the unified vocabulary.Verification
cargo build --workspace --all-features --tests --examples✅ (exit 0)cargo clippy --workspace --all-features --all-targets— 0 warnings ✅ (CI runs-D warnings)cargo test -p rig-core --all-features --lib— 1246 passed (see note) ✅cargo test -p rig --test core— 17 passed ✅cargo test -p rig --test anthropic/--test gemini(cassette replay) — 96 / 143 passed ✅ (exercises the streamingoutput()/messages()/content()path end-to-end)cargo test -p rig-core --all-features --doc— 110 passed ✅RUSTDOCFLAGS="-D warnings" cargo doc✅