agent-wait 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,10 +10,10 @@ __pycache__/
10
10
  build/
11
11
  cdk.out/
12
12
  reports/*.log
13
-
14
- # generated by the docs workflow from README.md / CHANGELOG.md
15
- docs/index.md
16
- docs/changelog.md
17
- # build output
18
- dist/
19
- site/
13
+
14
+ # generated by the docs workflow from README.md / CHANGELOG.md
15
+ docs/index.md
16
+ docs/changelog.md
17
+ # build output
18
+ dist/
19
+ site/
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Kamaljeet Singh
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kamaljeet Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-wait
3
+ Version: 0.3.0
4
+ Summary: Get a LangGraph interrupt out of the process -- to a topic, a queue, a webhook, a table -- so anyone can answer it, and the answer back in.
5
+ Project-URL: Homepage, https://skamalj.github.io/agent-wait/
6
+ Project-URL: Documentation, https://skamalj.github.io/agent-wait/
7
+ Project-URL: Source, https://github.com/skamalj/agent-wait
8
+ Project-URL: Issues, https://github.com/skamalj/agent-wait/issues
9
+ Project-URL: Changelog, https://github.com/skamalj/agent-wait/blob/main/CHANGELOG.md
10
+ Author-email: Kamaljeet Singh <skamalj@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,approval,durable,human-in-the-loop,interrupt,langgraph,serverless
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Description-Content-Type: text/markdown
24
+
25
+ # agent-wait
26
+
27
+ [![PyPI](https://img.shields.io/pypi/v/agent-wait.svg)](https://pypi.org/project/agent-wait/)
28
+ [![CI](https://github.com/skamalj/agent-wait/actions/workflows/ci.yml/badge.svg)](https://github.com/skamalj/agent-wait/actions/workflows/ci.yml)
29
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/skamalj/agent-wait/blob/main/LICENSE)
30
+
31
+ **Get a LangGraph interrupt out of the process, and the answer back in.**
32
+
33
+ When a node calls `interrupt()`, the graph pauses and the interrupt is handed to whatever
34
+ called `invoke()` — and that is where LangGraph stops. There is no built-in way to tell
35
+ anyone *else* that a question was asked, and no built-in way for anyone else to answer
36
+ it. The moment the question has to reach a person on Slack, an approvals dashboard, a
37
+ ticket queue or another service, you are writing that code yourself — whether your agent
38
+ is a server that runs for a year or a Lambda that is gone in seconds.
39
+
40
+ agent-wait is that code. It takes the interrupt and puts it somewhere people can see it —
41
+ a topic, a queue, a webhook, a database row — with everything needed to answer it in one
42
+ envelope, and documents the shape of the answer so the return leg is one `if`.
43
+
44
+ ```bash
45
+ pip install agent-wait langgraph-wait # core + LangGraph
46
+ pip install agent-wait-aws # SNS / SQS / EventBridge / DynamoDB announcers
47
+ ```
48
+
49
+ ## The whole thing
50
+
51
+ **In the graph** — one line, where the decision belongs:
52
+
53
+ ```python
54
+ from agent_wait import WaitPolicy
55
+ from langgraph_wait import ask
56
+
57
+
58
+ def review(state):
59
+ decision = ask(
60
+ {"kind": "refund_approval", "order_id": state["order_id"], "amount": state["amount"]},
61
+ policy=WaitPolicy(
62
+ timeout="P3D",
63
+ default={"action": "reject"},
64
+ allowed_actions=("approve", "reject"),
65
+ tags={"approver_group": "finance"},
66
+ ),
67
+ )
68
+ return {"decision": decision} # exactly what the approver sent
69
+ ```
70
+
71
+ `ask()` is a thin wrapper over `interrupt()`; the policy rides along inside the
72
+ interrupt value. A plain `interrupt(value)` works too, with the default policy.
73
+
74
+ **After the run** — one call:
75
+
76
+ ```python
77
+ from agent_wait_aws import SnsAnnounce
78
+ from langgraph_wait import publish_interrupts
79
+
80
+ result = graph.invoke(value, {"configurable": {"thread_id": thread_id}})
81
+ publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
82
+ ```
83
+
84
+ It reads `result["__interrupt__"]` — `Interrupt.id` and `Interrupt.value`, nothing else —
85
+ and hands one envelope per question to the announcers. No graph handle, no `get_state()`,
86
+ no checkpointer. If nothing interrupted, it does nothing.
87
+
88
+ **When the answer comes back** — the one `if` you write:
89
+
90
+ ```python
91
+ from langgraph.types import Command
92
+
93
+ if "question_id" in message: # an answer
94
+ value = Command(resume={message["question_id"]: message["answer"]})
95
+ else: # a start
96
+ value = message["input"]
97
+
98
+ result = graph.invoke(value, {"configurable": {"thread_id": message["thread_id"]}})
99
+ publish_interrupts(result, message["thread_id"], announce)
100
+ ```
101
+
102
+ That is the complete integration. A duplicate answer runs nothing — LangGraph ignores a
103
+ resume for a question the thread has moved past — so there is nothing to check.
104
+
105
+ ## Or tag the tool
106
+
107
+ ```python
108
+ from langgraph_wait import hitl
109
+
110
+
111
+ @tool
112
+ @hitl(WaitPolicy(timeout="P1D", default={"action": "reject"}, tags={"approver_group": "finance"}))
113
+ def issue_refund(order_id: str, amount: int) -> str: ...
114
+ ```
115
+
116
+ `@hitl` calls `ask()` with `{tool, args}` before the tool runs. `{"action": "approve"}`
117
+ runs it (optionally with edited `args`); anything else is returned to the model as the
118
+ tool's result. It also registers the policy by tool name, so questions raised by
119
+ LangChain's `HumanInTheLoopMiddleware` — which batches several tool calls into one
120
+ interrupt — are published with it.
121
+
122
+ **Async mode — the thread does not park:**
123
+
124
+ ```python
125
+ @tool
126
+ @hitl(FINANCE, mode="async", announce=[SnsAnnounce(topic_arn), DynamoDbAnnounce(table)])
127
+ def issue_refund(order_id: str, amount: int) -> str: ...
128
+ ```
129
+
130
+ The decorator *is* the publisher: the tool call announces the question and returns
131
+ `{"status": "pending_approval", "question_id": ...}` without running. The graph carries
132
+ on; nothing is parked; nothing to call after the run. The decision arrives later as a new
133
+ message and your graph acts on it. This is the mode for a single-thread channel like
134
+ WhatsApp, where the approver is not the person on the thread.
135
+
136
+ ## What goes out
137
+
138
+ ```json
139
+ {
140
+ "type": "wait.created",
141
+ "thread_id": "order-4471",
142
+ "question_id": "a1b2c3d4e5f60718",
143
+ "question": { "kind": "refund_approval", "amount": 41000 },
144
+ "allowed_actions": ["approve", "reject"],
145
+ "expires_at": "2026-09-14T09:00:00Z",
146
+ "default": { "action": "reject" },
147
+ "source": { "tool": "issue_refund" },
148
+ "reply_with": { "thread_id": "order-4471", "question_id": "a1b2c3d4e5f60718", "answer": null }
149
+ }
150
+ ```
151
+
152
+ `reply_with` is a filled-in stub: copy it, set `answer`, send it back. Whatever goes in
153
+ `answer` is what `ask()` returns — verbatim.
154
+
155
+ Full schema, the recommended answer shape, and how to deduplicate:
156
+ [Message formats](https://skamalj.github.io/agent-wait/message-formats/).
157
+
158
+ ## Announcers
159
+
160
+ Subclass `BaseAnnounce`, write one method:
161
+
162
+ ```python
163
+ from agent_wait import BaseAnnounce
164
+
165
+
166
+ class RedisAnnounce(BaseAnnounce):
167
+ name = "redis"
168
+
169
+ def __init__(self, client, **kw):
170
+ super().__init__(**kw)
171
+ self.client = client
172
+
173
+ def deliver(self, envelope, transition):
174
+ self.client.set(envelope.dedupe_key, envelope.to_json())
175
+ ```
176
+
177
+ A raise inside `deliver()` becomes a log line; the run completes. Shipped:
178
+
179
+ | Adapter | Package | Where the question lands |
180
+ |---|---|---|
181
+ | `WebhookAnnounce` | `agent-wait` | A URL. JSON POST, optional HMAC-SHA256 signature. Stdlib only. |
182
+ | `LogAnnounce` | `agent-wait` | A structured log line. The question never reaches INFO. |
183
+ | `InMemoryAnnounce` | `agent-wait` | A list. For tests. |
184
+ | `SnsAnnounce` | `agent-wait-aws` | A topic; `tags` become message attributes for subscription filters. |
185
+ | `SqsAnnounce` | `agent-wait-aws` | A queue; on FIFO, grouped by thread, deduplicated on the stable key. |
186
+ | `EventBridgeAnnounce` | `agent-wait-aws` | A bus. Notices partial failures behind a 200. |
187
+ | `DynamoDbAnnounce` | `agent-wait-aws` | **A row**, `status=open`, with a GSI an approvals UI can query. Write-only. |
188
+
189
+ [Writing an announcer](https://skamalj.github.io/agent-wait/writing-an-announcer/).
190
+
191
+ ## What the library does *not* do
192
+
193
+ - **Receive answers.** No endpoint, no validation, no ledger reads. The `if` above is yours.
194
+ - **Enforce anything.** `expires_at`, `default`, `answer_ttl`, `allowed_actions` are
195
+ published so the consumer has the asker's intent. Acting on them is the consumer's.
196
+ - **Store anything.** LangGraph's checkpoint is the only state in interrupt mode; in
197
+ async mode there is none unless you keep one.
198
+
199
+ ## One LangGraph 1.2.x behaviour to know
200
+
201
+ Two tools that each call `interrupt()`, dispatched by one `ToolNode`, get the **same
202
+ interrupt id** ([#6626](https://github.com/langchain-ai/langgraph/issues/6626)), and only
203
+ one surfaces per run ([#6624](https://github.com/langchain-ai/langgraph/issues/6624)).
204
+ A different question under an identical id defeats deduplication. Use one interrupting
205
+ tool per node, or `HumanInTheLoopMiddleware`, which batches them into one interrupt.
206
+ Pinned by a test that fails if LangGraph changes it.
207
+
208
+ ## Layout
209
+
210
+ ```
211
+ packages/agent-wait core: Question, WaitPolicy, the envelope, publish(), announcers. No deps.
212
+ packages/langgraph-wait ask(), @hitl, publish_interrupts(). The only LangGraph import.
213
+ packages/agent-wait-aws four announce adapters, and a CDK stack for the example.
214
+ examples/refund_agent a graph and a host, deployed to Lambda behind SQS.
215
+ docs/ message contract, architecture, announcer guide, consumer guide.
216
+ ```
217
+
218
+ MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
@@ -0,0 +1,194 @@
1
+ # agent-wait
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/agent-wait.svg)](https://pypi.org/project/agent-wait/)
4
+ [![CI](https://github.com/skamalj/agent-wait/actions/workflows/ci.yml/badge.svg)](https://github.com/skamalj/agent-wait/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/skamalj/agent-wait/blob/main/LICENSE)
6
+
7
+ **Get a LangGraph interrupt out of the process, and the answer back in.**
8
+
9
+ When a node calls `interrupt()`, the graph pauses and the interrupt is handed to whatever
10
+ called `invoke()` — and that is where LangGraph stops. There is no built-in way to tell
11
+ anyone *else* that a question was asked, and no built-in way for anyone else to answer
12
+ it. The moment the question has to reach a person on Slack, an approvals dashboard, a
13
+ ticket queue or another service, you are writing that code yourself — whether your agent
14
+ is a server that runs for a year or a Lambda that is gone in seconds.
15
+
16
+ agent-wait is that code. It takes the interrupt and puts it somewhere people can see it —
17
+ a topic, a queue, a webhook, a database row — with everything needed to answer it in one
18
+ envelope, and documents the shape of the answer so the return leg is one `if`.
19
+
20
+ ```bash
21
+ pip install agent-wait langgraph-wait # core + LangGraph
22
+ pip install agent-wait-aws # SNS / SQS / EventBridge / DynamoDB announcers
23
+ ```
24
+
25
+ ## The whole thing
26
+
27
+ **In the graph** — one line, where the decision belongs:
28
+
29
+ ```python
30
+ from agent_wait import WaitPolicy
31
+ from langgraph_wait import ask
32
+
33
+
34
+ def review(state):
35
+ decision = ask(
36
+ {"kind": "refund_approval", "order_id": state["order_id"], "amount": state["amount"]},
37
+ policy=WaitPolicy(
38
+ timeout="P3D",
39
+ default={"action": "reject"},
40
+ allowed_actions=("approve", "reject"),
41
+ tags={"approver_group": "finance"},
42
+ ),
43
+ )
44
+ return {"decision": decision} # exactly what the approver sent
45
+ ```
46
+
47
+ `ask()` is a thin wrapper over `interrupt()`; the policy rides along inside the
48
+ interrupt value. A plain `interrupt(value)` works too, with the default policy.
49
+
50
+ **After the run** — one call:
51
+
52
+ ```python
53
+ from agent_wait_aws import SnsAnnounce
54
+ from langgraph_wait import publish_interrupts
55
+
56
+ result = graph.invoke(value, {"configurable": {"thread_id": thread_id}})
57
+ publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
58
+ ```
59
+
60
+ It reads `result["__interrupt__"]` — `Interrupt.id` and `Interrupt.value`, nothing else —
61
+ and hands one envelope per question to the announcers. No graph handle, no `get_state()`,
62
+ no checkpointer. If nothing interrupted, it does nothing.
63
+
64
+ **When the answer comes back** — the one `if` you write:
65
+
66
+ ```python
67
+ from langgraph.types import Command
68
+
69
+ if "question_id" in message: # an answer
70
+ value = Command(resume={message["question_id"]: message["answer"]})
71
+ else: # a start
72
+ value = message["input"]
73
+
74
+ result = graph.invoke(value, {"configurable": {"thread_id": message["thread_id"]}})
75
+ publish_interrupts(result, message["thread_id"], announce)
76
+ ```
77
+
78
+ That is the complete integration. A duplicate answer runs nothing — LangGraph ignores a
79
+ resume for a question the thread has moved past — so there is nothing to check.
80
+
81
+ ## Or tag the tool
82
+
83
+ ```python
84
+ from langgraph_wait import hitl
85
+
86
+
87
+ @tool
88
+ @hitl(WaitPolicy(timeout="P1D", default={"action": "reject"}, tags={"approver_group": "finance"}))
89
+ def issue_refund(order_id: str, amount: int) -> str: ...
90
+ ```
91
+
92
+ `@hitl` calls `ask()` with `{tool, args}` before the tool runs. `{"action": "approve"}`
93
+ runs it (optionally with edited `args`); anything else is returned to the model as the
94
+ tool's result. It also registers the policy by tool name, so questions raised by
95
+ LangChain's `HumanInTheLoopMiddleware` — which batches several tool calls into one
96
+ interrupt — are published with it.
97
+
98
+ **Async mode — the thread does not park:**
99
+
100
+ ```python
101
+ @tool
102
+ @hitl(FINANCE, mode="async", announce=[SnsAnnounce(topic_arn), DynamoDbAnnounce(table)])
103
+ def issue_refund(order_id: str, amount: int) -> str: ...
104
+ ```
105
+
106
+ The decorator *is* the publisher: the tool call announces the question and returns
107
+ `{"status": "pending_approval", "question_id": ...}` without running. The graph carries
108
+ on; nothing is parked; nothing to call after the run. The decision arrives later as a new
109
+ message and your graph acts on it. This is the mode for a single-thread channel like
110
+ WhatsApp, where the approver is not the person on the thread.
111
+
112
+ ## What goes out
113
+
114
+ ```json
115
+ {
116
+ "type": "wait.created",
117
+ "thread_id": "order-4471",
118
+ "question_id": "a1b2c3d4e5f60718",
119
+ "question": { "kind": "refund_approval", "amount": 41000 },
120
+ "allowed_actions": ["approve", "reject"],
121
+ "expires_at": "2026-09-14T09:00:00Z",
122
+ "default": { "action": "reject" },
123
+ "source": { "tool": "issue_refund" },
124
+ "reply_with": { "thread_id": "order-4471", "question_id": "a1b2c3d4e5f60718", "answer": null }
125
+ }
126
+ ```
127
+
128
+ `reply_with` is a filled-in stub: copy it, set `answer`, send it back. Whatever goes in
129
+ `answer` is what `ask()` returns — verbatim.
130
+
131
+ Full schema, the recommended answer shape, and how to deduplicate:
132
+ [Message formats](https://skamalj.github.io/agent-wait/message-formats/).
133
+
134
+ ## Announcers
135
+
136
+ Subclass `BaseAnnounce`, write one method:
137
+
138
+ ```python
139
+ from agent_wait import BaseAnnounce
140
+
141
+
142
+ class RedisAnnounce(BaseAnnounce):
143
+ name = "redis"
144
+
145
+ def __init__(self, client, **kw):
146
+ super().__init__(**kw)
147
+ self.client = client
148
+
149
+ def deliver(self, envelope, transition):
150
+ self.client.set(envelope.dedupe_key, envelope.to_json())
151
+ ```
152
+
153
+ A raise inside `deliver()` becomes a log line; the run completes. Shipped:
154
+
155
+ | Adapter | Package | Where the question lands |
156
+ |---|---|---|
157
+ | `WebhookAnnounce` | `agent-wait` | A URL. JSON POST, optional HMAC-SHA256 signature. Stdlib only. |
158
+ | `LogAnnounce` | `agent-wait` | A structured log line. The question never reaches INFO. |
159
+ | `InMemoryAnnounce` | `agent-wait` | A list. For tests. |
160
+ | `SnsAnnounce` | `agent-wait-aws` | A topic; `tags` become message attributes for subscription filters. |
161
+ | `SqsAnnounce` | `agent-wait-aws` | A queue; on FIFO, grouped by thread, deduplicated on the stable key. |
162
+ | `EventBridgeAnnounce` | `agent-wait-aws` | A bus. Notices partial failures behind a 200. |
163
+ | `DynamoDbAnnounce` | `agent-wait-aws` | **A row**, `status=open`, with a GSI an approvals UI can query. Write-only. |
164
+
165
+ [Writing an announcer](https://skamalj.github.io/agent-wait/writing-an-announcer/).
166
+
167
+ ## What the library does *not* do
168
+
169
+ - **Receive answers.** No endpoint, no validation, no ledger reads. The `if` above is yours.
170
+ - **Enforce anything.** `expires_at`, `default`, `answer_ttl`, `allowed_actions` are
171
+ published so the consumer has the asker's intent. Acting on them is the consumer's.
172
+ - **Store anything.** LangGraph's checkpoint is the only state in interrupt mode; in
173
+ async mode there is none unless you keep one.
174
+
175
+ ## One LangGraph 1.2.x behaviour to know
176
+
177
+ Two tools that each call `interrupt()`, dispatched by one `ToolNode`, get the **same
178
+ interrupt id** ([#6626](https://github.com/langchain-ai/langgraph/issues/6626)), and only
179
+ one surfaces per run ([#6624](https://github.com/langchain-ai/langgraph/issues/6624)).
180
+ A different question under an identical id defeats deduplication. Use one interrupting
181
+ tool per node, or `HumanInTheLoopMiddleware`, which batches them into one interrupt.
182
+ Pinned by a test that fails if LangGraph changes it.
183
+
184
+ ## Layout
185
+
186
+ ```
187
+ packages/agent-wait core: Question, WaitPolicy, the envelope, publish(), announcers. No deps.
188
+ packages/langgraph-wait ask(), @hitl, publish_interrupts(). The only LangGraph import.
189
+ packages/agent-wait-aws four announce adapters, and a CDK stack for the example.
190
+ examples/refund_agent a graph and a host, deployed to Lambda behind SQS.
191
+ docs/ message contract, architecture, announcer guide, consumer guide.
192
+ ```
193
+
194
+ MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "agent-wait"
3
- version = "0.2.0"
4
- description = "Publish a LangGraph agent's interrupts to the outside world so a human can answer them -- from a queue, a Lambda, anywhere the process does not stick around."
3
+ version = "0.3.0"
4
+ description = "Get a LangGraph interrupt out of the process -- to a topic, a queue, a webhook, a table -- so anyone can answer it, and the answer back in."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  license = "MIT"
@@ -1,81 +1,78 @@
1
- """agent-wait: publish an agent's interrupts to the outside world.
2
-
3
- A graph node asks a question and pauses:
4
-
5
- from langgraph_wait import ask
6
-
7
- decision = ask({"kind": "refund_approval", "amount": amount},
8
- policy=WaitPolicy(timeout="P3D", allowed_actions=("approve", "reject")))
9
-
10
- The host runs the graph through a publisher, and whatever the graph parked on goes out
11
- to wherever people can see it:
12
-
13
- agent = WaitPublisher(LangGraphAdapter(graph), announce=[SnsAnnounce(topic_arn)])
14
-
15
- agent.invoke(payload, thread_id)
16
-
17
- That is the whole library. It publishes questions and it reports what a thread is parked
18
- on. It does not receive answers, hold state, mint credentials or run timers -- see
19
- `docs/migrating-from-0.1.md` for what that means if you are coming from v0.1.
20
- """
21
-
22
- from .announce import (
23
- AnnounceAdapter,
24
- BaseAnnounce,
25
- CompositeAnnounce,
26
- FailingAnnounce,
27
- InMemoryAnnounce,
28
- LogAnnounce,
29
- WebhookAnnounce,
30
- verify_signature,
31
- )
32
- from .errors import PolicyError, QuestionTooLarge, WaitError
33
- from .model import (
34
- MAX_QUESTION_BYTES,
35
- Clock,
36
- EntryPoint,
37
- FakeClock,
38
- PendingInterrupt,
39
- SystemClock,
40
- Transition,
41
- WaitEnvelope,
42
- canonical_json,
43
- check_question_size,
44
- iso,
45
- new_ulid,
46
- )
47
- from .policy import WaitPolicy, parse_duration
48
- from .publisher import FrameworkAdapter, WaitPublisher
49
-
50
- __version__ = "0.2.0"
51
-
52
- __all__ = [
53
- "MAX_QUESTION_BYTES",
54
- "AnnounceAdapter",
55
- "BaseAnnounce",
56
- "Clock",
57
- "CompositeAnnounce",
58
- "EntryPoint",
59
- "FailingAnnounce",
60
- "FakeClock",
61
- "FrameworkAdapter",
62
- "InMemoryAnnounce",
63
- "LogAnnounce",
64
- "PendingInterrupt",
65
- "PolicyError",
66
- "QuestionTooLarge",
67
- "SystemClock",
68
- "Transition",
69
- "WaitEnvelope",
70
- "WaitError",
71
- "WaitPolicy",
72
- "WaitPublisher",
73
- "WebhookAnnounce",
74
- "__version__",
75
- "canonical_json",
76
- "check_question_size",
77
- "iso",
78
- "new_ulid",
79
- "parse_duration",
80
- "verify_signature",
81
- ]
1
+ """agent-wait: announce an agent's interrupts to the outside world.
2
+
3
+ A graph node asks a question and pauses:
4
+
5
+ from langgraph_wait import ask
6
+
7
+ decision = ask({"kind": "refund_approval", "amount": amount},
8
+ policy=WaitPolicy(timeout="P3D", allowed_actions=("approve", "reject")))
9
+
10
+ After the run, the host announces whatever it parked on:
11
+
12
+ result = graph.invoke(value, config)
13
+ publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
14
+
15
+ That is the whole library. It builds one envelope per interrupt and hands it to the
16
+ announcers. It does not run the graph, receive answers, hold state or run timers.
17
+ """
18
+
19
+ from .announce import (
20
+ AnnounceAdapter,
21
+ BaseAnnounce,
22
+ CompositeAnnounce,
23
+ FailingAnnounce,
24
+ InMemoryAnnounce,
25
+ LogAnnounce,
26
+ WebhookAnnounce,
27
+ verify_signature,
28
+ )
29
+ from .errors import PolicyError, QuestionTooLarge, WaitError
30
+ from .model import (
31
+ MAX_QUESTION_BYTES,
32
+ Clock,
33
+ EntryPoint,
34
+ FakeClock,
35
+ Question,
36
+ SystemClock,
37
+ Transition,
38
+ WaitEnvelope,
39
+ canonical_json,
40
+ check_question_size,
41
+ iso,
42
+ new_ulid,
43
+ )
44
+ from .policy import WaitPolicy, parse_duration
45
+ from .publish import build_envelope, publish
46
+
47
+ __version__ = "0.3.0"
48
+
49
+ __all__ = [
50
+ "MAX_QUESTION_BYTES",
51
+ "AnnounceAdapter",
52
+ "BaseAnnounce",
53
+ "Clock",
54
+ "CompositeAnnounce",
55
+ "EntryPoint",
56
+ "FailingAnnounce",
57
+ "FakeClock",
58
+ "InMemoryAnnounce",
59
+ "LogAnnounce",
60
+ "PolicyError",
61
+ "Question",
62
+ "QuestionTooLarge",
63
+ "SystemClock",
64
+ "Transition",
65
+ "WaitEnvelope",
66
+ "WaitError",
67
+ "WaitPolicy",
68
+ "WebhookAnnounce",
69
+ "__version__",
70
+ "build_envelope",
71
+ "canonical_json",
72
+ "check_question_size",
73
+ "iso",
74
+ "new_ulid",
75
+ "parse_duration",
76
+ "publish",
77
+ "verify_signature",
78
+ ]
@@ -89,7 +89,7 @@ class BaseAnnounce(ABC):
89
89
  except Exception:
90
90
  # A backend being unreachable must not fail a run that has already parked.
91
91
  self._log.exception(
92
- "%s failed for interrupt %s (%s)", type(self).__name__, envelope.interrupt_id, transition
92
+ "%s failed for interrupt %s (%s)", type(self).__name__, envelope.question_id, transition
93
93
  )
94
94
 
95
95
  @abstractmethod