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.
Files changed (27) hide show
  1. {agent_wait-0.4.1 → agent_wait-0.5.0}/PKG-INFO +57 -32
  2. {agent_wait-0.4.1 → agent_wait-0.5.0}/README.md +47 -29
  3. agent_wait-0.5.0/pyproject.toml +109 -0
  4. agent_wait-0.5.0/reference/wait-sim/README.md +31 -0
  5. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/__init__.py +85 -78
  6. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/base.py +1 -1
  7. agent_wait-0.5.0/src/agent_wait/aws/__init__.py +19 -0
  8. agent_wait-0.5.0/src/agent_wait/aws/dynamodb.py +75 -0
  9. agent_wait-0.5.0/src/agent_wait/aws/eventbridge.py +57 -0
  10. agent_wait-0.5.0/src/agent_wait/aws/sns.py +51 -0
  11. agent_wait-0.5.0/src/agent_wait/aws/sqs.py +50 -0
  12. agent_wait-0.5.0/src/agent_wait/framework.py +55 -0
  13. agent_wait-0.5.0/src/agent_wait/langgraph/__init__.py +69 -0
  14. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/publish.py +1 -1
  15. agent_wait-0.5.0/src/agent_wait/wait.py +203 -0
  16. agent_wait-0.4.1/pyproject.toml +0 -38
  17. {agent_wait-0.4.1 → agent_wait-0.5.0}/.gitignore +0 -0
  18. {agent_wait-0.4.1 → agent_wait-0.5.0}/LICENSE +0 -0
  19. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/__init__.py +0 -0
  20. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/composite.py +0 -0
  21. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/log.py +0 -0
  22. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/memory.py +0 -0
  23. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/announce/webhook.py +0 -0
  24. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/errors.py +0 -0
  25. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/model.py +0 -0
  26. {agent_wait-0.4.1 → agent_wait-0.5.0}/src/agent_wait/policy.py +0 -0
  27. {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.1
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.
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
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/skamalj/agent-wait/blob/main/LICENSE)
30
37
  [![Docs](https://img.shields.io/badge/docs-skamalj.github.io%2Fagent--wait-black.svg)](https://skamalj.github.io/agent-wait/)
31
38
 
32
- **Get a LangGraph interrupt out of the process, and the answer back in.**
39
+ **Make an agent wait for something outside the process — and get the answer back in.**
33
40
 
34
- When a node calls `interrupt()`, the graph pauses and the interrupt is handed to whatever
35
- called `invoke()` — and that is where LangGraph stops. There is no built-in way to tell
36
- anyone *else* that a question was asked, and no built-in way for anyone else to answer
37
- it. The moment the question has to reach a person on Slack, an approvals dashboard, a
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
- agent-wait is that code. It takes the interrupt and puts it somewhere people can see it —
42
- a topic, a queue, a webhook, a database row — with everything needed to answer it in one
43
- envelope, and documents the shape of the answer so the return leg is one `if`.
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 langgraph-wait # core + LangGraph
47
- pip install agent-wait-aws # SNS / SQS / EventBridge / DynamoDB announcers
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
- @hitl(FINANCE)
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
- @hitl(FINANCE)
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 — `@hitl(FINANCE, decision="verdict")` looks for `verdict`.
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 agent_wait_aws import SnsAnnounce
104
- from langgraph_wait import publish_interrupts
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 agent_wait_aws import DynamoDbAnnounce, SnsAnnounce
151
+ from agent_wait.aws import DynamoDbAnnounce, SnsAnnounce
136
152
 
137
153
 
138
154
  @tool
139
- @hitl(FINANCE, mode="async", announce=[SnsAnnounce(topic_arn), DynamoDbAnnounce(table)])
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 `@hitl` function receives — verbatim.
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`, `InMemoryAnnounce`,
195
- and in `agent-wait-aws`: `SnsAnnounce`, `SqsAnnounce`, `EventBridgeAnnounce`,
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; `@hitl` interrupts inside the call. Both on one tool
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
- packages/agent-wait core: Question, WaitPolicy, the envelope, publish(), announcers. No deps.
224
- packages/langgraph-wait @hitl and publish_interrupts(). The only LangGraph import.
225
- packages/agent-wait-aws four announce adapters, and a CDK stack for the example.
226
- examples/refund_agent a graph and a host, written for Lambda behind SQS.
227
- docs/ message contract, architecture, announcer guide, consumer guide.
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
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/skamalj/agent-wait/blob/main/LICENSE)
6
6
  [![Docs](https://img.shields.io/badge/docs-skamalj.github.io%2Fagent--wait-black.svg)](https://skamalj.github.io/agent-wait/)
7
7
 
8
- **Get a LangGraph interrupt out of the process, and the answer back in.**
8
+ **Make an agent wait for something outside the process — and get the answer back in.**
9
9
 
10
- When a node calls `interrupt()`, the graph pauses and the interrupt is handed to whatever
11
- called `invoke()` — and that is where LangGraph stops. There is no built-in way to tell
12
- anyone *else* that a question was asked, and no built-in way for anyone else to answer
13
- it. The moment the question has to reach a person on Slack, an approvals dashboard, a
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
- agent-wait is that code. It takes the interrupt and puts it somewhere people can see it —
18
- a topic, a queue, a webhook, a database row — with everything needed to answer it in one
19
- envelope, and documents the shape of the answer so the return leg is one `if`.
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 langgraph-wait # core + LangGraph
23
- pip install agent-wait-aws # SNS / SQS / EventBridge / DynamoDB announcers
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
- @hitl(FINANCE)
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
- @hitl(FINANCE)
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 — `@hitl(FINANCE, decision="verdict")` looks for `verdict`.
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 agent_wait_aws import SnsAnnounce
80
- from langgraph_wait import publish_interrupts
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 agent_wait_aws import DynamoDbAnnounce, SnsAnnounce
120
+ from agent_wait.aws import DynamoDbAnnounce, SnsAnnounce
112
121
 
113
122
 
114
123
  @tool
115
- @hitl(FINANCE, mode="async", announce=[SnsAnnounce(topic_arn), DynamoDbAnnounce(table)])
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 `@hitl` function receives — verbatim.
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`, `InMemoryAnnounce`,
171
- and in `agent-wait-aws`: `SnsAnnounce`, `SqsAnnounce`, `EventBridgeAnnounce`,
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; `@hitl` interrupts inside the call. Both on one tool
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
- packages/agent-wait core: Question, WaitPolicy, the envelope, publish(), announcers. No deps.
200
- packages/langgraph-wait @hitl and publish_interrupts(). The only LangGraph import.
201
- packages/agent-wait-aws four announce adapters, and a CDK stack for the example.
202
- examples/refund_agent a graph and a host, written for Lambda behind SQS.
203
- docs/ message contract, architecture, announcer guide, consumer guide.
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