agent-wait 0.3.0__tar.gz → 0.4.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.
- {agent_wait-0.3.0 → agent_wait-0.4.0}/PKG-INFO +46 -49
- {agent_wait-0.3.0 → agent_wait-0.4.0}/README.md +191 -194
- {agent_wait-0.3.0 → agent_wait-0.4.0}/pyproject.toml +1 -1
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/__init__.py +1 -1
- {agent_wait-0.3.0 → agent_wait-0.4.0}/.gitignore +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/LICENSE +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/announce/__init__.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/announce/base.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/announce/composite.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/announce/log.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/announce/memory.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/announce/webhook.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/errors.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/model.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/policy.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/publish.py +0 -0
- {agent_wait-0.3.0 → agent_wait-0.4.0}/src/agent_wait/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-wait
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
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
5
|
Project-URL: Homepage, https://skamalj.github.io/agent-wait/
|
|
6
6
|
Project-URL: Documentation, https://skamalj.github.io/agent-wait/
|
|
@@ -48,28 +48,38 @@ pip install agent-wait-aws # SNS / SQS / EventBridge / Dynam
|
|
|
48
48
|
|
|
49
49
|
## The whole thing
|
|
50
50
|
|
|
51
|
-
**In the graph** — one
|
|
51
|
+
**In the graph** — one decorator, on the function where the decision belongs:
|
|
52
52
|
|
|
53
53
|
```python
|
|
54
54
|
from agent_wait import WaitPolicy
|
|
55
|
-
from langgraph_wait import
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
from langgraph_wait import hitl
|
|
56
|
+
|
|
57
|
+
FINANCE = WaitPolicy(
|
|
58
|
+
timeout="P3D",
|
|
59
|
+
default={"action": "reject"},
|
|
60
|
+
allowed_actions=("approve", "reject"),
|
|
61
|
+
tags={"approver_group": "finance"},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@tool
|
|
66
|
+
@hitl(FINANCE)
|
|
67
|
+
def issue_refund(order_id: str, amount: int) -> str:
|
|
68
|
+
payments.refund(order_id, amount) # runs only if the answer is {"action": "approve"}
|
|
69
|
+
return "refunded"
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
`
|
|
72
|
-
|
|
72
|
+
Calling `issue_refund(...)` inside a run parks the graph on `{"function": "issue_refund",
|
|
73
|
+
"args": {...}}`. On resume, `{"action": "approve"}` runs the body — optionally with edited
|
|
74
|
+
`args` — and anything else is returned in its place, so a model sees why it did not run.
|
|
75
|
+
|
|
76
|
+
A node that needs the answer itself declares a `decision` parameter and always runs:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
@hitl(FINANCE)
|
|
80
|
+
def review(state, decision=None):
|
|
81
|
+
return {"decision": decision} # verbatim: {"action": "approve", "note": "ok"}
|
|
82
|
+
```
|
|
73
83
|
|
|
74
84
|
**After the run** — one call:
|
|
75
85
|
|
|
@@ -83,7 +93,8 @@ publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
|
|
|
83
93
|
|
|
84
94
|
It reads `result["__interrupt__"]` — `Interrupt.id` and `Interrupt.value`, nothing else —
|
|
85
95
|
and hands one envelope per question to the announcers. No graph handle, no `get_state()`,
|
|
86
|
-
no checkpointer. If nothing interrupted, it does nothing.
|
|
96
|
+
no checkpointer. If nothing interrupted, it does nothing. A plain `interrupt(value)` from
|
|
97
|
+
a node that never heard of this library is published too, with the default policy.
|
|
87
98
|
|
|
88
99
|
**When the answer comes back** — the one `if` you write:
|
|
89
100
|
|
|
@@ -102,24 +113,7 @@ publish_interrupts(result, message["thread_id"], announce)
|
|
|
102
113
|
That is the complete integration. A duplicate answer runs nothing — LangGraph ignores a
|
|
103
114
|
resume for a question the thread has moved past — so there is nothing to check.
|
|
104
115
|
|
|
105
|
-
##
|
|
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:**
|
|
116
|
+
## Async mode — the thread does not park
|
|
123
117
|
|
|
124
118
|
```python
|
|
125
119
|
@tool
|
|
@@ -127,7 +121,7 @@ interrupt — are published with it.
|
|
|
127
121
|
def issue_refund(order_id: str, amount: int) -> str: ...
|
|
128
122
|
```
|
|
129
123
|
|
|
130
|
-
The decorator *is* the publisher: the
|
|
124
|
+
The decorator *is* the publisher: the call announces the question and returns
|
|
131
125
|
`{"status": "pending_approval", "question_id": ...}` without running. The graph carries
|
|
132
126
|
on; nothing is parked; nothing to call after the run. The decision arrives later as a new
|
|
133
127
|
message and your graph acts on it. This is the mode for a single-thread channel like
|
|
@@ -140,17 +134,17 @@ WhatsApp, where the approver is not the person on the thread.
|
|
|
140
134
|
"type": "wait.created",
|
|
141
135
|
"thread_id": "order-4471",
|
|
142
136
|
"question_id": "a1b2c3d4e5f60718",
|
|
143
|
-
"question": { "
|
|
137
|
+
"question": { "function": "issue_refund", "args": { "order_id": "order-4471", "amount": 41000 } },
|
|
144
138
|
"allowed_actions": ["approve", "reject"],
|
|
145
139
|
"expires_at": "2026-09-14T09:00:00Z",
|
|
146
140
|
"default": { "action": "reject" },
|
|
147
|
-
"source": { "
|
|
141
|
+
"source": { "function": "issue_refund" },
|
|
148
142
|
"reply_with": { "thread_id": "order-4471", "question_id": "a1b2c3d4e5f60718", "answer": null }
|
|
149
143
|
}
|
|
150
144
|
```
|
|
151
145
|
|
|
152
146
|
`reply_with` is a filled-in stub: copy it, set `answer`, send it back. Whatever goes in
|
|
153
|
-
`answer` is what `
|
|
147
|
+
`answer` is what the `@hitl` function receives — verbatim.
|
|
154
148
|
|
|
155
149
|
Full schema, the recommended answer shape, and how to deduplicate:
|
|
156
150
|
[Message formats](https://skamalj.github.io/agent-wait/message-formats/).
|
|
@@ -196,20 +190,23 @@ A raise inside `deliver()` becomes a log line; the run completes. Shipped:
|
|
|
196
190
|
- **Store anything.** LangGraph's checkpoint is the only state in interrupt mode; in
|
|
197
191
|
async mode there is none unless you keep one.
|
|
198
192
|
|
|
199
|
-
##
|
|
193
|
+
## Two things to know
|
|
194
|
+
|
|
195
|
+
**Not compatible with LangChain's `HumanInTheLoopMiddleware`.** It interrupts *before* a
|
|
196
|
+
tool is called, in its own shape; `@hitl` interrupts inside the call. Both on one tool
|
|
197
|
+
means two interrupts for one approval. Use one or the other.
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
interrupt id**
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
tool
|
|
206
|
-
Pinned by a test that fails if LangGraph changes it.
|
|
199
|
+
**One `interrupt()` per node** on langgraph 1.2.x. Two interrupting tools dispatched by
|
|
200
|
+
one `ToolNode` get the **same interrupt id**
|
|
201
|
+
([#6626](https://github.com/langchain-ai/langgraph/issues/6626)), and only one surfaces
|
|
202
|
+
per run ([#6624](https://github.com/langchain-ai/langgraph/issues/6624)). Give each
|
|
203
|
+
interrupting tool its own node. Pinned by a test that fails if LangGraph changes it.
|
|
207
204
|
|
|
208
205
|
## Layout
|
|
209
206
|
|
|
210
207
|
```
|
|
211
208
|
packages/agent-wait core: Question, WaitPolicy, the envelope, publish(), announcers. No deps.
|
|
212
|
-
packages/langgraph-wait
|
|
209
|
+
packages/langgraph-wait @hitl and publish_interrupts(). The only LangGraph import.
|
|
213
210
|
packages/agent-wait-aws four announce adapters, and a CDK stack for the example.
|
|
214
211
|
examples/refund_agent a graph and a host, deployed to Lambda behind SQS.
|
|
215
212
|
docs/ message contract, architecture, announcer guide, consumer guide.
|
|
@@ -1,194 +1,191 @@
|
|
|
1
|
-
# agent-wait
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/agent-wait/)
|
|
4
|
-
[](https://github.com/skamalj/agent-wait/actions/workflows/ci.yml)
|
|
5
|
-
[](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
|
|
28
|
-
|
|
29
|
-
```python
|
|
30
|
-
from agent_wait import WaitPolicy
|
|
31
|
-
from langgraph_wait import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"question_id": "a1b2c3d4e5f60718",
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
|
156
|
-
|
|
157
|
-
| `
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
|
|
1
|
+
# agent-wait
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/agent-wait/)
|
|
4
|
+
[](https://github.com/skamalj/agent-wait/actions/workflows/ci.yml)
|
|
5
|
+
[](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 decorator, on the function where the decision belongs:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from agent_wait import WaitPolicy
|
|
31
|
+
from langgraph_wait import hitl
|
|
32
|
+
|
|
33
|
+
FINANCE = WaitPolicy(
|
|
34
|
+
timeout="P3D",
|
|
35
|
+
default={"action": "reject"},
|
|
36
|
+
allowed_actions=("approve", "reject"),
|
|
37
|
+
tags={"approver_group": "finance"},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@tool
|
|
42
|
+
@hitl(FINANCE)
|
|
43
|
+
def issue_refund(order_id: str, amount: int) -> str:
|
|
44
|
+
payments.refund(order_id, amount) # runs only if the answer is {"action": "approve"}
|
|
45
|
+
return "refunded"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Calling `issue_refund(...)` inside a run parks the graph on `{"function": "issue_refund",
|
|
49
|
+
"args": {...}}`. On resume, `{"action": "approve"}` runs the body — optionally with edited
|
|
50
|
+
`args` — and anything else is returned in its place, so a model sees why it did not run.
|
|
51
|
+
|
|
52
|
+
A node that needs the answer itself declares a `decision` parameter and always runs:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
@hitl(FINANCE)
|
|
56
|
+
def review(state, decision=None):
|
|
57
|
+
return {"decision": decision} # verbatim: {"action": "approve", "note": "ok"}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**After the run** — one call:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from agent_wait_aws import SnsAnnounce
|
|
64
|
+
from langgraph_wait import publish_interrupts
|
|
65
|
+
|
|
66
|
+
result = graph.invoke(value, {"configurable": {"thread_id": thread_id}})
|
|
67
|
+
publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
It reads `result["__interrupt__"]` — `Interrupt.id` and `Interrupt.value`, nothing else —
|
|
71
|
+
and hands one envelope per question to the announcers. No graph handle, no `get_state()`,
|
|
72
|
+
no checkpointer. If nothing interrupted, it does nothing. A plain `interrupt(value)` from
|
|
73
|
+
a node that never heard of this library is published too, with the default policy.
|
|
74
|
+
|
|
75
|
+
**When the answer comes back** — the one `if` you write:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from langgraph.types import Command
|
|
79
|
+
|
|
80
|
+
if "question_id" in message: # an answer
|
|
81
|
+
value = Command(resume={message["question_id"]: message["answer"]})
|
|
82
|
+
else: # a start
|
|
83
|
+
value = message["input"]
|
|
84
|
+
|
|
85
|
+
result = graph.invoke(value, {"configurable": {"thread_id": message["thread_id"]}})
|
|
86
|
+
publish_interrupts(result, message["thread_id"], announce)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That is the complete integration. A duplicate answer runs nothing — LangGraph ignores a
|
|
90
|
+
resume for a question the thread has moved past — so there is nothing to check.
|
|
91
|
+
|
|
92
|
+
## Async mode — the thread does not park
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
@tool
|
|
96
|
+
@hitl(FINANCE, mode="async", announce=[SnsAnnounce(topic_arn), DynamoDbAnnounce(table)])
|
|
97
|
+
def issue_refund(order_id: str, amount: int) -> str: ...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The decorator *is* the publisher: the call announces the question and returns
|
|
101
|
+
`{"status": "pending_approval", "question_id": ...}` without running. The graph carries
|
|
102
|
+
on; nothing is parked; nothing to call after the run. The decision arrives later as a new
|
|
103
|
+
message and your graph acts on it. This is the mode for a single-thread channel like
|
|
104
|
+
WhatsApp, where the approver is not the person on the thread.
|
|
105
|
+
|
|
106
|
+
## What goes out
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"type": "wait.created",
|
|
111
|
+
"thread_id": "order-4471",
|
|
112
|
+
"question_id": "a1b2c3d4e5f60718",
|
|
113
|
+
"question": { "function": "issue_refund", "args": { "order_id": "order-4471", "amount": 41000 } },
|
|
114
|
+
"allowed_actions": ["approve", "reject"],
|
|
115
|
+
"expires_at": "2026-09-14T09:00:00Z",
|
|
116
|
+
"default": { "action": "reject" },
|
|
117
|
+
"source": { "function": "issue_refund" },
|
|
118
|
+
"reply_with": { "thread_id": "order-4471", "question_id": "a1b2c3d4e5f60718", "answer": null }
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`reply_with` is a filled-in stub: copy it, set `answer`, send it back. Whatever goes in
|
|
123
|
+
`answer` is what the `@hitl` function receives — verbatim.
|
|
124
|
+
|
|
125
|
+
Full schema, the recommended answer shape, and how to deduplicate:
|
|
126
|
+
[Message formats](https://skamalj.github.io/agent-wait/message-formats/).
|
|
127
|
+
|
|
128
|
+
## Announcers
|
|
129
|
+
|
|
130
|
+
Subclass `BaseAnnounce`, write one method:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from agent_wait import BaseAnnounce
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class RedisAnnounce(BaseAnnounce):
|
|
137
|
+
name = "redis"
|
|
138
|
+
|
|
139
|
+
def __init__(self, client, **kw):
|
|
140
|
+
super().__init__(**kw)
|
|
141
|
+
self.client = client
|
|
142
|
+
|
|
143
|
+
def deliver(self, envelope, transition):
|
|
144
|
+
self.client.set(envelope.dedupe_key, envelope.to_json())
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A raise inside `deliver()` becomes a log line; the run completes. Shipped:
|
|
148
|
+
|
|
149
|
+
| Adapter | Package | Where the question lands |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `WebhookAnnounce` | `agent-wait` | A URL. JSON POST, optional HMAC-SHA256 signature. Stdlib only. |
|
|
152
|
+
| `LogAnnounce` | `agent-wait` | A structured log line. The question never reaches INFO. |
|
|
153
|
+
| `InMemoryAnnounce` | `agent-wait` | A list. For tests. |
|
|
154
|
+
| `SnsAnnounce` | `agent-wait-aws` | A topic; `tags` become message attributes for subscription filters. |
|
|
155
|
+
| `SqsAnnounce` | `agent-wait-aws` | A queue; on FIFO, grouped by thread, deduplicated on the stable key. |
|
|
156
|
+
| `EventBridgeAnnounce` | `agent-wait-aws` | A bus. Notices partial failures behind a 200. |
|
|
157
|
+
| `DynamoDbAnnounce` | `agent-wait-aws` | **A row**, `status=open`, with a GSI an approvals UI can query. Write-only. |
|
|
158
|
+
|
|
159
|
+
[Writing an announcer](https://skamalj.github.io/agent-wait/writing-an-announcer/).
|
|
160
|
+
|
|
161
|
+
## What the library does *not* do
|
|
162
|
+
|
|
163
|
+
- **Receive answers.** No endpoint, no validation, no ledger reads. The `if` above is yours.
|
|
164
|
+
- **Enforce anything.** `expires_at`, `default`, `answer_ttl`, `allowed_actions` are
|
|
165
|
+
published so the consumer has the asker's intent. Acting on them is the consumer's.
|
|
166
|
+
- **Store anything.** LangGraph's checkpoint is the only state in interrupt mode; in
|
|
167
|
+
async mode there is none unless you keep one.
|
|
168
|
+
|
|
169
|
+
## Two things to know
|
|
170
|
+
|
|
171
|
+
**Not compatible with LangChain's `HumanInTheLoopMiddleware`.** It interrupts *before* a
|
|
172
|
+
tool is called, in its own shape; `@hitl` interrupts inside the call. Both on one tool
|
|
173
|
+
means two interrupts for one approval. Use one or the other.
|
|
174
|
+
|
|
175
|
+
**One `interrupt()` per node** on langgraph 1.2.x. Two interrupting tools dispatched by
|
|
176
|
+
one `ToolNode` get the **same interrupt id**
|
|
177
|
+
([#6626](https://github.com/langchain-ai/langgraph/issues/6626)), and only one surfaces
|
|
178
|
+
per run ([#6624](https://github.com/langchain-ai/langgraph/issues/6624)). Give each
|
|
179
|
+
interrupting tool its own node. Pinned by a test that fails if LangGraph changes it.
|
|
180
|
+
|
|
181
|
+
## Layout
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
packages/agent-wait core: Question, WaitPolicy, the envelope, publish(), announcers. No deps.
|
|
185
|
+
packages/langgraph-wait @hitl and publish_interrupts(). The only LangGraph import.
|
|
186
|
+
packages/agent-wait-aws four announce adapters, and a CDK stack for the example.
|
|
187
|
+
examples/refund_agent a graph and a host, deployed to Lambda behind SQS.
|
|
188
|
+
docs/ message contract, architecture, announcer guide, consumer guide.
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-wait"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|