Keep deferred tool entries in place when a `defer_loading=True` capability is loaded on Anthropic

Merged
DO
8d ago
+60-5

On Anthropic without a tool-search surface, resolve_request_tools declares a defer_loading=True capability's tools as defer_loading entries from the first request. When load_capability revealed them, _revealed_deferred_tool_order moved them to the tail of tools in reveal order (and load_capability reveals in name order), so the deferred entries earlier requests sent came back reordered. Anthropic leaves deferred entries out of its cache key, but the wire contract is append-only (anthropic_deferred_tool_blocks).

Now only a tool the reveal first declares moves to the tail: a mid-run newcomer, or a deferred tool that was withheld because a tool-search surface is on the wire. A tool already declared deferred stays where earlier requests sent it.

Reproduction (before this PR)

Two deferred capabilities, the first loaded, with tools declared out of name order (write_note, read_note, delete_note, then look_up_archive):

RequestDeferred entries
1 (before load)write_note, read_note, delete_note, look_up_archive
2 (after load), beforelook_up_archive, delete_note, read_note, write_note
2 (after load), this PRwrite_note, read_note, delete_note, look_up_archive

Fixing only the name-order sort in load_capability isn't enough: the tail move still reorders whenever the loaded capability isn't the last deferred one declared.

Tests

  • tests/test_history_append_only.py gets a VARIANTS table with a Capability(defer_loading=True) case: the two-capability scenario above, loaded in the first run and called in the second. Anthropic and OpenAI Responses pass.

OpenAI Chat

Chat Completions has no deferred-tool channel, so a load adds the capability's tools to tools, ahead of the messages already sent. That's the same one-time move #9405 makes for mid-run newcomers on Chat. The move is now an ALLOWED entry in the append-only matrix, limited to one move per load and only on openai-chat. The cache table in docs/capabilities/on-demand.md now says such a load breaks the prefix once, instead of "may break between turns".

Checklist

  • No breaking changes in accordance with the version policy.
  • Any permitted compatibility impact has a label, warning, migration, release note, and exact API-check waiver.
  • PR title is fit for the release changelog.

šŸ¤– Generated with Claude Code