fix(core): keep deferred provider calls only on the last surviving assistant turn#15682
Conversation
…tant turns When a provider dropped a tool-result chunk (#15668) or a run aborted mid-stream (#14148), the unresolved provider-executed tool call (e.g. Anthropic web_search, Gemini code_execution) was replayed on every subsequent request, deterministically bricking the thread. sanitizeV5UIMessages now only preserves deferred input-available provider-executed tool parts on the most recent assistant message and only when no later user message has closed the turn. Orphans on earlier assistant turns are dropped so outgoing history always satisfies the tool-call/tool-result pairing invariant. Fixes #15668 Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
🦋 Changeset detectedLatest commit: d44668f The changes in this PR will be included in the next version bump. This PR includes changesets to release 21 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
WalkthroughSanitization of V5 UI messages now prevents orphaned provider-executed tool parts from persisting across turns: Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/core/src/agent/message-list/conversion/output-converter.ts`:
- Around line 111-136: Compute the "open assistant turn" against the
post-sanitization view instead of the raw history: add a first pass that
iterates messages from the end to find lastSurvivingAssistantIdx (skip assistant
messages that would sanitize to zero parts using the same logic you apply later,
e.g., check m.parts and any sanitizer conditions) and use that index in place of
lastAssistantIdx when computing assistantTurnStillOpen inside the msgs.map;
update the variable name (or set lastAssistantIdx = lastSurvivingAssistantIdx)
so deferred providerExecuted/input-available parts on an earlier surviving
assistant are preserved when a trailing assistant would have been dropped.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2f6be80c-3d5e-4575-8a7f-4828c13a352f
📒 Files selected for processing (3)
.changeset/puny-rats-talk.mdpackages/core/src/agent/message-list/conversion/output-converter-provider-executed.test.tspackages/core/src/agent/message-list/conversion/output-converter.ts
Compute the open assistant turn from the last surviving assistant message after sanitization so trailing assistant placeholders that sanitize away do not cause valid deferred provider-executed calls to be dropped. PR comment: #15682 (comment) Co-Authored-By: Mastra Code (openai/gpt-5.4) <noreply@mastra.ai>
Fixes #15668 (similar class of bug to #14148).
When a provider drops a tool-result chunk (Vertex/Gemini
code_execution) or a run aborts mid-stream after atool-call(Anthropicweb_search), the unresolved provider-executed call gets persisted ininput-availablestate.sanitizeV5UIMessageswas unconditionally keeping everyinput-available + providerExecutedpart to protect Anthropic's legitimate N→N+1 deferral — which also meant orphans were replayed to the model forever, returning empty text on Gemini and violating the tool-call/tool-result invariant on Anthropic.The fix tightens the keep-rule:
input-availableprovider-executed parts are only preserved on the last surviving assistant message after sanitization, and only when no later user message has closed the turn. Anywhere else, they're treated as orphans and dropped.Conceptually:
Regression tests cover the Gemini
code_executionorphan (#15668), the Anthropicweb_searchorphan (#14148 class), a multi-assistant history where a stale provider call sits on an earlier assistant turn behind a later assistant message, and the case where a trailing assistant sanitizes away so the previous surviving assistant still keeps the legitimate deferred provider call. The existing deferred provider-executed coverage still passes unchanged.