Skip to content

feat(runtimes): mastra sub-agent streaming via a public-API stream tee - #961

Merged
blove merged 8 commits into
mainfrom
blove/mastra-subagent-streaming
Sep 2, 2026
Merged

feat(runtimes): mastra sub-agent streaming via a public-API stream tee#961
blove merged 8 commits into
mainfrom
blove/mastra-subagent-streaming

Conversation

@blove

@blove blove commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The Mastra demo's subagent card now streams the child's deltas live, flipping the matrix cell from Partial to Yes. Spec: docs/superpowers/specs/2026-09-02-mastra-subagent-streaming-design.md; plan: docs/superpowers/plans/2026-09-02-mastra-subagent-streaming.md.

Research verdict (why this layer): @ag-ui/mastra@1.1.2 is the latest pin and drops every tool-output chunk (case "tool-output": break) while buffering the delegation's TOOL_CALL_START/ARGS/END until the tool result. There is no alternative upstream layer, the official onChunk hook excludes tool-output, and a bridge subclass would couple to three TS-private methods plus a clone() that rebuilds the base class. The child's chunks are, however, public and typed on the parent fullStream ({type:'tool-output', payload:{output:<childChunk>, toolCallId, toolName}}), so the fix observes them through a public-API seam. Pins unchanged (@ag-ui/mastra@1.1.2, @mastra/core@1.63.2); the bridge is not modified.

What changed

  • deployments/ag-ui-mastra/streaming-tee.mjs (new): withDelegationTee(agent, observe) — a Proxy over the real Agent that forwards every member bound to the real instance (#private fields and the bridge's 'getMemory' in agent check keep working) and wraps only stream()/resumeStream() results so fullStream is an async generator that calls observe(chunk) before yielding to the bridge. One reader, one iteration, so observer writes are ordered ahead of the bridge's events for the same chunk.
  • deployments/ag-ui-mastra/subagent-emitter.mjs: the per-run injector gains chunk(c) alongside eventsFor(e). The delegation tool-call chunk synthesizes eager TOOL_CALL_START/ARGS/END (args are complete there; parentMessageId from the last start/step-start chunk) + SUBAGENT_STARTED; tool-output inner text-start/delta/end become attributed TEXT_MESSAGE_START/CONTENT/END; tool-result/tool-error/tool-call-suspended close the card. The bridge's later buffered TOOL_CALL_START/ARGS/END copies for a synthesized id are dropped in eventsFor (its TOOL_CALL_RESULT passes through). The old single-chunk synthesis stays only as the no-deltas fallback.
  • deployments/ag-ui-mastra/server.mjs: builds the proxy per request; both inputs write through the same SSE frame writer.
  • Docs: wire-capture doc (spike, after-streaming capture, browser verification, caveats, fallback design), choosing-an-adapter, runtimes intro, Mastra overview and how-it-connects, both runtime blog posts.

Evidence

  • Task 0 spike (raw fullStream, 248 chunks): tool-call (args complete) → tool-output x135 (explicit text-start, 108 text-delta, text-end) → tool-result; tool-call-suspended never appears; chat-tool-calls anchors cards to entries in the parent toolCalls() list, so the eager TOOL_CALL_START is required for the card to mount.
  • Live wire (real key): eager TOOL_CALL_START + SUBAGENT_STARTED at t=1578 ms; 94 attributed TEXT_MESSAGE_CONTENT (399 chars) t=6266–7183 ms; SUBAGENT_FINISHED before TOOL_CALL_RESULT at t=7382 ms; exactly one TOOL_CALL_START for the delegation id.
  • Browser (headless, 150 ms poll of the card's innerText while data-state="running"): 67 → 110 → 188 → 261 → 279 → 306 → 330 → 457 → 494 chars, then done. Screenshot: cockpit/runtimes/mastra/angular/e2e/manual/subagent-card-live.png.

Tallies

  • Lane unit tests (deployments/ag-ui-mastra): 42/42 (8 tee, 24 injector, 10 service).
  • Replay e2e cockpit/runtimes/mastra/angular: 5/5, assertions unchanged.
  • npx nx test website: green.

Caveats

  • Suspended delegations: the eager TOOL_CALL_START is already painted when a child suspends (the bridge would have retracted its buffered copy); the injector closes the card with SUBAGENT_FINISHED {outcome:{type:'suspended'}}. The demo's delegation cannot suspend; the path is unit-tested only.
  • Inner sub-agent tool calls remain out of scope (ignored under tool-output).
  • Follow-up: upstream tool-output mapping patch against the @ag-ui/mastra integration, referencing #2402/#2403 for the eager-START half.

🤖 Generated with Claude Code

blove and others added 7 commits September 2, 2026 11:39
…eam tee

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d-mount finding

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the bridge consumes them

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…CALL_* and attributed child deltas

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tream

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 2, 2026 8:48pm UTC

Request Review

@github-actions github-actions 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.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

…, superseded marker, tee edge tests

- Correct the emitter line-count claim in the blog post and adapter-choice
  docs from a stale "125 to 310" to the measured "190 to 360" (aws-strands
  189, microsoft-agent-framework 309, mastra emitter+tee 287+76=363).
- Mark the wire-capture-subagents.md verdict-summary row for incremental
  child streaming as superseded, pointing at the After streaming section
  that documents the actual (later) behavior.
- Add streaming-tee tests: consumer early break on the wrapped fullStream
  propagates to the source generator's return(); traceId/usage defined as
  prototype getters still resolve through the result proxy.
- Add a subagent-emitter test: an agent-* tool-call chunk with no prior
  start/step-start synthesizes TOOL_CALL_START with the parentMessageId key
  entirely absent, not present-but-undefined.
- Annotate the tee's has() trap as a deliberate restatement of default
  behavior rather than dead code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove enabled auto-merge (squash) September 2, 2026 20:45
@blove
blove merged commit 78bf1a2 into main Sep 2, 2026
31 checks passed
@blove
blove deleted the blove/mastra-subagent-streaming branch September 2, 2026 21:10
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.

1 participant