Skip to content

fix(core): keep deferred provider calls only on the last surviving assistant turn#15682

Merged
CalebBarnes merged 2 commits into
mainfrom
roamin/github-issue-analysis-request
May 7, 2026
Merged

fix(core): keep deferred provider calls only on the last surviving assistant turn#15682
CalebBarnes merged 2 commits into
mainfrom
roamin/github-issue-analysis-request

Conversation

@roaminro

@roaminro roaminro commented Apr 23, 2026

Copy link
Copy Markdown
Member

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 a tool-call (Anthropic web_search), the unresolved provider-executed call gets persisted in input-available state. sanitizeV5UIMessages was unconditionally keeping every input-available + providerExecuted part 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-available provider-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:

const lastSurvivingAssistantIdx = findLastAssistantAfterSanitization();
const assistantTurnStillOpen = idx === lastSurvivingAssistantIdx && idx > lastUserIdx;

if (p.state === 'input-available' && p.providerExecuted && assistantTurnStillOpen) {
  return true;
}

Regression tests cover the Gemini code_execution orphan (#15668), the Anthropic web_search orphan (#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.

…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>
@roaminro roaminro requested a review from CalebBarnes April 23, 2026 18:51
@vercel

vercel Bot commented Apr 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mastra-docs-1.x Skipped Skipped Apr 23, 2026 7:52pm

Request Review

@changeset-bot

changeset-bot Bot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d44668f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@mastra/core Patch
mastracode Patch
@mastra/mcp-docs-server Patch
@internal/playground Patch
@mastra/client-js Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
mastra Patch
@mastra/deployer-cloud Patch
@mastra/deployer-vercel Patch
@mastra/playground-ui Patch
@mastra/react Patch
@mastra/server Patch
@mastra/deployer Patch
create-mastra Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/deployer-cloudflare Patch
@mastra/deployer-netlify Patch

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

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Sanitization of V5 UI messages now prevents orphaned provider-executed tool parts from persisting across turns: input-available provider-executed tool parts are retained only on the most recent assistant message when no subsequent user turn exists; earlier orphaned tool parts are dropped.

Changes

Cohort / File(s) Summary
Changeset note
\.changeset/puny-rats-talk.md
Adds a patch Changeset documenting the bug fix for orphaned provider-executed tool calls causing threads to become unusable.
Tests
packages/core/src/agent/message-list/conversion/output-converter-provider-executed.test.ts
Adds regression tests for #15668: builds multi-turn histories with provider-executed tool orphans (Gemini code_execution, Anthropic web_search, stale deferred calls) and asserts converted messages contain no unpaired tool-call parts; includes a preservation case for a deferred tool on the last assistant.
Core sanitization logic
packages/core/src/agent/message-list/conversion/output-converter.ts
Updates sanitizeV5UIMessages: precomputes last user and surviving assistant indices, introduces getSafeParts, and restricts retention of provider-executed input-available parts to the most-recent assistant without a following user, dropping earlier orphaned parts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code changes directly address the objectives from #15668: preventing persistence and replay of orphaned provider-executed tool calls by sanitizing orphaned parts on closed assistant turns.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing orphaned provider-executed tool calls in message conversion and sanitization, with comprehensive test coverage for related issues.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main bug fix: constraining provider-executed tool calls to only the last surviving assistant turn to prevent orphaned calls from being replayed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch roamin/github-issue-analysis-request

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3497c1d and b4857f5.

📒 Files selected for processing (3)
  • .changeset/puny-rats-talk.md
  • packages/core/src/agent/message-list/conversion/output-converter-provider-executed.test.ts
  • packages/core/src/agent/message-list/conversion/output-converter.ts

Comment thread packages/core/src/agent/message-list/conversion/output-converter.ts Outdated
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>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x April 23, 2026 19:52 Inactive
@roaminro roaminro changed the title fix(core): drop orphaned provider-executed tool calls on closed assistant turns fix(core): keep deferred provider calls only on the last surviving assistant turn Apr 23, 2026
@CalebBarnes CalebBarnes merged commit 99869ec into main May 7, 2026
50 checks passed
@CalebBarnes CalebBarnes deleted the roamin/github-issue-analysis-request branch May 7, 2026 18:02
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.

[BUG] Gemini code_execution (Vertex) leaves orphan tool calls → empty-response cascade

2 participants