feat(runtimes): stand-down guard so the mastra subagent injector retires when upstream lands - #971
Merged
Merged
Conversation
The injector exists only because @ag-ui/mastra 1.1.2 drops the child stream (`case "tool-output": break`). Upstream is expected to grow its own sub-agent surface — @ag-ui/core 0.0.59 already ships the SUBAGENT_* schemas and the LangGraph integration already emits them behind `subagent_visibility`. Once the installed bridge emits SUBAGENT_* too, injecting alongside it puts duplicates on the wire: a duplicate SUBAGENT_STARTED for one subagentRunId is a hard AG-UI verifier error, and two distinct ids paint two cards for one delegation. The first bridge SUBAGENT_* now latches `bridgeEmitsSubagentEvents`, after which `chunk()` injects nothing and `eventsFor()` is a passthrough. Detection is sound because `chunk()` output is written straight to the socket and never re-enters `eventsFor()`, so a SUBAGENT_* seen there is always the bridge's. Two details that carry the design: - The latch is process-wide, not per-injector. `createSubagentInjector()` is called per request, so a per-run flag would re-learn every run and duplicate the first delegation of each one. - Standing down mid-delegation closes what we already announced, or RUN_FINISHED trips the verifier's "subagents are still active" rule. The close is neutral (SUBAGENT_FINISHED with no `outcome`, which the schema allows) because the delegation neither succeeded nor failed. `synthesized` is kept so the bridge's buffered TOOL_CALL_* copies still dedupe against the eager ones already on the wire. Known limit, documented in the module header: the tee sees a chunk before the bridge does, so the delegation in flight at detection already has our SUBAGENT_STARTED on the wire and cannot be retracted. Only the first delegation of the first run after an upgrade is affected. The clean exit remains deleting this module and the tee once upstream lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove
enabled auto-merge (squash)
September 3, 2026 00:01
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Contributor
Contributor
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
deployments/ag-ui-mastra/subagent-emitter.mjsexists only because@ag-ui/mastra1.1.2 discards the sub-agent child stream (case "tool-output": break). That is not a defect — the comment's stated reason ("there is no AG-UI mapping for interim output") was true when written.It is no longer true.
@ag-ui/core0.0.59 ships theSUBAGENT_*schemas, and the LangGraph integration already emits them behindsubagent_visibility(inline/attributed/hidden). Mastra is not covered by the maintainer's open sub-agent PRs (#2137, #2351) yet, but it is a matter of time.The day the installed bridge emits
SUBAGENT_*itself, injecting alongside it puts duplicates on the wire:SUBAGENT_STARTEDfor onesubagentRunIdis a hard AG-UI verifier error (verify.tsrejects it for the whole run), andThis PR makes that upgrade a non-event instead of an outage.
What
The first bridge
SUBAGENT_STARTED/SUBAGENT_FINISHED/SUBAGENT_ERRORlatchesbridgeEmitsSubagentEvents, after whichchunk()injects nothing andeventsFor()is a passthrough.Detection is sound by construction:
chunk()output is written straight to the SSE socket and never re-enterseventsFor(), so aSUBAGENT_*observed there is always the bridge's, never our own echo.Two details carry the design:
The latch is process-wide, not per-injector.
createSubagentInjector()is called per request, so a per-run flag would re-learn on every run and duplicate the first delegation of each one. HencecreateBridgeCapability(), created once inserver.mjsand passed to every run.Standing down mid-delegation closes what we already announced. Leaving our subagent open would trip the verifier's "subagents are still active" rule at
RUN_FINISHED. The close is deliberately neutral —SUBAGENT_FINISHEDwith nooutcome, which the schema allows — because the delegation neither succeeded nor failed; we simply stopped owning it.synthesizedis kept so the bridge's bufferedTOOL_CALL_*copies still dedupe against the eager ones already on the wire.Known limit (documented in the module header)
The tee observes a chunk before the bridge does, so the delegation already in flight at the moment of detection has our
SUBAGENT_STARTEDon the wire already and cannot be retracted — it is closed neutrally instead. Only that first delegation of the first run after an upgrade is affected; the latch is process-wide, so every run after starts retired.This converts "breaks in production" into "one cosmetic blip, then a silent no-op". It is a safety net, not a substitute for the real exit: delete the emitter and the tee once upstream lands.
Tests
TDD — the 5 behavioral tests were watched failing before implementation; the other 3 are regression guards on behavior the change must not break (notably that the
synthesizeddedupe survives stand-down, and that independent capabilities don't leak, which would fail had I used a module-level global).npm testindeployments/ag-ui-mastra: 53 passing, 0 failing across all three suites.🤖 Generated with Claude Code