Skip to content

refactor(agent)!: unify PromptResponse and FinalResponse into one type - #2056

Merged
gold-silver-copper merged 3 commits into
mainfrom
refactor/unify-run-outcome-2046
Jul 9, 2026
Merged

refactor(agent)!: unify PromptResponse and FinalResponse into one type#2056
gold-silver-copper merged 3 commits into
mainfrom
refactor/unify-run-outcome-2046

Conversation

@gold-silver-copper

Copy link
Copy Markdown
Contributor

Fixes #2046option 3 (full merge into a single type), as requested.

Problem

The blocking (.prompt()) and streaming (.stream_prompt()) surfaces returned two different structs — PromptResponse and FinalResponse — 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.

data old PromptResponse old FinalResponse
final model output output response()
token usage usage aggregated_usage / usage()
message history messages history()
per-call records completion_calls completion_calls
structured final turn content

Change

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()
  • content / content() — the structured final assistant turn, now available on the blocking surface too

The sans-IO state machine (AgentRun) populates content at both Done construction sites, so blocking callers get it "for free"; the streaming driver keeps its existing finalize_streamed_choice shaping. Only the result-type boundary changed — the shared engine (AgentRun + drive_agent) was already unified.

Migration (breaking)

  • FinalResponsePromptResponse
  • .response().output()
  • .history().messages()
  • .aggregated_usage / .assistant_content() removed → .usage() / .content()
  • The streamed final-response item now serializes its fields as snake_case (matching the blocking type and the sibling CompletionCall) rather than camelCase.

The MultiTurnStreamItem::FinalResponse variant name is kept, so if let MultiTurnStreamItem::FinalResponse(res) = … call sites are unchanged — only the accessors on res move to the unified vocabulary.

Verification

  • cargo build --workspace --all-features --tests --examples ✅ (exit 0)
  • cargo clippy --workspace --all-features --all-targets0 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 streaming output()/messages()/content() path end-to-end)
  • cargo test -p rig-core --all-features --doc — 110 passed ✅
  • RUSTDOCFLAGS="-D warnings" cargo doc

Note: providers::gemini::image_generation::tests::image_generation_2xx_error_envelope_preserves_status_and_body fails on main today and is untouched by this PR (no image_generation.rs change) — pre-existing, out of scope.

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.
@gold-silver-copper
gold-silver-copper added this pull request to the merge queue Jul 9, 2026
Merged via the queue into main with commit a26409c Jul 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(agent): unify PromptResponse and FinalResponse result vocabularies

1 participant