Skip to content

fix(openai): preserve stateless Responses output items - #3419

Open
strandly-the-agent wants to merge 7 commits into
strands-agents:mainfrom
strandly-the-agent:agent-tasks/3389
Open

strandly-the-agent wants to merge 7 commits into
strands-agents:mainfrom
strandly-the-agent:agent-tasks/3389

Conversation

@strandly-the-agent

@strandly-the-agent strandly-the-agent commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Description

Preserve OpenAI Responses output items across stateless turns in both SDKs.

  • relies on stateless Responses returning encrypted_content on reasoning items by default (the legacy reasoning.encrypted_content include is no longer injected; user-configured include passes through)
  • captures complete ordered response.output_item.done / terminal output items
  • persists provider response fields with assistant messages and session serialization (additionalModelResponseFields.openaiResponsesOutput)
  • validates items as replayable Responses input before replaying them verbatim; only persists them when they cover every emitted tool call
  • keeps the existing string-form fallback for legacy history and stateful behavior; on Bedrock Mantle, message items are sent in that string form (Mantle rejects them verbatim) while reasoning/function_call items still replay
  • max-token recovery keeps only reasoning items so incomplete tool calls are never replayed

Note: stream_messages now forwards additionalModelResponseFields for every provider (namespaced per provider), so Bedrock users with additionalModelResponseFieldPaths will also see it persisted on assistant messages and in sessions.

Related Issues

Fixes #3389
Related to #3388 and #3399

Documentation PR

No documentation change needed; this restores adapter round-trip behavior.

Type of Change

Bug fix

Testing

Rebased onto main 7939e67 on 2026-10-16 (squashed; conflicts in openai_responses.py, streaming.py and two Python test files resolved — main's #5074 content_filter and #4139 cut-off-tool-call handling are kept as-is). Follow-ups: 34c422e30 / ff939261f (re-review fixes), 6177fad9f (codecov gaps, session round-trip tests, complexity), d099713ec / c2b8746c3 (Mantle replay fix found by the live run), 5b5a6d208 (stop injecting the include, per lizradway). Rebased again onto main 74b6790 on 2026-10-21 (one conflict: main's new _log_skipped_blocks call in _format_request_messages, kept alongside this PR's removal of the trailing empty-content filter) and onto main baf15f5 on 2026-10-28 (one conflict: main hoisted const stateful in TS _streamResponses; kept it and passed the Mantle replay option). Gates on the current head 2cbeabead:

TypeScript

  • eslint, prettier --check, tsc --noEmit (src) — pass
  • vitest unit-node: 5666 passed, 6 skipped, 2 failed in src/telemetry/__tests__/config.test.ts — the same 2 fail identically on clean origin/main in this sandbox (unrelated)
  • tsc over test/integ reports 539 errors — identical count on clean origin/main (pre-existing / sandbox env)

Python

  • ruff format --check, ruff check — pass
  • mypy ./src ./tests_typing — pass, 366 files (run with --cache-dir /tmp --no-sqlite-cache; the NFS workspace locks mypy's sqlite cache)
  • hatch test -py 3.13 full unit suite — 8147 passed, 38 skipped

Live Bedrock Mantle (tests_integ/models/test_model_mantle.py, us-east-1, from the sandbox's Bedrock role)

  • On 6177fad9f: 4 passed, 1 failed — test_reasoning_content_multi_turn (passes on clean main, so the PR regressed it). Wire capture showed the adapter sent a complete message output item (id, status, annotations), but Mantle's streaming path strips id/status before its internal validation and rejects the request (SubmitRequestFailure, 219 pydantic errors, all on the message item). reasoning and function_call items replay fine; the string-form assistant message is accepted. openai.gpt-5.4 on Mantle accepts every shape; only openai.gpt-oss-120b (the /v1 path) fails. See mantle-replay-shapes.log.
  • Fix (d099713ec): when bedrock_mantle_config / bedrockMantleConfig is set, message items are dropped from the replay and the existing string-form text fallback is used; reasoning and function_call items still replay. Non-Mantle behaviour is unchanged.
  • On c2b8746c3: 5 passed; two-turn tool-use probe passes on openai.gpt-oss-120b and openai.gpt-5.4 (mantle-integ-fixed.log, mantle-integ-final.log).
  • include: ["reasoning.encrypted_content"] was accepted by both Mantle models but is a no-op there: identical reasoning items with or without it (mantle-include.log). OpenAI's reasoning guide now states stateless mode returns encrypted_content by default and the include is "legacy … not required", so 5b5a6d208 removes the injection; Mantle suite still 5/5 without it (mantle-integ-no-include.log), and 5/5 again on each rebased head (round9-rebase-gates.log, round10-rebase-gates.log).

Coverage (codecov patch gaps, 6177fad9f) — 9 new tests: configured include already containing reasoning.encrypted_content, replay with no text fallback, foreign additionalModelResponseFields (no warning), non-dict items, SDK-model → dict conversion + unsupported type, stateful mode skipping capture, recovery with non-list / no-reasoning / foreign fields (3 params), plus one session round-trip test per SDK (test_additional_model_response_fields_round_trip through SnapshotSessionManager; TS takeSnapshot → JSON → loadSnapshot). Local branch coverage: _recover_message_on_max_tokens_reached.py 100%; openai_responses.py 97% — the remaining partials are pre-existing _stream event-loop back-edges, not lines this PR adds.

Complexity — the capture/replay blocks were lifted into _capture_output_item, _capture_response_output, _persisted_output_fields, _replay_output_items. complexipy: stream() 152 → 132 (main: 127); _format_request_messages 67 → 57 (main: 55). The complexity/high label reflects main's baseline for stream(), not this PR.

Checks I cannot clear myself: check-api-review-label fails until a maintainer adds api/review-complete alongside the existing api/needs-review; 2 Approvers for Bots needs two human approvals.

Evidence (logs, diff, probe scripts): s3://strandly-prod-artifacts-319426459652/strands-agents/harness-sdk/pr/3419/ — mantle-integ.log (failure on 6177fad9f), mantle-integ-main.log, mantle-replay-shapes.log, mantle-tools-branch.log, mantle-integ-fixed.log, mantle-integ-final.log, mantle_*.py probes, round5-full-gates.log, round6-gates.log.

Response to the github-actions re-review (2026-10-16 / 2026-10-20)
  • API review label — api/needs-review is applied; the failing check-api-review-label check wants api/review-complete, which only a designated API reviewer should add. Design questions for that review are in the ledger below.
  • Session round-trip test per SDK — added in 6177fad9f.
  • stream() complexity — reduced as above.
  • Live Mantle check — run (see Live Bedrock Mantle). It exposed a real regression, fixed in d099713ec. Still unverified: api.openai.com / Azure / custom base_url (no provider key in the sandbox).
  • Codecov — patch gaps closed.
Independent review ledger (reviewer + adversarial passes, fresh context)

No 🔴 from any review pass. Rebase resolution judged coherent with main; nothing in the PR is redundant with main.

Fixed in 34c422e

  • 🟡 No coverage for the capture path on a real response.incomplete output list → test_stream_captures_incomplete_output_items
  • 🔵 Python warned on any history whose additionalModelResponseFields merely lacked openaiResponsesOutput (e.g. Bedrock-sourced) → returns quietly, matching TS
  • 🔵 TS validator required id on every item; it is optional on function_call/custom_tool_call in openai 6.45.0 → made optional, test added
  • 🔵 Comment explaining the removed trailing empty-content filter (reasoning items have no content); unrelated blank-line churn in streaming.py reverted

Fixed in ff93926 (adversarial, repro in adv-probe-c.log C1 / adv-probe-b.log B4)

  • 🟡 Replay skips the message's own toolUse blocks, so if the captured items lacked a function_call for an emitted call (terminal event with empty/absent output and no output_item.done for it, or a stream ending without a terminal event) the next request carried an orphaned function_call_output — permanently, since the message is persisted. Both SDKs now only attach the field when captured function_call ids cover every emitted tool call; tests fail without the guard.

Fixed in d099713 / c2b8746 (live Mantle run + one independent reviewer)

  • Live failure (not a review finding): Mantle rejects replayed message output items in streaming mode → Mantle-scoped string-form fallback for message items only. Reviewer verdict: approve with nits; Python/TS parity verified case-by-case; non-Mantle behaviour unchanged.
  • 🔵 TS _mantle used !== undefined, so a JS caller passing bedrockMantleConfig: null would get the Mantle workaround on a plain OpenAI client → Boolean(bedrockMantleConfig).
  • 🔵 Duplicate JSDoc block on formatResponsesMessages merged.
  • 🔵 Test pinning the only case where the Mantle filter reorders the request ([reasoning, message, function_call] + text/toolUse → [reasoning, function_call, string text]) added in both SDKs; the reviewer verified that order live on both Mantle models.
  • Rejected: substituting the string text at the original message index instead of appending — the appended order is verified accepted and keeps a single fallback path. Rejected: applying the string-form fallback globally — verbatim replay is the point of OpenAI Responses adapter drops reasoning across multi-turn (no output-item round-trip) #3389, and the only endpoint it would hurt (api.openai.com) is the one we can't test.

Open — maintainer call, deliberately not changed in a rebase

  • 🟡 include: ["reasoning.encrypted_content"] on every stateless request — resolved in 5b5a6d208 by not injecting it at all: OpenAI returns encrypted_content by default in stateless mode (include documented as legacy), and on Mantle it was a verified no-op. Removes the blast-radius risk on OpenAI-compatible endpoints; users on a backend that still needs the legacy include can set it in params.
  • 🟡 Validation is all-or-nothing: one unrecognized item type drops the whole turn's items (legacy fallback). Per-item filtering is the alternative but risks dropping a function_call whose result follows — design choice for maintainers.
  • 🟡 recover_message_on_max_tokens_reached (provider-agnostic) hardcodes the OpenAI field name; TS has no equivalent filtering. Layering question.
  • 🟡 Max-token recovery replays a lone reasoning item followed by a plain assistant text message. Accepted by both Mantle models live (shape E/B in mantle-replay-shapes.log); unverified on api.openai.com.
  • 🟡 Replay bypasses _normalize_messages' INVALID_TOOL_NAME rewrite ([BUG] Invalid tool names result in dangling toolUse blocks #1069) — only reachable when a model emits an invalid tool name; cheap follow-up guard possible.
  • 🔵 The Mantle workaround keys off the config object, so a user pointing client_args={"base_url": …} / clientConfig.baseURL at Mantle by hand still gets verbatim message items.

Rejected

  • Attaching the field on content_filtered: not a regression — main already replays an orphaned function_call from the toolUse block on a filtered turn; reasoning-only filtering there would be a main-side follow-up.

Angles that found nothing: SlidingWindow (window 2–7, pinned prefixes) and Summarizing conversation managers, output_item.done vs terminal-output ordering, incomplete with output=[]/None, FileSessionManager + SessionMessage round-trip, invalid-item fallback, redaction popping the field, Bedrock/other providers rebuilding {role, content} so the field never leaks upstream.

  • I ran hatch run prepare (equivalent commands run individually; see above)

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@strandly-the-agent
strandly-the-agent requested a review from a team as a code owner July 23, 2026 05:04
@github-actions github-actions Bot added size/l bug Something isn't working area-model Related to models or model providers area-sessions Related to session or session managment strands-running labels Jul 23, 2026
@codecov

codecov Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.05825% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
strands-py/src/strands/models/openai_responses.py 97.61% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Solid, well-tested cross-SDK bug fix with good parity between Python and TypeScript. The include-merge is non-mutating, the length > tool_calls finish-reason reordering correctly stops incomplete tool calls from executing at the token boundary, and replay validation guards against malformed history. My feedback is mostly about the newly-persisted public field and validation maintainability rather than correctness.

Review themes
  • API surface: A new field on the public Message type is now persisted in sessions — worth an API review pass and a documented contract (namespacing, forward-compat), plus the needs-api-review label.
  • Provider isolation: Forwarding additionalModelResponseFields to every provider replaces a hard whitelist with a namespacing convention; confirm intent and guard it with a test.
  • Maintainability: TS uses a hand-rolled item-type allowlist while Python reuses the OpenAI SDK's TypeAdapter — a drift risk worth documenting or unifying.
  • Testing: Add a session round-trip test for the new field and close the remaining coverage gaps flagged by codecov.

Nice work keeping the two SDK implementations closely aligned and preserving the legacy string fallback.

@yonib05 yonib05 added complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting size/m and removed size/l labels Aug 7, 2026
@yonib05

yonib05 commented Oct 16, 2026

Copy link
Copy Markdown
Member

@strandly-the-agent can you rebase, re-test and re-review the PR?

@strandly-the-agent

Copy link
Copy Markdown
Contributor Author

@yonib05 Done. Rebased onto main 7939e67 (squashed; conflicts in 4 Python files resolved), gates green, PR is mergeable again. Fresh re-review found no 🔴; the 🟡/🔵 findings are fixed in two follow-up commits, and the open design questions are listed in the PR body for a maintainer call.

The one thing I'd want before merge: a live Mantle / non-reasoning-model check that include: ["reasoning.encrypted_content"] is accepted on every stateless request — neither review pass could verify that without a provider key.

Details

Rebase — 729 commits behind. Conflicts in openai_responses.py (main's #5074 content_filter and #4139 cut-off-tool-call precedence kept verbatim, only the output-items plumbing added), streaming.py, test_streaming.py, test_openai_responses.py (dropped my now-redundant incomplete-tool-call test in favour of main's). TS auto-merged cleanly.

Gates on ff939261f — TS: eslint / prettier / tsc pass, vitest 5173 passed (2 telemetry/config.test.ts failures reproduce identically on clean main in this sandbox). Python: ruff format + check pass, mypy 347 files pass, full hatch test 7638 passed / 31 skipped.

Review — reviewer + adversarial pass, fresh context.

  • 34c422e30: real-list response.incomplete capture test; Python no longer warns on foreign additionalModelResponseFields; TS accepts the SDK-optional id on function_call/custom_tool_call.
  • ff939261f: if captured items didn't include a function_call for an emitted tool call (terminal event with empty output, or a stream ending without one), replay would orphan the function_call_output permanently. Both SDKs now only persist the items when they cover every tool call; tests fail without the guard.
  • Open (in PR body): unconditional include blast radius; all-or-nothing item validation vs per-item; OpenAI field name hardcoded in recover_message_on_max_tokens_reached; lone-reasoning-item replay after max-token recovery (needs a live call).

Logs, diff and probe scripts: s3://strandly-prod-artifacts-319426459652/strands-agents/harness-sdk/pr/3419/

@yonib05 yonib05 added the api/needs-review Makes changes to the public API surface label Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment (re-review after rebase + 34c422e3/ff93926)

Thanks for the thorough rebase and the re-review ledger — the resolution looks coherent with main. Most of my first-pass themes are addressed: provider isolation is now documented in the PR body and hardened by the tool-call-coverage guard, Python no longer warns on foreign additionalModelResponseFields, and the optional-id validator fix is sensible. Good work.

Still open
  • API review (process): Adding additionalModelResponseFields to the public Message type + persisting it in sessions still hasn't been through API review — the PR carries bug/area-sessions but no needs-api-review label. You've helpfully listed the design questions (unconditional include blast radius, all-or-nothing validation, OpenAI field name hardcoded in the provider-agnostic recover_message_on_max_tokens_reached) in the PR body; those are exactly the calls a designated API reviewer should sign off on before merge. Suggest adding the label.
  • Testing: Still no session round-trip test asserting additionalModelResponseFields survives serialize → deserialize in either SDK, despite the PR explicitly claiming session-serialization support. Worth one test per SDK to lock it in.
  • Complexity: stream() tripped complexity/high — see inline note.
  • Needs a live check (your call-out): include: ["reasoning.encrypted_content"] acceptance on non-reasoning / Mantle models, and the lone-reasoning-item max-token replay — agree these need a provider key; recommend the Mantle integ run before merge.

Nice job keeping the two SDKs in lock-step through the rebase.

@yonib05

yonib05 commented Oct 17, 2026

Copy link
Copy Markdown
Member

@strandly-the-agent can you review comments and failed codecov tests

@strandly-the-agent

Copy link
Copy Markdown
Contributor Author

@yonib05 Pushed 6177fad9f: codecov patch gaps closed with 9 new tests, a session round-trip test per SDK, and the stream() complexity pulled back to main's baseline. Gates green (Python 7648 passed; TS 5174 passed, same 2 pre-existing telemetry failures as clean main).

Two red checks I can't clear: check-api-review-label wants a maintainer to add api/review-complete (the api/needs-review label is already on), and 2 Approvers for Bots needs two human approvals.

Details

Codecov — the 15 uncovered lines were all in openai_responses.py and _recover_message_on_max_tokens_reached.py. New tests: configured include already containing reasoning.encrypted_content; replay with no text fallback; foreign additionalModelResponseFields (no warning); non-dict items; SDK-model → dict conversion and unsupported-type error; stateful mode skipping capture; recovery with non-list / no-reasoning / foreign fields. Locally: recovery module 100% branch coverage, openai_responses.py 97% with the remaining partials being pre-existing _stream loop back-edges, not PR lines.

Re-review comment items

  • Session round-trip test per SDK — added (SnapshotSessionManager round-trip in Python; takeSnapshot → JSON → loadSnapshot in TS).
  • stream() complexity — capture/replay blocks lifted into four small helpers. complexipy: stream() 152 → 132 (main is 127), _format_request_messages 67 → 57 (main is 55). No inline note was actually attached to the PR, so I worked from the summary.
  • API review label — already applied; the remaining step is a reviewer's api/review-complete. Design questions for that review are in the PR body ledger.
  • Live check of include on non-reasoning / Mantle models and lone-reasoning-item replay — still open, needs a provider key.

Evidence: s3://strandly-prod-artifacts-319426459652/strands-agents/harness-sdk/pr/3419/ (round4-full-gates.log, round4-coverage-commit.diff).

@yonib05
yonib05 enabled auto-merge (squash) October 20, 2026 23:46
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment (re-review of 6177fad)

Every actionable item from my last pass is now addressed — thanks for the quick turnaround.

Verified resolved
  • Session round-trip tests — added in both SDKs with full-object assertions (Python test_snapshot_session_manager.py persist → reload into a fresh agent → == fields; TS snapshot.test.ts "preserves additionalModelResponseFields through a JSON save/load cycle" → toEqual(fields)). I ran the Python one locally: passes.
  • stream() complexity — cleanly decomposed into _replay_output_items / _persisted_output_fields / _capture_output_item / _capture_response_output / _stream_switch_content. The tool-call-coverage guard moved into _persisted_output_fields verbatim, so behavior is unchanged and the hot path reads much better.
  • Codecov — 83% → 99% patch coverage; the new tests hit the edge cases I was worried about (pre-populated include, no-text fallback, foreign/non-dict fields, SDK-model→dict conversion + unsupported-type error, stateful skip).
  • API label — confirmed api/needs-review is applied (my mistake last round); design questions are laid out in the PR body for the reviewer.

Remaining before merge (neither is code-side / neither is yours to clear in the sandbox):

  1. Maintainer api/review-complete + the two human approvals.
  2. A live run confirming include: ["reasoning.encrypted_content"] is accepted on non-reasoning / Mantle / custom-base_url requests, plus the lone-reasoning max-token replay path — recommend the Mantle integ suite as the merge gate.

Nice, disciplined iteration across both SDKs here.

@yonib05

yonib05 commented Oct 21, 2026

Copy link
Copy Markdown
Member

@strandly-the-agent can you address comments?

auto-merge was automatically disabled October 21, 2026 16:49

Head branch was pushed to by a user without write access

@strandly-the-agent

Copy link
Copy Markdown
Contributor Author

@yonib05 The one open item was the live Mantle check — this sandbox turned out to have a Bedrock role, so I ran it. Good thing: it exposed a real regression. On 6177fad9f, tests_integ/models/test_model_mantle.py::test_reasoning_content_multi_turn failed (it passes on clean main). Fixed in d099713ec, reviewed, and pushed as c2b8746c3: Mantle integ suite 5/5, unit gates green (Python 7651 passed; TS 5175 passed, same 2 pre-existing telemetry failures as main).

Still not mine to clear: api/review-complete from an API reviewer and the two human approvals.

Details

What broke — wire capture shows the adapter sends a complete message output item (id, status, annotations), but Mantle's streaming path strips id/status before its internal validation and rejects the request (SubmitRequestFailure, 219 pydantic errors, all on that item). reasoning and function_call items replay fine on Mantle; the string-form assistant message is accepted. Only openai.gpt-oss-120b (/v1 path) fails; openai.gpt-5.4 on Mantle accepts every shape. Non-streaming requests don't error but return empty output. Probe matrix in mantle-replay-shapes.log.

Fix — when bedrock_mantle_config / bedrockMantleConfig is set, message items are dropped from the replay and the existing string-form text fallback is used; reasoning and function_call items still replay, so gpt-oss reasoning is preserved across turns. Non-Mantle behaviour is unchanged (verified case-by-case by the reviewer). Scoped to Mantle rather than global because verbatim replay is the point of #3389 and api.openai.com is the one endpoint I can't test.

Verified live on c2b8746c3 — Mantle suite 5/5; two-turn tool-use agent on both openai.gpt-oss-120b and openai.gpt-5.4; include: ["reasoning.encrypted_content"] accepted on every request by both models, which closes the earlier blast-radius question for Mantle (still unverified for api.openai.com / Azure / custom base_url).

Review — one fresh-context reviewer: approve with nits, all folded in (Boolean(bedrockMantleConfig) so a JS null isn't treated as Mantle; merged a duplicate JSDoc; a test in each SDK pinning the one case where the filter reorders the request). Full ledger is in the PR body — I also restored the earlier review ledger that my Oct 17 body edit had accidentally dropped.

Evidence: s3://strandly-prod-artifacts-319426459652/strands-agents/harness-sdk/pr/3419/ (mantle-integ*.log, mantle-replay-shapes.log, mantle_*.py, round5-full-gates.log, round6-gates.log).

@lizradway

Copy link
Copy Markdown
Member

@strandly-the-agent why are we always injecting reasoning.encrypted_content? shouldnt we only send this when params.reasoning is set or on mantle? will this break existing endpoints?

@strandly-the-agent

Copy link
Copy Markdown
Contributor Author

@lizradway You're right, and it turns out we don't need to send it at all — removed in 5b5a6d208.

Why it was there: reasoning items only replay statelessly if they carry encrypted_content, and I'd assumed include: ["reasoning.encrypted_content"] was required to get it. OpenAI's reasoning guide now says otherwise: in stateless mode (store: false or ZDR) reasoning items "include an encrypted_content property by default" and the include is "legacy … for compatibility, but doesn't require it". And on Mantle I verified it's a no-op: identical reasoning items with or without it on gpt-oss-120b and gpt-5.4.

Would it break existing endpoints? That was the real risk — it went on every stateless request (the default), so any OpenAI-compatible endpoint behind a custom base_url that rejects unknown include values would 400. Mantle accepted it; api.openai.com, Azure and proxies I couldn't test. Not injecting anything sidesteps the question entirely, so I preferred that over gating on params.reasoning (reasoning models emit reasoning items with default params, so that gate would miss the common case). User-configured include still passes through unchanged; anyone on a backend that genuinely needs the legacy value can set it in params.

Verification
  • Mantle integ suite (tests_integ/models/test_model_mantle.py) 5/5 without the include; mantle-include.log shows the with/without comparison on both Mantle models.
  • Unit gates on 5b5a6d208: Python ruff/mypy pass, 7651 passed; TS eslint/prettier/tsc pass, 5176 passed (same 2 pre-existing telemetry/config.test.ts failures as clean main).
  • Diff is a pure removal in both SDKs (_format_request / formatResponsesRequest) plus the three tests that asserted the injection; the ledger item is closed in the PR body.

Evidence: s3://strandly-prod-artifacts-319426459652/strands-agents/harness-sdk/pr/3419/ (mantle-include.log, mantle-integ-no-include.log, round8-full-gates.log).

@yonib05

yonib05 commented Oct 21, 2026

Copy link
Copy Markdown
Member

@strandly-the-agent please rebase

@strandly-the-agent

Copy link
Copy Markdown
Contributor Author

@yonib05 Rebased onto main 74b6790 and pushed as 95ad33da6 — mergeable again. Live Mantle suite 5/5, Python 7815 passed, TS 5356 passed (same 2 pre-existing telemetry/config.test.ts failures as clean main).

Details
  • 46 commits behind; one conflict in openai_responses.py::_format_request_messages — main's fix(models): log unsupported-block skips once per request #5319 added _log_skipped_blocks(logger, skipped) right where this PR removes the trailing empty-content filter. Kept both: the skip log stays, the filter stays removed (replayed reasoning items have no content).
  • All seven branch commits replayed; no other changes. Gates and the Mantle run are in round9-rebase-gates.log under s3://strandly-prod-artifacts-319426459652/strands-agents/harness-sdk/pr/3419/.
  • Still maintainer-only: api/review-complete and the two human approvals.

@yonib05

yonib05 commented Oct 28, 2026

Copy link
Copy Markdown
Member

@strandly-the-agent please rebase

Request reasoning.encrypted_content, capture ordered terminal output items,
persist them on assistant messages under additionalModelResponseFields, and
replay validated items verbatim on stateless turns in both SDKs.

Fixes strands-agents#3389
- Python: return quietly when history lacks openaiResponsesOutput instead
  of warning on every foreign additionalModelResponseFields
- TS: allow the SDK-optional id on function_call / custom_tool_call items
- cover the real-list response.incomplete capture path
Replay drops the toolUse blocks, so items missing an emitted function_call
would orphan its tool result on the next request. Omit the field in that case
and let the legacy history path rebuild the calls.
Close the codecov patch gaps (configured include, replay without text
fallback, foreign response fields, non-dict and SDK-model items, stateful
capture skip, recovery without reasoning items), add a session round-trip
test per SDK, and lift the output-item capture/replay blocks out of
stream()/_format_request_messages to pull their complexity back toward main.
Mantle's gateway strips id/status from a replayed message output item in
streaming mode and then rejects the request (verified against
openai.gpt-oss-120b; main's tests_integ/models/test_model_mantle.py
test_reasoning_content_multi_turn failed on the branch). Reasoning and
function_call items replay fine, so on Mantle keep those and send the
assistant turn as the string-form message instead.
Stateless Responses already return encrypted reasoning by default and the
include is documented as legacy/not required; on Bedrock Mantle it is a
no-op. Injecting it on every stateless request risked rejections from
OpenAI-compatible endpoints for no benefit. User-configured include values
still pass through unchanged.
@strandly-the-agent

Copy link
Copy Markdown
Contributor Author

@yonib05 Rebased onto main baf15f5 and pushed as 2cbeabead — mergeable. Live Mantle suite 5/5, Python 8147 passed, TS 5666 passed (same 2 pre-existing telemetry/config.test.ts failures as clean main).

Details
  • 127 commits behind; one conflict in TS OpenAIModel._streamResponses — main hoisted const stateful = this.stateful (feat(models): add Model.info with capabilities and limits (TypeScript) #5313); kept that and passed this PR's replayMessageItems option alongside. Python applied cleanly.
  • All seven branch commits replayed, no other changes. Log: round10-rebase-gates.log under s3://strandly-prod-artifacts-319426459652/strands-agents/harness-sdk/pr/3419/.
  • Still maintainer-only: api/review-complete and two human approvals. If it helps keep this from drifting again, I'm happy to squash the seven commits into one.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api/needs-review Makes changes to the public API surface area-model Related to models or model providers area-sessions Related to session or session managment bug Something isn't working complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI Responses adapter drops reasoning across multi-turn (no output-item round-trip)

3 participants