feat(examples): ag-ui demo emits the protocol's SUBAGENT_* events - #964
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
4 tasks
…>=0.1.22 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The research subgraph opens each assistant turn with a message_start carrying a <tool_call_id>-sub-m<n> message id (SubagentRunState owns the counter), the SubagentStreamHandler forwards each token as a raw delta instead of the accumulated text-so-far, and tool_call / tool_result payloads carry tool_call_id / name / args / content. The research tool body emits an error phase before re-raising. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SubagentEmittingAgent wraps LangGraphAgent.run and expands the graph's subagent_activity CUSTOM events 1:N into SUBAGENT_STARTED/FINISHED/ERROR plus subagentRunId-attributed TEXT_MESSAGE_* and TOOL_CALL_* events (the child's lookup call and result included). It replaces the 1:1 ActivityEmittingAgent/_dispatch_event translator and the ACTIVITY_* convention. Because the child is a streamed subgraph, the bridge also emitted the child's text and tool call unattributed into the parent transcript; the wrapper drops those duplicates inside the delegation window. 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>
…s emit-* metadata; drop the inferred duplicate filter
ag-ui-langgraph streams compiled subgraphs and would emit the research
child's own `lookup` TOOL_CALL_* and answer TEXT_MESSAGE_* as unattributed
events in the parent transcript. The bridge honors a declared opt-out
(`ag_ui_langgraph/agent.py:993-994` reads run metadata `emit-messages` /
`emit-tool-calls`), so the `research` tool now passes
`metadata={"emit-messages": False, "emit-tool-calls": False}` on the
subgraph invocation; callbacks, CUSTOM events and STEP_* still flow.
SubagentEmittingAgent is back to a pure 1:N expander: the delegation
window flag, `_is_child_duplicate` and the remembered-id sets are gone
along with their four tests. New tests: bridge-native events pass through
untouched even inside a delegation; the research tool declares the
opt-out on its invocation config.
Wire doc: "Declared opt-out (review follow-up)" re-capture — zero
unattributed content events inside the SUBAGENT_* window with no
wrapper-side filtering, parent TOOL_CALL_RESULT / answer and STEP_*
present; earlier "wrapper drops duplicates" wording marked superseded.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
blove
force-pushed
the
blove/agui-examples-subagent-events
branch
from
September 2, 2026 21:08
acd93c3 to
267e044
Compare
Contributor
blove
enabled auto-merge (squash)
September 2, 2026 21:10
5 tasks
blove
added a commit
that referenced
this pull request
Sep 2, 2026
…966) * fix(examples): re-export requirements.txt so the Railway image installs ag-ui-protocol 0.1.22 PR #964 bumped pyproject.toml/uv.lock to ag-ui-protocol>=0.1.22 but left requirements.txt (what the Dockerfile actually installs from) pinned to 0.1.19. The new SubagentStartedEvent import doesn't exist in 0.1.19, so the Railway ag-ui-demo container fails at boot, /ok never comes up, and `railway up --detach` silently leaves the old deployment live. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: fail when a Railway lane's requirements.txt drifts from uv.lock requirements.txt is what the Dockerfile actually installs from, but uv sync/ uv.lock is what's kept in sync during normal dependency bumps — nothing previously caught the two falling out of sync (see the prior commit). Add a `uv export --no-hashes` + diff step to the examples/ag-ui e2e job and the per-cap cockpit e2e job (skipped for caps that don't check in a requirements.txt) so drift fails CI with a clear fix-it message instead of surfacing as a silent production boot failure. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.
Second demo in the SUBAGENT_* migration spec, Tasks 4–5 of the plan (the cockpit demo landed in #962; this branch is rebased on top of it):
examples/ag-ui— the richer fork whose research child is a compiled subgraph running a reason →lookuptool → answer loop — now emits the protocol'sSUBAGENT_STARTED/FINISHED/ERRORplussubagentRunId-attributedTEXT_MESSAGE_*andTOOL_CALL_*events instead of the privateACTIVITY_*convention, with per-token deltas. Standalone rule honored: the emitter is a copy (with tool branches), nothing imports across examples.What changed
ag-ui-protocol>=0.1.22(was transitively 0.1.19, pre-Subagent classes);uv lock.src/graph.py,src/streaming/subagent_stream_handler.py):message_start {message_id}once per assistant turn (<toolCallId>-sub-m<n>,SubagentRunStateowns the counter),message {message_id, delta}per raw token (accumulator gone),tool_call {tool_call_id, name, args},tool_result {tool_call_id, content},error {message}on the tool's except path.SubagentEmittingAgent(src/streaming/subagent_emitting_agent.py) wrapsLangGraphAgent.runfor 1:N expansion per the contract;tool_call→ attributedTOOL_CALL_START(parentMessageId= open child message) /ARGS/END;tool_result→ attributedTOOL_CALL_RESULT(role: tool,<id>-result). ReplacesActivityEmittingAgent+activity_transform.py(deleted with its 17 tests). It is a pure expander: every non-subagent_activityevent passes through untouched.src/graph.py, theresearchtool):ag-ui-langgraphstreams compiled subgraphs and would emit the child's ownlookupTOOL_CALL_*and answerTEXT_MESSAGE_*as unattributed events into the parent transcript (baseline §2b). The bridge honors a declared opt-out —ag_ui_langgraph/agent.py:993-994reads LangChain run metadataemit-messages/emit-tool-callsand skips those emissions for runs carrying them asFalse, while callbacks (SubagentStreamHandler,adispatch_custom_event),STEP_*, and the parent's ownTOOL_CALL_RESULT/ answer are untouched — so the tool passesconfig={"callbacks": [...], "metadata": {"emit-messages": False, "emit-tool-calls": False}}onsubgraph.ainvoke, and the metadata inherits into the child run. (Review follow-up: an earlier revision of this PR inferred the duplicates in the wrapper by dropping every unattributed content event inside the delegation window; that filter, its remembered-id sets and its four tests are gone.)examples/ag-ui/python/docs/wire-capture-subagents.md(baseline, after, "Declared opt-out (review follow-up)" re-capture, browser verification); blog2026-08-27-langgraph-subgraphs-when-to-split.mdx:185/:212 and2026-08-31-what-changes-when-the-runtime-changes.mdx:227 now describe the standard events. Adapter docs that describe the legacyACTIVITY_*support are untouched (out of scope per spec).Evidence
uv run pytest -q→ 26 passed (6 handler, 15 emitter incl.test_bridge_native_events_pass_through_untouched_even_inside_a_delegation, 4 emission incl.test_research_tool_declares_the_child_silent_via_bridge_metadata, which drives theresearchtool against a capturing subgraph and assertsmetadata["emit-messages"] is False/metadata["emit-tool-calls"] is Falseon the invocation config).npx playwright test --config examples/ag-ui/angular/e2e/playwright.config.ts→ 37 passed,subagent-card.spec.tsassertions unchanged (≥2 messages,lookupcall + result, child text out of the parent bubble); fixtures unchanged.npx nx test website→ green.TEXT_MESSAGE_*/TOOL_CALL_*betweenSUBAGENT_STARTED(82) andSUBAGENT_FINISHED(942) — thelookupcall appears exactly once (123, attributed) and the child's answer only as 259 attributed deltas; the parent'sTOOL_CALL_RESULT(944) and its own answer (958, 1 START / 49 CONTENT / 1 END, the only unattributedTEXT_MESSAGE_STARTin the run) are present;STEP_*still passes (9/9, the child'sagent/toolsnodes included). The joined child deltas equal the parentTOOL_CALL_RESULT.contentbyte-for-byte.ACTIVITY_DELTAcarrying 306,482 bytes of accumulated text (1,810-char answer) → per-token attributedTEXT_MESSAGE_CONTENTcarrying exactly the answer's bytes; 0 CUSTOM / 0 ACTIVITY on the wire.TOOL_CALL_START9 →TOOL_CALL_END69 →SUBAGENT_STARTED82 → … →SUBAGENT_FINISHED942 →TOOL_CALL_RESULT944 — the block nests between END and RESULT, card never nameless.lookup+ result projected at t≈18.9s, answer turn grows 46 → 1,878 chars over 150ms samples whilerunning, thencomplete+ collapse. Screenshotexamples/ag-ui/angular/e2e/manual/subagent-card-live.png.Deviations / findings
<chat-tool-call-card>viamessage.toolCallIds, whichrouteSubagentContentEventdoes not populate on attributedTOOL_CALL_START— thelookupcall is inagent.subagents()[..].toolCalls(what the e2e asserts) but not drawn inside the card. The wire carriesparentMessageId, so the fix is reducer-side.search_documents); the capture prompt asks for the deep-dive explicitly. Replay is unaffected.llm.astreaminside the tool body, not a compiled subgraph, so the bridge never streams it: the after-emitter capture incockpit/ag-ui/subagents/python/docs/wire-capture-subagents.mdshows only attributed events betweenSUBAGENT_STARTED(306) andSUBAGENT_FINISHED(1295) and exactly one unattributedTEXT_MESSAGE_STARTin the run (3896, the orchestrator). No change needed there.🤖 Generated with Claude Code