Skip to content

fix(streaming): get_final_message() drops container, breaking code execution continuation - #1776

Open
epatey wants to merge 1 commit into
anthropics:mainfrom
epatey:fix/streaming-container-propagation
Open

fix(streaming): get_final_message() drops container, breaking code execution continuation#1776
epatey wants to merge 1 commit into
anthropics:mainfrom
epatey:fix/streaming-container-propagation

Conversation

@epatey

@epatey epatey commented Jul 23, 2026

Copy link
Copy Markdown

When streaming with client.messages.stream(), the final message's container field is always None, even though the API sends it in the message_delta event. The streaming code copies stop_reason, stop_sequence, stop_details, and usage from that event, but not container. The equivalent code for client.beta.messages.stream() does copy it.

Without the container id, a caller can't continue a conversation that has unfinished code execution — the next request fails with 400: container_id is required when there are pending tool uses generated by code execution with tools.

This adds the missing copy, plus sync and async tests that fail without it.

Fixes #1424.

…ecution continuation

When streaming with client.messages.stream(), the final message's
container field is always None, even though the API sends it in the
message_delta event. The streaming code copies stop_reason,
stop_sequence, stop_details, and usage from that event, but not
container. The equivalent code for client.beta.messages.stream() does
copy it.

Without the container id, a caller can't continue a conversation that
has unfinished code execution -- the next request fails with:
'container_id is required when there are pending tool uses generated by
code execution with tools.'

This adds the missing copy, plus sync and async tests that fail
without it.

Fixes anthropics#1424
@epatey
epatey marked this pull request as ready for review July 23, 2026 16:47
@epatey
epatey requested a review from a team as a code owner July 23, 2026 16:47
epatey added a commit to UKGovernmentBEIS/inspect_ai that referenced this pull request Jul 23, 2026
Three fixes for the 400 'container_id is required when there are
pending tool uses generated by code execution with tools':

- Replay pending server tool spans: a span whose results never arrived
  (the turn ended first, e.g. a client tool call cut in) had no content
  item to anchor it and was silently dropped from the assistant replay.
  Spans that never produced content now replay verbatim; spans whose
  content was deliberately deleted by a scaffold still drop.

- Replay the container id across turns: record each response's
  container id (keyed by assistant message id, incl. dump/restore and
  head/tail continuation merge) and pass it as the 'container' request
  param when the last assistant turn left container work pending.

- Capture the container under streaming: the SDK's non-beta stream
  accumulator drops message_delta.container (fix proposed in
  anthropics/anthropic-sdk-python#1776), so capture it from the raw
  event in _capture_compaction_from_stream. This also un-breaks the
  existing same-turn pause_turn continuation, which reads
  head_message.container.
dragonstyle pushed a commit to UKGovernmentBEIS/inspect_ai that referenced this pull request Jul 27, 2026
…alls (container id not replayed) (#4603)

* Add failing test: pending code execution lost after client tool call

When a turn mixes code-execution server tool use with a client tool
call, the response ends with stop_reason 'tool_use' while the code
execution is still pending, and carries a container id. The follow-up
request must replay the pending server_tool_use block and include the
container id as the 'container' param. inspect does neither: the
pending block is dropped from the assistant replay, and the container
id is only threaded through same-turn pause_turn continuations, never
across turns. The API rejects such follow-ups with 'container_id is
required when there are pending tool uses generated by code execution
with tools.'

(The id is also lost under streaming by an SDK bug -- the non-beta
stream accumulator drops message_delta.container; fix proposed in
anthropics/anthropic-sdk-python#1776. This test uses the non-streaming
path, where the response includes the id, to isolate inspect's gaps.)

* Fix Anthropic container continuation after client tool calls

Three fixes for the 400 'container_id is required when there are
pending tool uses generated by code execution with tools':

- Replay pending server tool spans: a span whose results never arrived
  (the turn ended first, e.g. a client tool call cut in) had no content
  item to anchor it and was silently dropped from the assistant replay.
  Spans that never produced content now replay verbatim; spans whose
  content was deliberately deleted by a scaffold still drop.

- Replay the container id across turns: record each response's
  container id (keyed by assistant message id, incl. dump/restore and
  head/tail continuation merge) and pass it as the 'container' request
  param when the last assistant turn left container work pending.

- Capture the container under streaming: the SDK's non-beta stream
  accumulator drops message_delta.container (fix proposed in
  anthropics/anthropic-sdk-python#1776), so capture it from the raw
  event in _capture_compaction_from_stream. This also un-breaks the
  existing same-turn pause_turn continuation, which reads
  head_message.container.

* Mark SDK stream-container workaround with removal pointer

* Deduplicate SDK workaround comment

* Handle server tool results that arrive a turn after their use block

The live test surfaced a fourth gap: when a turn ends with a server
tool call still running, its result arrives at the start of the NEXT
response, where the parser raised 'CodeExecutionToolResultBlock without
previous ServerToolUseBlock' (pending_tool_uses only covers the current
response). Resolve orphaned results against the use block recorded in
the prior turn's span, and record only the result in the new message's
span (the use block replays with its own message, so it isn't
duplicated).

Also adds a slow live test driving the full shape end-to-end (warmup
exec to create the container, then a parallel exec + client tool call
leaving pending work, then the tool-result follow-up), and moves the
CHANGELOG entry to the Bugfix convention (prefix + bottom of the
Unreleased list).

* Live test observes only the public API

Replace the private _pending_container_for_input skip-gate with a
public-surface check: the mixed turn is an assistant message making a
client tool call whose content lacks the sleep's output, and successful
resumption is that output surfacing in a later assistant message's code
execution content. This also lets the test run against unfixed code.

Verified red/green live: with the orphaned-result fix reverted (fixes
1-3 present) the test fails with 'CodeExecutionToolResultBlock without
previous ServerToolUseBlock'; with the full fix it passes.

* Live test detects silent loss via container-state seed

The previous public-surface check (sleep output appearing in a later
message) could not distinguish true resumption from silent loss: with
the pending call dropped from the replay, the model re-runs the command
in a fresh container -- and if it re-runs the warmup too, produces the
output anyway.

Now the warmup plants a random seed file AND prints the seed, and the
pending command echoes it back. Only the original container can produce
marker-<planted seed>: after silent loss a re-run yields a bare
'marker-' (no seed file) or a different seed (re-warmed), so the test
fails instead of passing vacuously or skipping.

Verified live both ways: FAILS against the pre-fix provider (with the
SDK #1776 workaround applied, inert here since the test runs
non-streaming); PASSES at HEAD with the marker carrying the exact
planted seed.

* Address review: verify resolved-span replay on later turns, document limits

- extend unit + live tests to a turn after the pending work resolves
  (use block replays with its message, result with its own, no container)
- document why the prior span deliberately stays open after
  add_prior_turn_result (history truncation re-pends the work)
- document bridge limitation for containers (no bridge-surviving key
  exists for a pending span)
- note pending-span replay position is unanchored by design
- trim call-site comments that restated the docstring
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.

get_final_message() does not propagate message_delta.container into aggregated Message (breaks code_execution continuation)

1 participant