Fix supervisor narration leaking into final text#18644
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
🦋 Changeset detectedLatest commit: 27a233d The changes in this PR will be included in the next version bump. This PR includes changesets to release 22 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 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughSub-agent text chunks forwarded to a supervisor stream are now tagged with ChangesSub-agent narration exclusion from supervisor output text
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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
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. Comment |
PR triageLinked issue check skipped for core contributor @labifrancis. PR complexity score
Applied label: Changed test gateChanged tests failed against the base branch as expected. Label: |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/core/src/agent/__tests__/supervisor-integration.test.ts (1)
975-987: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winOptional: 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 addingexpect(streamedText).toContain('Sub-agent streamed answer.')and assertingstream.textdoes not contain the sub-agent/narration text. AgetFullOutput()assertion would also surface the divergence flagged inoutput.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 valueOptional: 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
📒 Files selected for processing (4)
.changeset/every-icons-rest.mdpackages/core/src/agent/__tests__/supervisor-integration.test.tspackages/core/src/agent/agent.tspackages/core/src/stream/base/output.ts
| if (chunk.payload.stepResult.reason === 'tool-calls') { | ||
| self.#bufferedText.splice(self.#bufferedTextStepStartIndex); | ||
| } | ||
| self.#bufferedTextStepStartIndex = self.#bufferedText.length; | ||
|
|
There was a problem hiding this comment.
🎯 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 -SRepository: 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 -SRepository: 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.tsRepository: 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.
Summary
stream.textFixes #17986
Test plan
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
stream.text(while still streaming it to consumers).text-start/text-delta/text-endchunks with metadata (subAgentId+__mastraExcludeFromOutputText: true) so they’re forwarded live but excluded from the final output-text aggregation.step-finishwhen the step result reason istool-calls.MastraModelOutputto correctly buffer/truncate streamed text across step boundaries using a persisted checkpoint (#bufferedTextStepStartIndex), skipping buffered text when__mastraExcludeFromOutputTextis set.should stream sub-agent text without including it in the supervisor final text) asserting:stream.textresolves to only the supervisor’s final answer.@mastra/coredocumenting the fix and verifies via core package build, type-check, and unit tests.