Skip to content

Changelog

0.8.0 — 2026-09-27

@wait(when=...) — ask only sometimes. The decorator parked every call to the function it decorated, so a threshold ("refunds over 25,000 need finance") had to live outside it: two tools with a model choosing between them, or a router node in front. Both put the rule somewhere you cannot enforce it, and a model deciding whether an approval applies is not a routing mistake, it is an incident.

@tool
@wait(FINANCE, when=lambda order_id, amount: amount > 25_000)
def issue_refund(order_id: str, amount: int) -> str: ...

One tool, one rule, in code. Under the threshold the body runs and nothing is published; over it, the call parks exactly as before. The predicate sees the published args and not the framework's injected context; it is re-evaluated when the run resumes, so it has to be deterministic on the same arguments; and if it raises, the call parks anyway and the exception is logged on agent_wait.wait.

Six lines in the core, so all three frameworks get it at once. No change to Framework, the envelope, the answer shape or any announcer. __agent_wait__ gains "conditional".

0.7.0 — 2026-09-13

Strands Agents. pip install agent-wait[strands] adds agent_wait.strands with the same two names, wait and publish_interrupts, bound to Strands' tool interrupts: @wait on a @tool(context=True) function calls tool_context.interrupt("agent_wait", reason=<packed question>); the run stops with stop_reason == "interrupt"; publish_interrupts reads result.interrupts; question_id is the interrupt id (one per tool call). The answer goes back verbatim through agent([{"interruptResponse": {"interruptId": id, "response": answer}}]) and the wrapper applies its usual rules. A bare tool_context.interrupt(...) from a tool that never heard of the library is published with the default policy. Async mode reads agent.session_id.

No change to the core or the Framework interface. The implementor is 40 lines.

Documented per framework: a session manager is what keeps the parked question alive between the return and the resume; a duplicate answer, an unknown id, or a plain prompt to a parked run all raise and run nothing. Verified against 1.55.1, including resume from a fresh Agent on the same FileSessionManager session.

0.6.0 — 2026-09-12

Pydantic AI. pip install agent-wait[pydantic-ai] adds agent_wait.pydantic_ai with the same two names, wait and publish_interrupts, bound to Pydantic AI's deferred tools: @wait raises ApprovalRequired with the packed question as metadata; the run ends with DeferredToolRequests; publish_interrupts reads it; question_id is the tool_call_id. On the approved re-run the answer arrives through DeferredToolResults.metadata, so a decision parameter and edited args work exactly as on LangGraph; ToolDenied is the framework's own "returned in its place". requires_approval=True tools and CallDeferred calls are published with the default policy. Async mode reads thread_id from deps.

No change to the core, the Framework interface, the envelope or the answer shape. The implementor is 50 lines; the contract test in tests/core did not change.

Documented per framework: Pydantic AI has no checkpointer, so the host stores message_history under thread_id and must store the post-run history before acknowledging an answer — a duplicate is refused only if the stored history already holds the tool's return. The RunContext parameter must be named ctx.

0.5.0 — 2026-09-12

One package. langgraph-wait and agent-wait-aws are folded into agent-wait as subpackages behind extras. pip install agent-wait is the framework-free core and depends on nothing; agent-wait[langgraph] adds agent_wait.langgraph; agent-wait[aws] adds agent_wait.aws. Importing a subpackage without its extra raises an ImportError that names the extra to install.

@hitl is @wait. The decorator makes a function wait for something outside the process; a person is the common case, not the only one — a vendor callback, a payment processor, another agent are the same shape. The rename says what it does; the metadata still says "human-in-the-loop" first, because that is what people search for.

Interface and implementors. @wait and publish_interrupts are now written once in the core against a three-method Framework interface (interrupt, interrupts_in, current_thread_id); agent_wait.langgraph is LangGraphFramework plus two lines of binding. Announcers keep the same split: BaseAnnounce in the core, providers as implementors. A strands extra follows the same pattern. Core exports Framework, make_wait, make_publish_interrupts, question_id_for.

Migration is two lines: from langgraph_wait import hitl, publish_interrupts → from agent_wait.langgraph import wait, publish_interrupts; from agent_wait_aws import … → from agent_wait.aws import …. The envelope, the answer shape, and every behaviour are unchanged; the decorated function's tag is __agent_wait__.

The langgraph-wait and agent-wait-aws distributions are not updated past 0.4.1.

0.4.1 — 2026-09-11

Documentation only, after review. A dedicated Announcers page with every shipped announcer's constructor and behaviour; the configurable decision parameter name shown next to its example; the announcer guide describes one way to write one. No code changes.

0.4.0 — 2026-09-11

@hitl is the only way to declare a question. ask() is gone; the decorator makes the function it sits on interruptible. A function with a decision parameter (name configurable with decision=) always runs and receives the answer; one without runs only on {"action": "approve"}. question is {"function", "args"}; source is {"function"}.

HumanInTheLoopMiddleware support removed. It interrupts before the tool, @hitl inside it; both on one tool is two interrupts. Documented as incompatible. The langchain dev dependency and the policy registry went with it.

Public surface of langgraph-wait is now hitl and publish_interrupts.

0.3.0 — 2026-09-11

Publish only. The WaitPublisher wrapper, pending(), republish(), is_answer() and resume_command() are gone. The library is ask() / @hitl in the graph and publish_interrupts(result, thread_id, announce) after the run — it reads result["__interrupt__"] and nothing else. No graph handle, no get_state().

@hitl. A tool decorator. mode="interrupt" calls ask() before the tool runs; mode="async" publishes from inside the tool through the decorator's own announcers and returns pending without parking the thread. Both register the policy by tool name.

HumanInTheLoopMiddleware (langchain ≥ 1.0) batches are understood: one envelope for the batch, the answer is {"decisions": [...]}.

Schema. PendingInterrupt → Question; interrupt_id → question_id; single transition wait.created; policy gains answer_ttl; envelope gains source. The recommended answer message is documented, not received.

Removed from the receive path entirely. DynamoDbAnnounce writes the row and never reads it. Nothing in the library checks, dedupes or expires an answer — LangGraph ignores a duplicate resume on its own (verified), and the rest is the consumer's.

0.2.1 — 2026-09-11

Documentation only. The README and package description now say what the library is for without underselling it: LangGraph hands an interrupt to whatever called invoke() and stops there — there is no built-in way to get it to anyone else, or for anyone else to answer, regardless of whether the process sticks around. Also adds the Writing an announcer guide. No code changes.

0.2.0 — 2026-09-11

First public release.

The library publishes interrupts and stops. One call — WaitPublisher.invoke() — runs the graph and publishes an envelope for each question it parked on, by diffing the framework's own pending set before and after. pending() reads that set; republish() re-announces it. ask() declares a question and its advisory policy at the interrupt site. There is no store, no inbound handling, no credential, no timer.

Announcers. BaseAnnounce implements the never-raise contract once; subclasses write deliver(). Shipped: WebhookAnnounce (signed, stdlib), LogAnnounce, InMemoryAnnounce, and in agent-wait-aws: SnsAnnounce, SqsAnnounce, EventBridgeAnnounce, DynamoDbAnnounce.

Verified against langgraph 1.2.11, with the observations recorded in reports/langgraph-spike-observations.txt and pinned by tests: interrupt ids are stable across re-entry; tasks[*].interrupts over-reports after a partial parallel resume (task.result is the discriminator); two interrupting tools in one ToolNode share an id.

Coming from 0.1

0.1 (tagged, never published) owned the whole round trip — tokens, a wait store, leases, a scheduler-driven timeout, an inbound dispatch(). All of it was removed.