diff --git a/CHANGELOG.md b/CHANGELOG.md index d003e07..69e1752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.6.24 - 2026-07-25 + +### Fixed +- **A plain-string `interrupt("Approve deleting X?")` — the canonical LangGraph HITL form — now + shows the human what they're approving, instead of "(no action details provided)" (gh #95).** + The approval menu dropped the payload of a bare-string/scalar interrupt (dict payloads rendered + fine), so a confirmation like `interrupt("Approve deleting ALL files in /home?")` asked the user + to Approve/Reject blind — defeating the point of HITL. The renderer already handled a scalar + (`format_interrupt_request` returns `str(action)`); the payload was dropped upstream in + `langstage-core`'s `on_interrupt` handler, which `json.loads()`'d the string, failed on a + non-JSON question, and fell back to an empty request. Fixed in **langstage-core 1.0.28** on the + chunk wire the CLI consumes; this raises the floor to `>= 1.0.28` so a fresh install gets it. + ## 0.6.23 - 2026-07-23 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index f18a83f..672b2e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "langstage-cli" -version = "0.6.23" +version = "0.6.24" description = "The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph" readme = "README.md" requires-python = ">=3.11" @@ -31,7 +31,7 @@ dependencies = [ # is a HARD dep. A bare `pip install langstage-cli` must be able to run a turn. # >=1.0.6 for the shared preflight primitive core.verify() used by --verify. # >=1.0.14 for describe(configurable=) — the single complete config diagnostic. - "langstage-core[agui]>=1.0.24", + "langstage-core[agui]>=1.0.28", "click>=8.0.0", "python-dotenv", ] diff --git a/tests/test_agui_stream.py b/tests/test_agui_stream.py index 022feb6..d110db4 100644 --- a/tests/test_agui_stream.py +++ b/tests/test_agui_stream.py @@ -201,3 +201,34 @@ def test_non_streaming_tool_agent_surfaces_tool_call_and_result(): assert results and results[0]["tool_result"] == "Sunny, 24C", ( "tool result dropped on the CLI snapshot path (needs langstage-core >= 1.0.24, gh #91)" ) + + +def test_plain_string_interrupt_reaches_the_renderer_with_its_payload(): + """gh #95: a bare-string interrupt("Approve X?") must surface its text to the CLI, + not "(no action details provided)". The fix is in langstage-core (>= 1.0.28, on the + chunk wire the CLI consumes); this pins that the frame carries the string and the + CLI renderer shows it.""" + from langgraph.checkpoint.memory import MemorySaver + from langgraph.types import interrupt + + from langstage_cli.cli import format_interrupt_request + + def ask(state): + interrupt("Approve deleting ALL files in /home? (y/n)") + return {"messages": [AIMessage(content="done")]} + + b = StateGraph(MessagesState) + b.add_node("ask", ask) + b.add_edge(START, "ask") + b.add_edge("ask", END) + agent = build_session_agent(b.compile(checkpointer=MemorySaver())) + + chunks = _collect(agent, "go") + interrupts = [c for c in chunks if c.get("status") == "interrupt"] + assert interrupts, chunks + ars = interrupts[0]["interrupt"]["action_requests"] + assert ars == ["Approve deleting ALL files in /home? (y/n)"], ( + "string interrupt dropped (needs core >= 1.0.28, gh #95)" + ) + # the renderer surfaces the string (not "(no action details provided)") + assert format_interrupt_request(ars[0])[0] == "Approve deleting ALL files in /home? (y/n)"