Keep a first-position `ToolAvailabilityDeltaPart` announcement out of the standing system prompt
A ToolAvailabilityDeltaPart in the first position of the first ModelRequest was hoisted into the provider's standing system prompt instead of being announced in the conversation. On models that announce deltas as statements, the delta renders as a SystemPromptPart, and the standing prompt was counted on that rendered request, so the announcement counted as part of it. Non-inline adapters then lifted it into the system field, where it stood over the whole run and rewrote the first cache section on the turn it appeared and again on the turn it didn't. Such a delta comes from a hand-built message_history, a UI-adapter load, or history trimming that promotes a later request to first position.
The fix counts the standing prompt on the history as authored: prepare_messages counts the first request's opening SystemPromptParts before any delta renders and passes that count to _wrap_non_leading_system_prompts. _announce_tool_availability_delta_messages likewise keeps only authored system prompts at the front when it sorts tool results forward. A delta isn't a SystemPromptPart, so the leading run breaks at it, and the announcement takes the same <system>-wrapped, in-conversation route as an announcement anywhere else.
The issue's Google probe, before and after:
before system: {'role': 'user', 'parts': [{'text': 'The following tool(s) are now available: `weather`'}]}
contents: [{'role': 'user', 'parts': [{'text': 'go'}]}]
after system: None
contents: [{'role': 'user', 'parts': [{'text': '<system>The following tool(s) are now available: `weather`</system>'}, {'text': 'go'}]}]
The same happened with an authored standing prompt ahead of the delta ([SystemPromptPart, delta, UserPromptPart]): both went into the system field, and now only the authored one does. test_first_request_announcement_is_not_standing_system_prompt covers three shapes: delta first, delta after a standing prompt, and delta before a tool return. The last one pins the sort order, since the announcement has to go behind the tool results.
The change is independent of how announcements render, so #7251 (and the <context id> rendering from #7891) can pick it up by merging main.
One behavior to note: when the first authored request renders to nothing (a delta whose only tool is no longer served) and the next request opens with a SystemPromptPart, non-inline models now wrap that part as <system> text instead of hoisting it. That matches the documented rule that only the first request's opening system prompts are standing. In the agent path, consecutive requests are merged before prepare_messages, so a request right after the dropped one isn't a separate first request anyway.
- Closes #7899
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