feat(strands): bridge native Strands interrupts to AG-UI interrupt round-trip - #2286
feat(strands): bridge native Strands interrupts to AG-UI interrupt round-trip#2286ciolo wants to merge 6 commits into
Conversation
…und-trip
Translate native Strands `tool_context.interrupt()` pauses into the AG-UI
interrupt lifecycle so human-in-the-loop flows work over the protocol:
- Consume `RunAgentInput.resume`, building the Strands resume prompt
`[{"interruptResponse": {"interruptId", "response"}}]`; `status="cancelled"`
resumes with the `INTERRUPT_CANCELLED` denial sentinel.
- Detect a paused run after the stream loop via the terminal `AgentResult`
(`stop_reason == "interrupt"`), falling back to the agent's
`_interrupt_state` when the result event is consumed by the early-break path.
- Emit `RunFinishedInterruptOutcome` with one AG-UI `Interrupt` per Strands
interrupt (name -> categorical `reason`, original reason under
`metadata.strands_reason`); non-interrupt runs still finish bare.
- Bump `ag-ui-protocol` floor to >=0.1.19 (ships the interrupt types).
- Document the HITL round-trip and the durable `SessionManager` requirement
for stateless deployments; add unit tests for all four behaviors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
269a418 to
062cb45
Compare
|
hi @contextablemark and @ranst91 ! Apologies for direct tagging. |
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen install the packages you need: # Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1785893517' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1785893517' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1785893517' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1785893517' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1785893517' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1785893517' --index testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1785893517
Commit: 7b4c8f2 |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/claude-managed-agents
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
|
Thanks for this, and apologies for the slow review — worth explaining why, because you've walked into a genuinely crowded area rather than a quiet one. Context: four changes are currently converging on
We've verified yours is orthogonal to the halt work: cherry-picking our commits onto your head applies with zero conflicts, the union suite is green, and your feature behaves identically with and without them. So this isn't blocked on the others — but it does explain the delay, and it's why the review below is unusually specific about which code path each finding lives on. Separately, the Now the review. Credit where it's due first: this avoids three traps that are genuinely hard to know about, and we only found them by instrumenting Strands directly.
The Three blocking issues1. A falsy resume payload never resolves. This is the default path, not an edge case: 2. The resume branch should be additive, not exclusive. The comment at 3. Non-blocking4. Populate 5. Add an escape from an abandoned interrupt. A next turn with no 6. One test that drives a real 7. Three small ones. (a) Move the 8. Two things for the README: the interrupted tool's body re-executes on resume (measured), so anything before Happy to pair on 1–3 if useful, and the dojo cc @leotac, since you asked about adoption timing. |
Co-authored-by: Francesco De Felice <francesco.de.felice@verizonconnect.com>
Fixes #2205
Summary
Bridges native Strands
tool_context.interrupt()pauses to the AG-UI interrupt lifecycle, so human-in-the-loop flows work over the protocol. Previously a native interrupt either surfaced asRUN_ERRORor silently finished as a completed run, and a client'sresumepayload was dropped.This is a wrapper-only change in
integrations/aws-strands/python/— the AG-UI protocol types it relies on already ship inag-ui-protocol0.1.19.I filed #2205 ~2 weeks ago and pinged
#-💎-contributingon Discord; opening this PR to move the discussion forward. Happy to adjust scope or approach.Changes
RunAgentInput.resume— builds the Strands resume prompt[{"interruptResponse": {"interruptId", "response"}}]and drivesstream_asyncwith it (takes precedence over other stream paths, since a resume run carries no fresh prompt).status="cancelled"resumes with the documented denial sentinelINTERRUPT_CANCELLED = {"cancelled": True}.AgentResult(stop_reason == "interrupt"), captured before thecomplete/force_stopearly-break so it's never dropped; falls back to the agent's_interrupt_state.activatedif the result event was consumed.RunFinishedInterruptOutcome— one AG-UIInterruptper Strands interrupt. The Strands interrupt name maps to the categorical AG-UIreason; the free-form Strands reason object is preserved undermetadata.strands_reason. Non-interrupt runs still finish bare (no behavior change).ag-ui-protocolfloor to>=0.1.19(ships the interrupt types) and re-lock.SessionManagerrequirement for stateless / multi-container deployments (the in-memory per-thread cache only preserves interrupt state within one process).Test plan
tests/test_interrupt.py(6 cases): pause → interrupt outcome, state-fallback detection, no-interrupt-finishes-bare, resolved-resume prompt shape, cancelled sentinel, multi-entry resume.uv run pytest tests/→ 182 passed, 2 skipped.human_in_the_loopexample not included in this PR — happy to add if maintainers want it.🤖 Generated with Claude Code