agent-wait 0.2.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.
@@ -0,0 +1,19 @@
1
+ *.egg-info/
2
+ *.pyc
3
+ .coverage
4
+ .env
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .scratch/
8
+ .venv/
9
+ __pycache__/
10
+ build/
11
+ cdk.out/
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/
@@ -0,0 +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.
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-wait
3
+ Version: 0.2.0
4
+ Summary: 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.
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
+ **Publish a LangGraph agent's interrupts to the outside world, so a human can answer them.**
32
+
33
+ A LangGraph node calls `interrupt()` and the graph stops. If the agent runs in a Lambda,
34
+ a container, or anything else that doesn't stick around, the process exits and nobody
35
+ knows a question was asked or where to send the answer. agent-wait takes that pause and
36
+ puts it somewhere people can see it — a topic, a queue, a webhook, a database row — with
37
+ everything needed to answer it in one envelope.
38
+
39
+ It does not receive the answer. That part is yours, and it is about a dozen lines.
40
+
41
+ ```bash
42
+ pip install agent-wait langgraph-wait # core + LangGraph
43
+ pip install agent-wait-aws # SNS / SQS / EventBridge / DynamoDB announcers
44
+ ```
45
+
46
+ ## The whole thing
47
+
48
+ **In the graph** — one line, where the decision belongs:
49
+
50
+ ```python
51
+ from agent_wait import WaitPolicy
52
+ from langgraph_wait import ask
53
+
54
+
55
+ def review(state):
56
+ if state["amount"] <= 5_000:
57
+ return {"decision": {"action": "approve", "by": "policy:auto"}}
58
+
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", "reason": "no response in 3 days"},
64
+ allowed_actions=("approve", "reject"),
65
+ tags={"approver_group": "finance"},
66
+ ),
67
+ )
68
+ return {"decision": decision}
69
+ ```
70
+
71
+ `ask()` is a thin wrapper over `interrupt()`. The node pauses exactly as LangGraph pauses;
72
+ what `ask()` adds is the policy, which rides along and comes back out in the envelope. A
73
+ plain `interrupt(value)` works too, with default policy — a graph that already interrupts
74
+ gets published with no edit at all.
75
+
76
+ **In the host** — wire it once:
77
+
78
+ ```python
79
+ from agent_wait import WaitPublisher
80
+ from agent_wait_aws import SnsAnnounce
81
+ from langgraph_wait import LangGraphAdapter
82
+
83
+ agent = WaitPublisher(LangGraphAdapter(graph), announce=[SnsAnnounce(topic_arn)])
84
+ ```
85
+
86
+ **Then route each message.** Starts and answers arrive at the same place; `interrupt_id`
87
+ tells them apart:
88
+
89
+ ```python
90
+ from langgraph_wait import is_answer, resume_command
91
+
92
+
93
+ def route(message):
94
+ thread_id = message["thread_id"]
95
+ if is_answer(message):
96
+ if not is_still_open(thread_id, message["interrupt_id"]):
97
+ return # somebody already answered
98
+ return agent.invoke(resume_command(message), thread_id)
99
+ if agent.pending(thread_id):
100
+ return agent.republish(thread_id) # a redelivery; don't re-ask
101
+ return agent.invoke(message["input"], thread_id)
102
+
103
+
104
+ def is_still_open(thread_id, interrupt_id):
105
+ return any(p.interrupt_id == interrupt_id for p in agent.pending(thread_id))
106
+ ```
107
+
108
+ That is the complete integration. [`examples/refund_agent/`](https://github.com/skamalj/agent-wait/tree/main/examples/refund_agent)
109
+ is it, deployed to Lambda behind SQS.
110
+
111
+ ## What goes out
112
+
113
+ ```json
114
+ {
115
+ "type": "wait.created",
116
+ "thread_id": "order-4471",
117
+ "interrupt_id": "a1b2c3d4e5f60718",
118
+ "question": { "kind": "refund_approval", "amount": 41000 },
119
+ "allowed_actions": ["approve", "reject"],
120
+ "expires_at": "2026-09-12T09:00:00Z",
121
+ "default": { "action": "reject", "reason": "no response in 3 days" },
122
+ "reply_with": { "thread_id": "order-4471", "interrupt_id": "a1b2c3d4e5f60718", "answer": null }
123
+ }
124
+ ```
125
+
126
+ `reply_with` is a filled-in stub: the consumer copies it, sets `answer`, and posts it to
127
+ wherever your agent listens. Whatever goes in `answer` is what the `ask()` call returns —
128
+ verbatim, with nothing merged into it.
129
+
130
+ A second envelope, `wait.resumed`, goes out when the graph moves past the question, so a
131
+ UI knows to retract the button.
132
+
133
+ Full schema, including how to deduplicate:
134
+ [Message formats](https://skamalj.github.io/agent-wait/message-formats/).
135
+
136
+ ## Announcers
137
+
138
+ An announcer is the only thing you are expected to implement. Subclass `BaseAnnounce`
139
+ and write one method:
140
+
141
+ ```python
142
+ from agent_wait import BaseAnnounce
143
+
144
+
145
+ class RedisAnnounce(BaseAnnounce):
146
+ name = "redis"
147
+
148
+ def __init__(self, client, **kw):
149
+ super().__init__(**kw)
150
+ self.client = client
151
+
152
+ def deliver(self, envelope, transition):
153
+ self.client.set(envelope.dedupe_key, envelope.to_json())
154
+ ```
155
+
156
+ The contract — **an announcer must never raise into the run** — is enforced by the base
157
+ class: an exception from `deliver()` becomes a log line, and the graph that just parked
158
+ stays parked.
159
+
160
+ Because nothing reads state back through this library, "announce" doesn't have to mean
161
+ "publish an event". It means *put the question where whoever answers it will find it*:
162
+
163
+ | Adapter | Package | Where the question lands |
164
+ |---|---|---|
165
+ | `WebhookAnnounce` | `agent-wait` | A URL. JSON POST, optional HMAC-SHA256 signature in the GitHub/Stripe shape. Stdlib only. |
166
+ | `LogAnnounce` | `agent-wait` | A structured log line. The question never reaches INFO. |
167
+ | `InMemoryAnnounce` | `agent-wait` | A list. For tests. |
168
+ | `SnsAnnounce` | `agent-wait-aws` | A topic; policy `tags` become message attributes for subscription filters. |
169
+ | `SqsAnnounce` | `agent-wait-aws` | A queue; on FIFO, grouped by thread and deduplicated on the stable key. |
170
+ | `EventBridgeAnnounce` | `agent-wait-aws` | A bus, with the transition as detail-type. Notices partial failures behind a 200. |
171
+ | `DynamoDbAnnounce` | `agent-wait-aws` | **A row.** `open` on `created`, `closed` on `resumed`. A GSI on `status` gives an approvals UI its query with no broker anywhere. |
172
+
173
+ Pass as many as you like; failures are contained per adapter.
174
+
175
+ ## What the library does *not* do
176
+
177
+ Deliberately — each of these is where teams' own opinions live:
178
+
179
+ - **Receive answers.** No inbound endpoint, no validation, no tokens. The router above is yours.
180
+ - **Enforce the timeout.** `expires_at` and `default` are published; a sweep of yours
181
+ sends the default when the deadline passes. There is a
182
+ [working one](https://github.com/skamalj/agent-wait/blob/main/examples/refund_agent/demo_scenarios.py)
183
+ in the example.
184
+ - **Decide a race.** `pending()` rejects an answer the graph has already moved past.
185
+ Two *different* answers in the same instant are your transport's problem — SQS FIFO
186
+ keyed by thread solves it; an HTTP endpoint with concurrent handlers needs a
187
+ conditional write.
188
+ - **Authenticate.** Whoever can write to your entry point can answer.
189
+ - **Store anything.** LangGraph's checkpoint is the only state.
190
+
191
+ ## Two LangGraph 1.2.x behaviours you should know about
192
+
193
+ Both verified against 1.2.11, both pinned by tests that fail if LangGraph changes them.
194
+
195
+ **`get_state().tasks[*].interrupts` over-reports** ([#4796](https://github.com/langchain-ai/langgraph/issues/4796),
196
+ [#6792](https://github.com/langchain-ai/langgraph/issues/6792)). Resume one of two parallel
197
+ interrupts and the finished task still lists its id. `pending()` filters on `task.result`,
198
+ which is `None` only while genuinely parked.
199
+
200
+ **Two interrupting tools in one `ToolNode` get the same id** ([#6626](https://github.com/langchain-ai/langgraph/issues/6626),
201
+ [#6624](https://github.com/langchain-ai/langgraph/issues/6624)). A different question under
202
+ an identical id defeats deduplication, and there is no filter for it. The rule is **one
203
+ `interrupt()` per node** — give each approval-requiring tool its own node, which is also
204
+ the fix for a node re-running its side effects on resume.
205
+
206
+ Details: [Architecture](https://skamalj.github.io/agent-wait/architecture/).
207
+
208
+ ## Layout
209
+
210
+ ```
211
+ packages/agent-wait core. No LangGraph, no AWS, no dependencies. pyright strict.
212
+ packages/langgraph-wait ask(), the adapter, resume_command(). The only LangGraph import.
213
+ packages/agent-wait-aws four announce adapters, and a CDK stack.
214
+ examples/refund_agent a graph, a router, and four scenarios against real AWS.
215
+ docs/ message contract, architecture, consumer guide.
216
+ ```
217
+
218
+ ```bash
219
+ uv sync
220
+ uv run pytest
221
+ uv run ruff check . && uv run pyright
222
+ ```
223
+
224
+ MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
@@ -0,0 +1,200 @@
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
+ **Publish a LangGraph agent's interrupts to the outside world, so a human can answer them.**
8
+
9
+ A LangGraph node calls `interrupt()` and the graph stops. If the agent runs in a Lambda,
10
+ a container, or anything else that doesn't stick around, the process exits and nobody
11
+ knows a question was asked or where to send the answer. agent-wait takes that pause and
12
+ puts it somewhere people can see it — a topic, a queue, a webhook, a database row — with
13
+ everything needed to answer it in one envelope.
14
+
15
+ It does not receive the answer. That part is yours, and it is about a dozen lines.
16
+
17
+ ```bash
18
+ pip install agent-wait langgraph-wait # core + LangGraph
19
+ pip install agent-wait-aws # SNS / SQS / EventBridge / DynamoDB announcers
20
+ ```
21
+
22
+ ## The whole thing
23
+
24
+ **In the graph** — one line, where the decision belongs:
25
+
26
+ ```python
27
+ from agent_wait import WaitPolicy
28
+ from langgraph_wait import ask
29
+
30
+
31
+ def review(state):
32
+ if state["amount"] <= 5_000:
33
+ return {"decision": {"action": "approve", "by": "policy:auto"}}
34
+
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", "reason": "no response in 3 days"},
40
+ allowed_actions=("approve", "reject"),
41
+ tags={"approver_group": "finance"},
42
+ ),
43
+ )
44
+ return {"decision": decision}
45
+ ```
46
+
47
+ `ask()` is a thin wrapper over `interrupt()`. The node pauses exactly as LangGraph pauses;
48
+ what `ask()` adds is the policy, which rides along and comes back out in the envelope. A
49
+ plain `interrupt(value)` works too, with default policy — a graph that already interrupts
50
+ gets published with no edit at all.
51
+
52
+ **In the host** — wire it once:
53
+
54
+ ```python
55
+ from agent_wait import WaitPublisher
56
+ from agent_wait_aws import SnsAnnounce
57
+ from langgraph_wait import LangGraphAdapter
58
+
59
+ agent = WaitPublisher(LangGraphAdapter(graph), announce=[SnsAnnounce(topic_arn)])
60
+ ```
61
+
62
+ **Then route each message.** Starts and answers arrive at the same place; `interrupt_id`
63
+ tells them apart:
64
+
65
+ ```python
66
+ from langgraph_wait import is_answer, resume_command
67
+
68
+
69
+ def route(message):
70
+ thread_id = message["thread_id"]
71
+ if is_answer(message):
72
+ if not is_still_open(thread_id, message["interrupt_id"]):
73
+ return # somebody already answered
74
+ return agent.invoke(resume_command(message), thread_id)
75
+ if agent.pending(thread_id):
76
+ return agent.republish(thread_id) # a redelivery; don't re-ask
77
+ return agent.invoke(message["input"], thread_id)
78
+
79
+
80
+ def is_still_open(thread_id, interrupt_id):
81
+ return any(p.interrupt_id == interrupt_id for p in agent.pending(thread_id))
82
+ ```
83
+
84
+ That is the complete integration. [`examples/refund_agent/`](https://github.com/skamalj/agent-wait/tree/main/examples/refund_agent)
85
+ is it, deployed to Lambda behind SQS.
86
+
87
+ ## What goes out
88
+
89
+ ```json
90
+ {
91
+ "type": "wait.created",
92
+ "thread_id": "order-4471",
93
+ "interrupt_id": "a1b2c3d4e5f60718",
94
+ "question": { "kind": "refund_approval", "amount": 41000 },
95
+ "allowed_actions": ["approve", "reject"],
96
+ "expires_at": "2026-09-12T09:00:00Z",
97
+ "default": { "action": "reject", "reason": "no response in 3 days" },
98
+ "reply_with": { "thread_id": "order-4471", "interrupt_id": "a1b2c3d4e5f60718", "answer": null }
99
+ }
100
+ ```
101
+
102
+ `reply_with` is a filled-in stub: the consumer copies it, sets `answer`, and posts it to
103
+ wherever your agent listens. Whatever goes in `answer` is what the `ask()` call returns —
104
+ verbatim, with nothing merged into it.
105
+
106
+ A second envelope, `wait.resumed`, goes out when the graph moves past the question, so a
107
+ UI knows to retract the button.
108
+
109
+ Full schema, including how to deduplicate:
110
+ [Message formats](https://skamalj.github.io/agent-wait/message-formats/).
111
+
112
+ ## Announcers
113
+
114
+ An announcer is the only thing you are expected to implement. Subclass `BaseAnnounce`
115
+ and write one method:
116
+
117
+ ```python
118
+ from agent_wait import BaseAnnounce
119
+
120
+
121
+ class RedisAnnounce(BaseAnnounce):
122
+ name = "redis"
123
+
124
+ def __init__(self, client, **kw):
125
+ super().__init__(**kw)
126
+ self.client = client
127
+
128
+ def deliver(self, envelope, transition):
129
+ self.client.set(envelope.dedupe_key, envelope.to_json())
130
+ ```
131
+
132
+ The contract — **an announcer must never raise into the run** — is enforced by the base
133
+ class: an exception from `deliver()` becomes a log line, and the graph that just parked
134
+ stays parked.
135
+
136
+ Because nothing reads state back through this library, "announce" doesn't have to mean
137
+ "publish an event". It means *put the question where whoever answers it will find it*:
138
+
139
+ | Adapter | Package | Where the question lands |
140
+ |---|---|---|
141
+ | `WebhookAnnounce` | `agent-wait` | A URL. JSON POST, optional HMAC-SHA256 signature in the GitHub/Stripe shape. Stdlib only. |
142
+ | `LogAnnounce` | `agent-wait` | A structured log line. The question never reaches INFO. |
143
+ | `InMemoryAnnounce` | `agent-wait` | A list. For tests. |
144
+ | `SnsAnnounce` | `agent-wait-aws` | A topic; policy `tags` become message attributes for subscription filters. |
145
+ | `SqsAnnounce` | `agent-wait-aws` | A queue; on FIFO, grouped by thread and deduplicated on the stable key. |
146
+ | `EventBridgeAnnounce` | `agent-wait-aws` | A bus, with the transition as detail-type. Notices partial failures behind a 200. |
147
+ | `DynamoDbAnnounce` | `agent-wait-aws` | **A row.** `open` on `created`, `closed` on `resumed`. A GSI on `status` gives an approvals UI its query with no broker anywhere. |
148
+
149
+ Pass as many as you like; failures are contained per adapter.
150
+
151
+ ## What the library does *not* do
152
+
153
+ Deliberately — each of these is where teams' own opinions live:
154
+
155
+ - **Receive answers.** No inbound endpoint, no validation, no tokens. The router above is yours.
156
+ - **Enforce the timeout.** `expires_at` and `default` are published; a sweep of yours
157
+ sends the default when the deadline passes. There is a
158
+ [working one](https://github.com/skamalj/agent-wait/blob/main/examples/refund_agent/demo_scenarios.py)
159
+ in the example.
160
+ - **Decide a race.** `pending()` rejects an answer the graph has already moved past.
161
+ Two *different* answers in the same instant are your transport's problem — SQS FIFO
162
+ keyed by thread solves it; an HTTP endpoint with concurrent handlers needs a
163
+ conditional write.
164
+ - **Authenticate.** Whoever can write to your entry point can answer.
165
+ - **Store anything.** LangGraph's checkpoint is the only state.
166
+
167
+ ## Two LangGraph 1.2.x behaviours you should know about
168
+
169
+ Both verified against 1.2.11, both pinned by tests that fail if LangGraph changes them.
170
+
171
+ **`get_state().tasks[*].interrupts` over-reports** ([#4796](https://github.com/langchain-ai/langgraph/issues/4796),
172
+ [#6792](https://github.com/langchain-ai/langgraph/issues/6792)). Resume one of two parallel
173
+ interrupts and the finished task still lists its id. `pending()` filters on `task.result`,
174
+ which is `None` only while genuinely parked.
175
+
176
+ **Two interrupting tools in one `ToolNode` get the same id** ([#6626](https://github.com/langchain-ai/langgraph/issues/6626),
177
+ [#6624](https://github.com/langchain-ai/langgraph/issues/6624)). A different question under
178
+ an identical id defeats deduplication, and there is no filter for it. The rule is **one
179
+ `interrupt()` per node** — give each approval-requiring tool its own node, which is also
180
+ the fix for a node re-running its side effects on resume.
181
+
182
+ Details: [Architecture](https://skamalj.github.io/agent-wait/architecture/).
183
+
184
+ ## Layout
185
+
186
+ ```
187
+ packages/agent-wait core. No LangGraph, no AWS, no dependencies. pyright strict.
188
+ packages/langgraph-wait ask(), the adapter, resume_command(). The only LangGraph import.
189
+ packages/agent-wait-aws four announce adapters, and a CDK stack.
190
+ examples/refund_agent a graph, a router, and four scenarios against real AWS.
191
+ docs/ message contract, architecture, consumer guide.
192
+ ```
193
+
194
+ ```bash
195
+ uv sync
196
+ uv run pytest
197
+ uv run ruff check . && uv run pyright
198
+ ```
199
+
200
+ MIT. Issues and PRs at [github.com/skamalj/agent-wait](https://github.com/skamalj/agent-wait).
@@ -0,0 +1,38 @@
1
+ [project]
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."
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 = ["agents", "langgraph", "human-in-the-loop", "interrupt", "approval", "serverless", "durable"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Topic :: Software Development :: Libraries",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://skamalj.github.io/agent-wait/"
25
+ Documentation = "https://skamalj.github.io/agent-wait/"
26
+ Source = "https://github.com/skamalj/agent-wait"
27
+ Issues = "https://github.com/skamalj/agent-wait/issues"
28
+ Changelog = "https://github.com/skamalj/agent-wait/blob/main/CHANGELOG.md"
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/agent_wait"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["src/agent_wait", "README.md", "LICENSE"]
@@ -0,0 +1,81 @@
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
+ ]
@@ -0,0 +1,16 @@
1
+ from .base import AnnounceAdapter, BaseAnnounce
2
+ from .composite import CompositeAnnounce
3
+ from .log import LogAnnounce
4
+ from .memory import FailingAnnounce, InMemoryAnnounce
5
+ from .webhook import WebhookAnnounce, verify_signature
6
+
7
+ __all__ = [
8
+ "AnnounceAdapter",
9
+ "BaseAnnounce",
10
+ "CompositeAnnounce",
11
+ "FailingAnnounce",
12
+ "InMemoryAnnounce",
13
+ "LogAnnounce",
14
+ "WebhookAnnounce",
15
+ "verify_signature",
16
+ ]