Skip to content

Fix supervisor narration leaking into final text#18644

Open
labifrancis wants to merge 2 commits into
mainfrom
fix-narration-text
Open

Fix supervisor narration leaking into final text#18644
labifrancis wants to merge 2 commits into
mainfrom
fix-narration-text

Conversation

@labifrancis

@labifrancis labifrancis commented Jun 29, 2026

Copy link
Copy Markdown

Summary

  • Keep delegated sub-agent narration streaming to consumers
  • Exclude delegated narration from supervisor final stream.text
  • Drop pre-delegation narration from tool-call steps before final answer aggregation
  • Add regression coverage for supervisor streamed narration

Fixes #17986

Test plan

  • pnpm build:core
  • pnpm --filter ./packages/core check
  • pnpm --filter ./packages/core test:unit src/agent/tests/supervisor-integration.test.ts

ELI5

When the supervisor “talks while thinking,” that chatter should be visible as it streams, but it shouldn’t end up mixed into the final saved answer. This PR makes Mastra keep narrated streaming text for live consumers while stripping it out from the supervisor’s final stream.text.

Summary

  • Prevents streamed supervisor narration from being included in the final resolved stream.text (while still streaming it to consumers).
  • Marks delegated sub-agent text-start/text-delta/text-end chunks with metadata (subAgentId + __mastraExcludeFromOutputText: true) so they’re forwarded live but excluded from the final output-text aggregation.
  • Removes pre-delegation narration from tool-call steps before final answer assembly by truncating buffered text around step-finish when the step result reason is tool-calls.
  • Updates MastraModelOutput to correctly buffer/truncate streamed text across step boundaries using a persisted checkpoint (#bufferedTextStepStartIndex), skipping buffered text when __mastraExcludeFromOutputText is set.
  • Adds an integration regression test (should stream sub-agent text without including it in the supervisor final text) asserting:
    • supervisor narration + delegated sub-agent streamed text both appear in the streamed deltas, and
    • stream.text resolves to only the supervisor’s final answer.
  • Includes a patch changeset for @mastra/core documenting the fix and verifies via core package build, type-check, and unit tests.

Supervisor delegation streams nested agent and pre-tool-call narration to consumers, but that text should not be aggregated into the supervisor's final answer. Mark delegated text chunks as stream-only and reset buffered narration from tool-call steps so stream.text resolves to the actual final response.\n\nCo-Authored-By: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
mastra-docs-1.x Ready Ready Preview, Comment Jun 29, 2026 9:23pm
mastra-playground-ui Ready Ready Preview, Comment Jun 29, 2026 9:23pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mastra-studio-preview Ignored Ignored Preview Jun 29, 2026 9:23pm

Request Review

@changeset-bot

changeset-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 27a233d

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

This PR includes changesets to release 22 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/react Patch
@mastra/playground-ui Patch
@mastra/server Patch
@mastra/deployer Patch
create-mastra Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
@mastra/next Patch
@mastra/tanstack-start Patch
@mastra/temporal 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 Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bd8671d9-1a28-4081-98d5-bb27693cf0c9

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3103e and 27a233d.

📒 Files selected for processing (1)
  • packages/core/src/agent/agent.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/agent/agent.ts

Walkthrough

Sub-agent text chunks forwarded to a supervisor stream are now tagged with __mastraExcludeFromOutputText: true in both delegation paths. MastraModelOutput uses that flag and a new checkpoint to keep those chunks out of the resolved stream.text value.

Changes

Sub-agent narration exclusion from supervisor output text

Layer / File(s) Summary
Sub-agent chunk tagging in delegation loops
packages/core/src/agent/agent.ts
stream() and streamLegacy() now wrap sub-agent text-start/text-delta/text-end chunks with metadata containing subAgentId and __mastraExcludeFromOutputText: true before forwarding them to context.writer.
MastraModelOutput exclusion flag and step checkpoint
packages/core/src/stream/base/output.ts
MastraModelOutput adds #bufferedTextStepStartIndex, skips excluded text chunks during buffering, truncates buffered text on tool-calls step-finish, and persists/restores the checkpoint in serialized state.
Integration test and changeset
packages/core/src/agent/__tests__/supervisor-integration.test.ts, .changeset/every-icons-rest.md
The new integration test checks that streamed sub-agent text is present in the stream but not in stream.text; the changeset records the patch release note.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • intojhanurag
  • TylerBarnes
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change to supervisor narration handling.
Linked Issues check ✅ Passed The implementation matches #17986 by streaming narrator text while excluding it from the final persisted answer.
Out of Scope Changes check ✅ Passed The changes are focused on narration streaming behavior, regression coverage, and the release note, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 fix-narration-text

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@dane-ai-mastra dane-ai-mastra Bot added the complexity: low Low-complexity PR label Jun 29, 2026
@dane-ai-mastra

dane-ai-mastra Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check skipped for core contributor @labifrancis.


PR complexity score

Factor Value Score impact
Files changed 4 +8
Lines changed 148 +6
Author merged PRs 0 -0
Test files changed Yes -10
Final score 4

Applied label: complexity: low


Changed test gate

Changed tests failed against the base branch as expected.

Label: tests: green ✅

@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

🧹 Nitpick comments (2)
packages/core/src/agent/__tests__/supervisor-integration.test.ts (1)

975-987: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Optional: strengthen assertions to lock in the full contract.

The test validates supervisor narration/final answer in the stream and stream.text, but the PR's headline behavior — sub-agent narration streamed yet excluded from final text — isn't directly asserted. Consider adding expect(streamedText).toContain('Sub-agent streamed answer.') and asserting stream.text does not contain the sub-agent/narration text. A getFullOutput() assertion would also surface the divergence flagged in output.ts (Lines 740-744).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/agent/__tests__/supervisor-integration.test.ts` around
lines 975 - 987, The streaming integration test for supervisor delegation should
assert the full contract around sub-agent output, not just the supervisor
narration and final answer. Update the existing test around
supervisorAgent.stream and streamedText to also verify the sub-agent’s streamed
narration is present in fullStream, while stream.text excludes both the
sub-agent text and any narration. If applicable, add a getFullOutput() assertion
so the test covers the distinction implemented in supervisorAgent.stream and
output handling.
packages/core/src/agent/agent.ts (1)

4952-4963: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract the duplicated chunk-tagging helper.

The exact same wrapping logic now lives in both the modern (Lines 4952-4963) and legacy (Lines 5071-5082) delegation loops. A small helper keeps the exclusion contract in one place so the two paths can't drift.

♻️ Suggested helper
const tagSubAgentTextChunk = (chunk: any) =>
  chunk.type === 'text-start' || chunk.type === 'text-delta' || chunk.type === 'text-end'
    ? { ...chunk, metadata: { ...chunk.metadata, subAgentId: agent.id, __mastraExcludeFromOutputText: true } }
    : chunk;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/agent/agent.ts` around lines 4952 - 4963, The chunk-tagging
logic for sub-agent delegation is duplicated in both the modern and legacy
loops, so extract it into a shared helper in agent.ts and reuse it from each
path. Keep the helper centered around the existing chunk type checks and
metadata augmentation used by the delegation code (including subAgentId and
__mastraExcludeFromOutputText) so the behavior stays identical while avoiding
drift between the two implementations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/stream/base/output.ts`:
- Around line 740-744: The tool-call truncation in the output buffering logic is
inconsistent because `#bufferedText` is trimmed while the already-copied
`stepResult.text` in `#bufferedSteps` is left intact, so `getFullOutput().text`
and `stream.text` diverge. Update the handling in `Output` around
`#bufferedTextStepStartIndex` and the `reason === 'tool-calls'` branch so both
buffered text and step output are truncated consistently when a tool-call step
occurs, and ensure this behavior only applies to the intended delegation case
rather than every regular tool-call step.

---

Nitpick comments:
In `@packages/core/src/agent/__tests__/supervisor-integration.test.ts`:
- Around line 975-987: The streaming integration test for supervisor delegation
should assert the full contract around sub-agent output, not just the supervisor
narration and final answer. Update the existing test around
supervisorAgent.stream and streamedText to also verify the sub-agent’s streamed
narration is present in fullStream, while stream.text excludes both the
sub-agent text and any narration. If applicable, add a getFullOutput() assertion
so the test covers the distinction implemented in supervisorAgent.stream and
output handling.

In `@packages/core/src/agent/agent.ts`:
- Around line 4952-4963: The chunk-tagging logic for sub-agent delegation is
duplicated in both the modern and legacy loops, so extract it into a shared
helper in agent.ts and reuse it from each path. Keep the helper centered around
the existing chunk type checks and metadata augmentation used by the delegation
code (including subAgentId and __mastraExcludeFromOutputText) so the behavior
stays identical while avoiding drift between the two implementations.
🪄 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: 6b2b2bec-0dbe-4c51-90e0-4ae4816af9eb

📥 Commits

Reviewing files that changed from the base of the PR and between 07bf909 and 6a3103e.

📒 Files selected for processing (4)
  • .changeset/every-icons-rest.md
  • packages/core/src/agent/__tests__/supervisor-integration.test.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/stream/base/output.ts

Comment on lines +740 to +744
if (chunk.payload.stepResult.reason === 'tool-calls') {
self.#bufferedText.splice(self.#bufferedTextStepStartIndex);
}
self.#bufferedTextStepStartIndex = self.#bufferedText.length;

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- packages/core/AGENTS.md ---\n'
cat packages/core/AGENTS.md

printf '\n--- outline: packages/core/src/stream/base/output.ts ---\n'
ast-grep outline packages/core/src/stream/base/output.ts --view expanded

printf '\n--- relevant slices ---\n'
sed -n '680,780p' packages/core/src/stream/base/output.ts

printf '\n--- search for getFullOutput / _getImmediateText / bufferedByStep ---\n'
rg -n "getFullOutput|_getImmediateText|bufferedByStep|bufferedTextStepStartIndex|tool-calls|stepResult.text|steps\.map\(step => step\.text\)" packages/core/src/stream/base/output.ts packages/core/src -S

Repository: mastra-ai/mastra

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/core/AGENTS.md ---'
cat packages/core/AGENTS.md

echo
echo '--- outline: packages/core/src/stream/base/output.ts ---'
ast-grep outline packages/core/src/stream/base/output.ts --view expanded

echo
echo '--- relevant slices ---'
sed -n '680,780p' packages/core/src/stream/base/output.ts

echo
echo '--- search for getFullOutput / _getImmediateText / bufferedByStep ---'
rg -n "getFullOutput|_getImmediateText|bufferedByStep|bufferedTextStepStartIndex|tool-calls|stepResult.text|steps\\.map\\(step => step\\.text\\)" packages/core/src/stream/base/output.ts packages/core/src -S

Repository: mastra-ai/mastra

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- text getter / full output / immediate text ---'
sed -n '1210,1465p' packages/core/src/stream/base/output.ts
echo
sed -n '1598,1635p' packages/core/src/stream/base/output.ts

echo
echo '--- focused tests around getFullOutput text shape ---'
sed -n '200,260p' packages/core/src/stream/base/output.test.ts
echo
sed -n '860,960p' packages/core/src/stream/base/output.test.ts

Repository: mastra-ai/mastra

Length of output: 14226


Keep the tool-call truncation aligned across outputs

splice() only trims #bufferedText, but stepResult.text is already copied into #bufferedSteps, so getFullOutput().text still includes the pre-tool narration while stream.text drops it. The reason === 'tool-calls' check also applies to every tool-call step, not just delegation, so this changes output for any agent that speaks before calling a regular tool.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/stream/base/output.ts` around lines 740 - 744, The
tool-call truncation in the output buffering logic is inconsistent because
`#bufferedText` is trimmed while the already-copied `stepResult.text` in
`#bufferedSteps` is left intact, so `getFullOutput().text` and `stream.text`
diverge. Update the handling in `Output` around `#bufferedTextStepStartIndex`
and the `reason === 'tool-calls'` branch so both buffered text and step output
are truncated consistently when a tool-call step occurs, and ensure this
behavior only applies to the intended delegation case rather than every regular
tool-call step.

@dane-ai-mastra dane-ai-mastra Bot added the tests: green ✅ Changed tests failed against base as expected label Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: low Low-complexity PR tests: green ✅ Changed tests failed against base as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Separate "narration" text from the final answer

1 participant