agent-wait 0.4.1__tar.gz → 0.5.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.4.1 → agent_wait-0.5.0}/PKG-INFO +57 -32
- {agent_wait-0.4.1 → agent_wait-0.5.0}/README.md +47 -29
- agent_wait-0.5.0/pyproject.toml +109 -0
- agent_wait-0.5.0/reference/wait-sim/README.md +31 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/__init__.py +85 -78
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/base.py +1 -1
- agent_wait-0.5.0/src/agent_wait/aws/__init__.py +19 -0
- agent_wait-0.5.0/src/agent_wait/aws/dynamodb.py +75 -0
- agent_wait-0.5.0/src/agent_wait/aws/eventbridge.py +57 -0
- agent_wait-0.5.0/src/agent_wait/aws/sns.py +51 -0
- agent_wait-0.5.0/src/agent_wait/aws/sqs.py +50 -0
- agent_wait-0.5.0/src/agent_wait/framework.py +55 -0
- agent_wait-0.5.0/src/agent_wait/langgraph/__init__.py +69 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/publish.py +1 -1
- agent_wait-0.5.0/src/agent_wait/wait.py +203 -0
- agent_wait-0.4.1/pyproject.toml +0 -38
- {agent_wait-0.4.1 → agent_wait-0.5.0}/.gitignore +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/LICENSE +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/__init__.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/composite.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/log.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/memory.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/webhook.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/errors.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/model.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/policy.py +0 -0
- {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-wait
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Human-in-the-loop for LangGraph agents -- and anything else a run has to wait for. Get the interrupt out of the process (SNS, SQS, EventBridge, DynamoDB, webhook) with everything needed to answer it; get 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/
|
|
7
7
|
Project-URL: Source, https://github.com/skamalj/agent-wait
|
|
@@ -10,7 +10,7 @@ Project-URL: Changelog, https://github.com/skamalj/agent-wait/blob/main/CHANGELO
|
|
|
10
10
|
Author-email: Kamaljeet Singh <skamalj@gmail.com>
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Keywords: agents,approval,durable,human-in-the-loop,interrupt,langgraph,serverless
|
|
13
|
+
Keywords: agents,approval,callback,durable,dynamodb,eventbridge,hitl,human-in-the-loop,interrupt,lambda,langchain,langgraph,serverless,sns,sqs,webhook
|
|
14
14
|
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -20,6 +20,13 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
22
|
Requires-Python: >=3.12
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: boto3>=1.35; extra == 'all'
|
|
25
|
+
Requires-Dist: langgraph<2,>=1.2; extra == 'all'
|
|
26
|
+
Provides-Extra: aws
|
|
27
|
+
Requires-Dist: boto3>=1.35; extra == 'aws'
|
|
28
|
+
Provides-Extra: langgraph
|
|
29
|
+
Requires-Dist: langgraph<2,>=1.2; extra == 'langgraph'
|
|
23
30
|
Description-Content-Type: text/markdown
|
|
24
31
|
|
|
25
32
|
# agent-wait
|
|
@@ -29,32 +36,40 @@ Description-Content-Type: text/markdown
|
|
|
29
36
|
[](https://github.com/skamalj/agent-wait/blob/main/LICENSE)
|
|
30
37
|
[](https://skamalj.github.io/agent-wait/)
|
|
31
38
|
|
|
32
|
-
**
|
|
39
|
+
**Make an agent wait for something outside the process — and get the answer back in.**
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
ticket queue or another service, you are writing that code yourself — whether your agent
|
|
39
|
-
is a server that runs for a year or a Lambda that is gone in seconds.
|
|
41
|
+
Human-in-the-loop is the common case: a refund over the limit, a contract clause, a
|
|
42
|
+
deploy. But the same shape covers a vendor callback, a payment processor, a KYC check, or
|
|
43
|
+
another agent — anything the run cannot continue without, and nothing inside the process
|
|
44
|
+
can supply.
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
LangGraph has `interrupt()` for this. When a node calls it the graph pauses and the
|
|
47
|
+
interrupt is handed to whatever called `invoke()` — and that is where LangGraph stops.
|
|
48
|
+
There is no built-in way to tell anyone *else* that a question was asked, and no built-in
|
|
49
|
+
way for anyone else to answer it. The moment the question has to reach Slack, an approvals
|
|
50
|
+
dashboard, a ticket queue or another service, you are writing that code yourself — whether
|
|
51
|
+
your agent is a server that runs for a year or a Lambda that is gone in seconds.
|
|
52
|
+
|
|
53
|
+
agent-wait is that code. It takes the interrupt and puts it somewhere people (or systems)
|
|
54
|
+
can see it — a topic, a queue, a webhook, a database row — with everything needed to answer
|
|
55
|
+
it in one envelope, and documents the shape of the answer so the return leg is one `if`.
|
|
44
56
|
|
|
45
57
|
```bash
|
|
46
|
-
pip install agent-wait
|
|
47
|
-
pip install agent-wait
|
|
58
|
+
pip install "agent-wait[langgraph]" # @wait + publish_interrupts for LangGraph
|
|
59
|
+
pip install "agent-wait[langgraph,aws]" # + SNS / SQS / EventBridge / DynamoDB announcers
|
|
48
60
|
```
|
|
49
61
|
|
|
62
|
+
The bare `agent-wait` is the framework-free core (policy, envelope, webhook and in-memory
|
|
63
|
+
announcers) and depends on nothing. Each extra pulls in exactly one framework or provider.
|
|
64
|
+
|
|
50
65
|
## The whole thing
|
|
51
66
|
|
|
52
67
|
**In the graph** — one decorator, on the function where the decision belongs:
|
|
53
68
|
|
|
54
69
|
```python
|
|
55
70
|
from agent_wait import WaitPolicy
|
|
71
|
+
from agent_wait.langgraph import wait
|
|
56
72
|
from langchain_core.tools import tool
|
|
57
|
-
from langgraph_wait import hitl
|
|
58
73
|
|
|
59
74
|
FINANCE = WaitPolicy(
|
|
60
75
|
timeout="P3D",
|
|
@@ -65,7 +80,7 @@ FINANCE = WaitPolicy(
|
|
|
65
80
|
|
|
66
81
|
|
|
67
82
|
@tool
|
|
68
|
-
@
|
|
83
|
+
@wait(FINANCE)
|
|
69
84
|
def issue_refund(order_id: str, amount: int) -> str:
|
|
70
85
|
payments.refund(order_id, amount) # runs only if the answer is {"action": "approve"}
|
|
71
86
|
return "refunded"
|
|
@@ -85,23 +100,24 @@ none is enforced — the consumer acts on them:
|
|
|
85
100
|
| `answer_ttl` | How long an answer stays usable after it is given. |
|
|
86
101
|
| `allowed_actions` | Which answers are meaningful — the buttons to draw. |
|
|
87
102
|
| `tags` | Routing hints; become SNS message attributes. |
|
|
103
|
+
| `correlation` | Your reference for the thing being waited on — a vendor job id, a ticket. |
|
|
88
104
|
|
|
89
105
|
A node that needs the answer itself declares a `decision` parameter and always runs,
|
|
90
106
|
approve or not, with the answer in it:
|
|
91
107
|
|
|
92
108
|
```python
|
|
93
|
-
@
|
|
109
|
+
@wait(FINANCE)
|
|
94
110
|
def review(state, decision=None):
|
|
95
111
|
return {"decision": decision} # verbatim: {"action": "approve", "note": "ok"}
|
|
96
112
|
```
|
|
97
113
|
|
|
98
|
-
The parameter name is yours — `@
|
|
114
|
+
The parameter name is yours — `@wait(FINANCE, decision="verdict")` looks for `verdict`.
|
|
99
115
|
|
|
100
116
|
**After the run** — one call:
|
|
101
117
|
|
|
102
118
|
```python
|
|
103
|
-
from
|
|
104
|
-
from
|
|
119
|
+
from agent_wait.aws import SnsAnnounce
|
|
120
|
+
from agent_wait.langgraph import publish_interrupts
|
|
105
121
|
|
|
106
122
|
result = graph.invoke(value, {"configurable": {"thread_id": thread_id}})
|
|
107
123
|
publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
|
|
@@ -132,11 +148,11 @@ resume for a question the thread has moved past — so there is nothing to check
|
|
|
132
148
|
## Async mode — the thread does not park
|
|
133
149
|
|
|
134
150
|
```python
|
|
135
|
-
from
|
|
151
|
+
from agent_wait.aws import DynamoDbAnnounce, SnsAnnounce
|
|
136
152
|
|
|
137
153
|
|
|
138
154
|
@tool
|
|
139
|
-
@
|
|
155
|
+
@wait(FINANCE, mode="async", announce=[SnsAnnounce(topic_arn), DynamoDbAnnounce(table)])
|
|
140
156
|
def issue_refund(order_id: str, amount: int) -> str: ...
|
|
141
157
|
```
|
|
142
158
|
|
|
@@ -165,7 +181,7 @@ question; the `DynamoDbAnnounce` row (or whatever you keep) is the only record.
|
|
|
165
181
|
```
|
|
166
182
|
|
|
167
183
|
`reply_with` is a filled-in stub: copy it, set `answer`, send it back. Whatever goes in
|
|
168
|
-
`answer` is what the `@
|
|
184
|
+
`answer` is what the `@wait` function receives — verbatim.
|
|
169
185
|
|
|
170
186
|
Full schema, the recommended answer shape, and how to deduplicate:
|
|
171
187
|
[Message formats](https://skamalj.github.io/agent-wait/message-formats/).
|
|
@@ -191,8 +207,8 @@ class RedisAnnounce(BaseAnnounce):
|
|
|
191
207
|
|
|
192
208
|
A raise inside `deliver()` becomes a log line; the run completes.
|
|
193
209
|
|
|
194
|
-
Shipped: `WebhookAnnounce` (signed JSON POST, stdlib), `LogAnnounce`,
|
|
195
|
-
|
|
210
|
+
Shipped in the core: `WebhookAnnounce` (signed JSON POST, stdlib), `LogAnnounce`,
|
|
211
|
+
`InMemoryAnnounce`. With `[aws]`: `SnsAnnounce`, `SqsAnnounce`, `EventBridgeAnnounce`,
|
|
196
212
|
`DynamoDbAnnounce` (the question as a row an approvals UI can query). Each one, its
|
|
197
213
|
constructor and where the question lands: [Announcers](https://skamalj.github.io/agent-wait/announcers/).
|
|
198
214
|
Your own: [Writing an announcer](https://skamalj.github.io/agent-wait/writing-an-announcer/).
|
|
@@ -208,7 +224,7 @@ Your own: [Writing an announcer](https://skamalj.github.io/agent-wait/writing-an
|
|
|
208
224
|
## Two things to know
|
|
209
225
|
|
|
210
226
|
**Not compatible with LangChain's `HumanInTheLoopMiddleware`.** It interrupts *before* a
|
|
211
|
-
tool is called, in its own shape; `@
|
|
227
|
+
tool is called, in its own shape; `@wait` interrupts inside the call. Both on one tool
|
|
212
228
|
means two interrupts for one approval. Use one or the other.
|
|
213
229
|
|
|
214
230
|
**One `interrupt()` per node** on langgraph 1.2.x. Two interrupting tools dispatched by
|
|
@@ -219,12 +235,21 @@ interrupting tool its own node. Pinned by a test that fails if LangGraph changes
|
|
|
219
235
|
|
|
220
236
|
## Layout
|
|
221
237
|
|
|
238
|
+
One distribution, one import root. Frameworks and providers are subpackages behind extras:
|
|
239
|
+
|
|
222
240
|
```
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
agent_wait core: WaitPolicy, Question, the envelope, publish(), BaseAnnounce,
|
|
242
|
+
Webhook/Log/InMemory announcers, and the Framework interface. No deps.
|
|
243
|
+
agent_wait.langgraph [langgraph] @wait and publish_interrupts() bound to LangGraph.
|
|
244
|
+
agent_wait.aws [aws] SnsAnnounce, SqsAnnounce, EventBridgeAnnounce, DynamoDbAnnounce.
|
|
245
|
+
examples/refund_agent a graph, a host written for Lambda behind SQS, and its CDK stack.
|
|
246
|
+
docs/ message contract, architecture, announcer guide, consumer guide.
|
|
228
247
|
```
|
|
229
248
|
|
|
249
|
+
`@wait` and `publish_interrupts` are written once against a three-method `Framework`
|
|
250
|
+
interface; `agent_wait.langgraph` is one implementor of it. Announcers are the same
|
|
251
|
+
split: `BaseAnnounce` is the interface, each provider subpackage the implementors. A
|
|
252
|
+
`strands` extra follows the same pattern — see
|
|
253
|
+
[Architecture](https://skamalj.github.io/agent-wait/architecture/).
|
|
254
|
+
|
|
230
255
|
MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
|
|
@@ -5,32 +5,40 @@
|
|
|
5
5
|
[](https://github.com/skamalj/agent-wait/blob/main/LICENSE)
|
|
6
6
|
[](https://skamalj.github.io/agent-wait/)
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**Make an agent wait for something outside the process — and get the answer back in.**
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
ticket queue or another service, you are writing that code yourself — whether your agent
|
|
15
|
-
is a server that runs for a year or a Lambda that is gone in seconds.
|
|
10
|
+
Human-in-the-loop is the common case: a refund over the limit, a contract clause, a
|
|
11
|
+
deploy. But the same shape covers a vendor callback, a payment processor, a KYC check, or
|
|
12
|
+
another agent — anything the run cannot continue without, and nothing inside the process
|
|
13
|
+
can supply.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
LangGraph has `interrupt()` for this. When a node calls it the graph pauses and the
|
|
16
|
+
interrupt is handed to whatever called `invoke()` — and that is where LangGraph stops.
|
|
17
|
+
There is no built-in way to tell anyone *else* that a question was asked, and no built-in
|
|
18
|
+
way for anyone else to answer it. The moment the question has to reach Slack, an approvals
|
|
19
|
+
dashboard, a ticket queue or another service, you are writing that code yourself — whether
|
|
20
|
+
your agent is a server that runs for a year or a Lambda that is gone in seconds.
|
|
21
|
+
|
|
22
|
+
agent-wait is that code. It takes the interrupt and puts it somewhere people (or systems)
|
|
23
|
+
can see it — a topic, a queue, a webhook, a database row — with everything needed to answer
|
|
24
|
+
it in one envelope, and documents the shape of the answer so the return leg is one `if`.
|
|
20
25
|
|
|
21
26
|
```bash
|
|
22
|
-
pip install agent-wait
|
|
23
|
-
pip install agent-wait
|
|
27
|
+
pip install "agent-wait[langgraph]" # @wait + publish_interrupts for LangGraph
|
|
28
|
+
pip install "agent-wait[langgraph,aws]" # + SNS / SQS / EventBridge / DynamoDB announcers
|
|
24
29
|
```
|
|
25
30
|
|
|
31
|
+
The bare `agent-wait` is the framework-free core (policy, envelope, webhook and in-memory
|
|
32
|
+
announcers) and depends on nothing. Each extra pulls in exactly one framework or provider.
|
|
33
|
+
|
|
26
34
|
## The whole thing
|
|
27
35
|
|
|
28
36
|
**In the graph** — one decorator, on the function where the decision belongs:
|
|
29
37
|
|
|
30
38
|
```python
|
|
31
39
|
from agent_wait import WaitPolicy
|
|
40
|
+
from agent_wait.langgraph import wait
|
|
32
41
|
from langchain_core.tools import tool
|
|
33
|
-
from langgraph_wait import hitl
|
|
34
42
|
|
|
35
43
|
FINANCE = WaitPolicy(
|
|
36
44
|
timeout="P3D",
|
|
@@ -41,7 +49,7 @@ FINANCE = WaitPolicy(
|
|
|
41
49
|
|
|
42
50
|
|
|
43
51
|
@tool
|
|
44
|
-
@
|
|
52
|
+
@wait(FINANCE)
|
|
45
53
|
def issue_refund(order_id: str, amount: int) -> str:
|
|
46
54
|
payments.refund(order_id, amount) # runs only if the answer is {"action": "approve"}
|
|
47
55
|
return "refunded"
|
|
@@ -61,23 +69,24 @@ none is enforced — the consumer acts on them:
|
|
|
61
69
|
| `answer_ttl` | How long an answer stays usable after it is given. |
|
|
62
70
|
| `allowed_actions` | Which answers are meaningful — the buttons to draw. |
|
|
63
71
|
| `tags` | Routing hints; become SNS message attributes. |
|
|
72
|
+
| `correlation` | Your reference for the thing being waited on — a vendor job id, a ticket. |
|
|
64
73
|
|
|
65
74
|
A node that needs the answer itself declares a `decision` parameter and always runs,
|
|
66
75
|
approve or not, with the answer in it:
|
|
67
76
|
|
|
68
77
|
```python
|
|
69
|
-
@
|
|
78
|
+
@wait(FINANCE)
|
|
70
79
|
def review(state, decision=None):
|
|
71
80
|
return {"decision": decision} # verbatim: {"action": "approve", "note": "ok"}
|
|
72
81
|
```
|
|
73
82
|
|
|
74
|
-
The parameter name is yours — `@
|
|
83
|
+
The parameter name is yours — `@wait(FINANCE, decision="verdict")` looks for `verdict`.
|
|
75
84
|
|
|
76
85
|
**After the run** — one call:
|
|
77
86
|
|
|
78
87
|
```python
|
|
79
|
-
from
|
|
80
|
-
from
|
|
88
|
+
from agent_wait.aws import SnsAnnounce
|
|
89
|
+
from agent_wait.langgraph import publish_interrupts
|
|
81
90
|
|
|
82
91
|
result = graph.invoke(value, {"configurable": {"thread_id": thread_id}})
|
|
83
92
|
publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
|
|
@@ -108,11 +117,11 @@ resume for a question the thread has moved past — so there is nothing to check
|
|
|
108
117
|
## Async mode — the thread does not park
|
|
109
118
|
|
|
110
119
|
```python
|
|
111
|
-
from
|
|
120
|
+
from agent_wait.aws import DynamoDbAnnounce, SnsAnnounce
|
|
112
121
|
|
|
113
122
|
|
|
114
123
|
@tool
|
|
115
|
-
@
|
|
124
|
+
@wait(FINANCE, mode="async", announce=[SnsAnnounce(topic_arn), DynamoDbAnnounce(table)])
|
|
116
125
|
def issue_refund(order_id: str, amount: int) -> str: ...
|
|
117
126
|
```
|
|
118
127
|
|
|
@@ -141,7 +150,7 @@ question; the `DynamoDbAnnounce` row (or whatever you keep) is the only record.
|
|
|
141
150
|
```
|
|
142
151
|
|
|
143
152
|
`reply_with` is a filled-in stub: copy it, set `answer`, send it back. Whatever goes in
|
|
144
|
-
`answer` is what the `@
|
|
153
|
+
`answer` is what the `@wait` function receives — verbatim.
|
|
145
154
|
|
|
146
155
|
Full schema, the recommended answer shape, and how to deduplicate:
|
|
147
156
|
[Message formats](https://skamalj.github.io/agent-wait/message-formats/).
|
|
@@ -167,8 +176,8 @@ class RedisAnnounce(BaseAnnounce):
|
|
|
167
176
|
|
|
168
177
|
A raise inside `deliver()` becomes a log line; the run completes.
|
|
169
178
|
|
|
170
|
-
Shipped: `WebhookAnnounce` (signed JSON POST, stdlib), `LogAnnounce`,
|
|
171
|
-
|
|
179
|
+
Shipped in the core: `WebhookAnnounce` (signed JSON POST, stdlib), `LogAnnounce`,
|
|
180
|
+
`InMemoryAnnounce`. With `[aws]`: `SnsAnnounce`, `SqsAnnounce`, `EventBridgeAnnounce`,
|
|
172
181
|
`DynamoDbAnnounce` (the question as a row an approvals UI can query). Each one, its
|
|
173
182
|
constructor and where the question lands: [Announcers](https://skamalj.github.io/agent-wait/announcers/).
|
|
174
183
|
Your own: [Writing an announcer](https://skamalj.github.io/agent-wait/writing-an-announcer/).
|
|
@@ -184,7 +193,7 @@ Your own: [Writing an announcer](https://skamalj.github.io/agent-wait/writing-an
|
|
|
184
193
|
## Two things to know
|
|
185
194
|
|
|
186
195
|
**Not compatible with LangChain's `HumanInTheLoopMiddleware`.** It interrupts *before* a
|
|
187
|
-
tool is called, in its own shape; `@
|
|
196
|
+
tool is called, in its own shape; `@wait` interrupts inside the call. Both on one tool
|
|
188
197
|
means two interrupts for one approval. Use one or the other.
|
|
189
198
|
|
|
190
199
|
**One `interrupt()` per node** on langgraph 1.2.x. Two interrupting tools dispatched by
|
|
@@ -195,12 +204,21 @@ interrupting tool its own node. Pinned by a test that fails if LangGraph changes
|
|
|
195
204
|
|
|
196
205
|
## Layout
|
|
197
206
|
|
|
207
|
+
One distribution, one import root. Frameworks and providers are subpackages behind extras:
|
|
208
|
+
|
|
198
209
|
```
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
agent_wait core: WaitPolicy, Question, the envelope, publish(), BaseAnnounce,
|
|
211
|
+
Webhook/Log/InMemory announcers, and the Framework interface. No deps.
|
|
212
|
+
agent_wait.langgraph [langgraph] @wait and publish_interrupts() bound to LangGraph.
|
|
213
|
+
agent_wait.aws [aws] SnsAnnounce, SqsAnnounce, EventBridgeAnnounce, DynamoDbAnnounce.
|
|
214
|
+
examples/refund_agent a graph, a host written for Lambda behind SQS, and its CDK stack.
|
|
215
|
+
docs/ message contract, architecture, announcer guide, consumer guide.
|
|
204
216
|
```
|
|
205
217
|
|
|
218
|
+
`@wait` and `publish_interrupts` are written once against a three-method `Framework`
|
|
219
|
+
interface; `agent_wait.langgraph` is one implementor of it. Announcers are the same
|
|
220
|
+
split: `BaseAnnounce` is the interface, each provider subpackage the implementors. A
|
|
221
|
+
`strands` extra follows the same pattern — see
|
|
222
|
+
[Architecture](https://skamalj.github.io/agent-wait/architecture/).
|
|
223
|
+
|
|
206
224
|
MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agent-wait"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Human-in-the-loop for LangGraph agents -- and anything else a run has to wait for. Get the interrupt out of the process (SNS, SQS, EventBridge, DynamoDB, webhook) with everything needed to answer it; get the answer back in."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Kamaljeet Singh", email = "skamalj@gmail.com" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"human-in-the-loop",
|
|
12
|
+
"hitl",
|
|
13
|
+
"approval",
|
|
14
|
+
"langgraph",
|
|
15
|
+
"langchain",
|
|
16
|
+
"agents",
|
|
17
|
+
"interrupt",
|
|
18
|
+
"durable",
|
|
19
|
+
"serverless",
|
|
20
|
+
"lambda",
|
|
21
|
+
"sqs",
|
|
22
|
+
"sns",
|
|
23
|
+
"eventbridge",
|
|
24
|
+
"dynamodb",
|
|
25
|
+
"webhook",
|
|
26
|
+
"callback",
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Software Development :: Libraries",
|
|
36
|
+
"Typing :: Typed",
|
|
37
|
+
]
|
|
38
|
+
dependencies = []
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
langgraph = ["langgraph>=1.2,<2"]
|
|
42
|
+
aws = ["boto3>=1.35"]
|
|
43
|
+
all = ["agent-wait[langgraph,aws]"]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://skamalj.github.io/agent-wait/"
|
|
47
|
+
Documentation = "https://skamalj.github.io/agent-wait/"
|
|
48
|
+
Source = "https://github.com/skamalj/agent-wait"
|
|
49
|
+
Issues = "https://github.com/skamalj/agent-wait/issues"
|
|
50
|
+
Changelog = "https://github.com/skamalj/agent-wait/blob/main/CHANGELOG.md"
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["hatchling"]
|
|
54
|
+
build-backend = "hatchling.build"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/agent_wait"]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
include = ["src/agent_wait", "README.md", "LICENSE"]
|
|
61
|
+
|
|
62
|
+
[dependency-groups]
|
|
63
|
+
dev = [
|
|
64
|
+
"agent-wait[all]",
|
|
65
|
+
"pytest>=8.3",
|
|
66
|
+
"pytest-cov>=5.0",
|
|
67
|
+
"ruff>=0.6",
|
|
68
|
+
"pyright>=1.1.380",
|
|
69
|
+
"moto[dynamodb,sqs,sns,events]>=5.0",
|
|
70
|
+
"boto3-stubs[dynamodb,sqs,sns,events]>=1.35",
|
|
71
|
+
"langgraph-checkpoint-sqlite>=2.0",
|
|
72
|
+
"aws-cdk-lib>=2.160",
|
|
73
|
+
"constructs>=10.3",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.uv.sources]
|
|
77
|
+
agent-wait = { workspace = true }
|
|
78
|
+
|
|
79
|
+
[tool.uv.workspace]
|
|
80
|
+
members = ["."]
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
testpaths = ["tests", "examples"]
|
|
84
|
+
addopts = "-q --strict-markers"
|
|
85
|
+
norecursedirs = ["cdk.out", "build", ".venv", "reference"]
|
|
86
|
+
markers = [
|
|
87
|
+
"e2e: requires real AWS; opt in with AGENT_WAIT_E2E=1",
|
|
88
|
+
"spike: empirical probes of LangGraph behaviour",
|
|
89
|
+
]
|
|
90
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff]
|
|
93
|
+
line-length = 110
|
|
94
|
+
target-version = "py312"
|
|
95
|
+
extend-exclude = ["reference", "cdk.out"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint]
|
|
98
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
99
|
+
ignore = ["B008", "RUF012"]
|
|
100
|
+
|
|
101
|
+
[tool.ruff.lint.per-file-ignores]
|
|
102
|
+
"tests/**" = ["E501"]
|
|
103
|
+
|
|
104
|
+
[tool.pyright]
|
|
105
|
+
include = ["src"]
|
|
106
|
+
exclude = ["reference", "**/node_modules", "**/__pycache__"]
|
|
107
|
+
pythonVersion = "3.12"
|
|
108
|
+
typeCheckingMode = "strict"
|
|
109
|
+
reportMissingTypeStubs = false
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# agent-wait · end-to-end simulation (LangGraph agent on Lambda)
|
|
2
|
+
|
|
3
|
+
Runs a real LangGraph graph (1.2.x) through the durable-wait design, with in-memory stand-ins
|
|
4
|
+
for the AWS services. Every stand-in mimics the one property the design relies on:
|
|
5
|
+
|
|
6
|
+
| Stand-in | Real service | Property relied on |
|
|
7
|
+
|---------------------|-----------------------|------------------------------------------------------|
|
|
8
|
+
| `DynamoWaitStore` | DynamoDB | conditional writes (status CAS), unique idempotency key |
|
|
9
|
+
| `SqsFifo` | SQS FIFO | one in-flight message per MessageGroupId, visibility timeout, redelivery |
|
|
10
|
+
| `EventBridgeScheduler` | EventBridge Scheduler | one-time schedule named by wait_id, deleted on cancel |
|
|
11
|
+
| `EventBridgeBus` | EventBridge | fan-out to subscribers (SNS/Slack/webhooks) |
|
|
12
|
+
| `run_handler` etc. | Lambda | stateless invocations that can die mid-way |
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pip install langgraph
|
|
16
|
+
python simulate.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
- `agent_wait/core.py` — the framework-agnostic core: Wait model, 5 ports, HMAC tokens, Waiter, sweeper
|
|
21
|
+
- `agent_wait/langgraph_adapter.py` — `ask()` + the LangGraph adapter (extract interrupts, build resume, still_pending)
|
|
22
|
+
- `agent_wait/aws_sim.py` — the AWS stand-ins
|
|
23
|
+
- `app/graph.py` — the customer's refund graph (knows nothing about AWS)
|
|
24
|
+
- `app/lambdas.py` — three Lambda handlers: run (SQS), answer (API Gateway), on_timer (Scheduler)
|
|
25
|
+
- `simulate.py` — four scenarios; `RUN_OUTPUT.txt` is a captured run
|
|
26
|
+
|
|
27
|
+
Scenarios:
|
|
28
|
+
- A crash after checkpoint but before register → redelivery → idempotent; double-click; stale reject; tampered token; resume on another Lambda; refund issued once
|
|
29
|
+
- B no answer for 3 days → Scheduler fires → default reject → late click gets 409
|
|
30
|
+
- C crash after resume applied but before SQS ack → redelivery → still_pending() guard → no second refund
|
|
31
|
+
- D crash after DynamoDB create but before timer/notify → sweeper completes setup
|