agent-wait 0.6.0__tar.gz → 0.7.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.6.0 → agent_wait-0.7.0}/PKG-INFO +49 -8
- {agent_wait-0.6.0 → agent_wait-0.7.0}/README.md +43 -5
- {agent_wait-0.6.0 → agent_wait-0.7.0}/pyproject.toml +5 -3
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/__init__.py +5 -5
- agent_wait-0.7.0/src/agent_wait/strands/__init__.py +93 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/.gitignore +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/LICENSE +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/reference/wait-sim/README.md +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/announce/__init__.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/announce/base.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/announce/composite.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/announce/log.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/announce/memory.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/announce/webhook.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/aws/__init__.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/aws/dynamodb.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/aws/eventbridge.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/aws/sns.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/aws/sqs.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/errors.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/framework.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/langgraph/__init__.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/model.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/policy.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/publish.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/py.typed +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/pydantic_ai/__init__.py +0 -0
- {agent_wait-0.6.0 → agent_wait-0.7.0}/src/agent_wait/wait.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-wait
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Human-in-the-loop for LangGraph
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Human-in-the-loop for LangGraph, Pydantic AI and Strands 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,callback,durable,dynamodb,eventbridge,hitl,human-in-the-loop,interrupt,lambda,langchain,langgraph,pydantic-ai,serverless,sns,sqs,webhook
|
|
13
|
+
Keywords: agents,approval,callback,durable,dynamodb,eventbridge,hitl,human-in-the-loop,interrupt,lambda,langchain,langgraph,pydantic-ai,serverless,sns,sqs,strands,webhook
|
|
14
14
|
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -24,12 +24,15 @@ Provides-Extra: all
|
|
|
24
24
|
Requires-Dist: boto3>=1.35; extra == 'all'
|
|
25
25
|
Requires-Dist: langgraph<2,>=1.2; extra == 'all'
|
|
26
26
|
Requires-Dist: pydantic-ai-slim<3,>=2.43; extra == 'all'
|
|
27
|
+
Requires-Dist: strands-agents<2,>=1.55; extra == 'all'
|
|
27
28
|
Provides-Extra: aws
|
|
28
29
|
Requires-Dist: boto3>=1.35; extra == 'aws'
|
|
29
30
|
Provides-Extra: langgraph
|
|
30
31
|
Requires-Dist: langgraph<2,>=1.2; extra == 'langgraph'
|
|
31
32
|
Provides-Extra: pydantic-ai
|
|
32
33
|
Requires-Dist: pydantic-ai-slim<3,>=2.43; extra == 'pydantic-ai'
|
|
34
|
+
Provides-Extra: strands
|
|
35
|
+
Requires-Dist: strands-agents<2,>=1.55; extra == 'strands'
|
|
33
36
|
Description-Content-Type: text/markdown
|
|
34
37
|
|
|
35
38
|
# agent-wait
|
|
@@ -46,8 +49,8 @@ deploy. But the same shape covers a vendor callback, a payment processor, a KYC
|
|
|
46
49
|
another agent — anything the run cannot continue without, and nothing inside the process
|
|
47
50
|
can supply.
|
|
48
51
|
|
|
49
|
-
LangGraph has `interrupt()` for this; Pydantic AI has `ApprovalRequired
|
|
50
|
-
run pauses and the question is handed to whatever called it — and that is where the
|
|
52
|
+
LangGraph has `interrupt()` for this; Pydantic AI has `ApprovalRequired`; Strands has
|
|
53
|
+
`tool_context.interrupt()`. In all of them the run pauses and the question is handed to whatever called it — and that is where the
|
|
51
54
|
framework stops. There is no built-in way to tell anyone *else* that a question was asked,
|
|
52
55
|
and no built-in way for anyone else to answer it. The moment the question has to reach
|
|
53
56
|
Slack, an approvals dashboard, a ticket queue or another service, you are writing that
|
|
@@ -61,11 +64,14 @@ it in one envelope, and documents the shape of the answer so the return leg is o
|
|
|
61
64
|
```bash
|
|
62
65
|
pip install "agent-wait[langgraph]" # @wait + publish_interrupts for LangGraph
|
|
63
66
|
pip install "agent-wait[pydantic-ai]" # the same two names for Pydantic AI
|
|
67
|
+
pip install "agent-wait[strands]" # ... and for Strands Agents
|
|
64
68
|
pip install "agent-wait[langgraph,aws]" # + SNS / SQS / EventBridge / DynamoDB announcers
|
|
65
69
|
```
|
|
66
70
|
|
|
67
71
|
The bare `agent-wait` is the framework-free core (policy, envelope, webhook and in-memory
|
|
68
|
-
announcers) and depends on nothing. Each extra pulls in exactly one framework or provider
|
|
72
|
+
announcers) and depends on nothing. Each extra pulls in exactly one framework or provider —
|
|
73
|
+
the full list, with what each framework leaves to you:
|
|
74
|
+
[Frameworks and providers](https://skamalj.github.io/agent-wait/frameworks-and-providers/).
|
|
69
75
|
|
|
70
76
|
## The whole thing
|
|
71
77
|
|
|
@@ -194,6 +200,40 @@ only if the history you pass already holds the tool's return. `requires_approval
|
|
|
194
200
|
tools and `CallDeferred` calls (a job handed to an external system) are published too,
|
|
195
201
|
with the default policy.
|
|
196
202
|
|
|
203
|
+
## The same two names on Strands
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from agent_wait.strands import publish_interrupts, wait
|
|
207
|
+
from strands import Agent, tool
|
|
208
|
+
from strands.session.file_session_manager import FileSessionManager
|
|
209
|
+
from strands.types.tools import ToolContext
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@tool(context=True)
|
|
213
|
+
@wait(FINANCE)
|
|
214
|
+
def issue_refund(order_id: str, amount: int, tool_context: ToolContext) -> str:
|
|
215
|
+
payments.refund(order_id, amount)
|
|
216
|
+
return "refunded"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
agent = Agent(tools=[issue_refund], session_manager=FileSessionManager(session_id=thread_id))
|
|
220
|
+
result = agent("refund order 4471")
|
|
221
|
+
publish_interrupts(result, agent.session_id, announce=[SnsAnnounce(topic_arn)])
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The run stops with `result.stop_reason == "interrupt"`; `question_id` is the interrupt id
|
|
225
|
+
(one per tool call). The answer goes back through the framework's own resume, verbatim:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
agent = Agent(tools=[issue_refund], session_manager=FileSessionManager(session_id=thread_id))
|
|
229
|
+
result = agent([{"interruptResponse": {"interruptId": question_id, "response": answer}}])
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Two rules the framework leaves to you: `@tool(context=True)` with the parameter named
|
|
233
|
+
`tool_context`, and a session manager — that is what keeps the parked question alive
|
|
234
|
+
between the return and the resume. A second answer after the run has moved on raises
|
|
235
|
+
(`ValueError`), so a host that may see duplicates checks `stop_reason` first or catches.
|
|
236
|
+
|
|
197
237
|
## Async mode — the thread does not park
|
|
198
238
|
|
|
199
239
|
```python
|
|
@@ -291,14 +331,15 @@ agent_wait core: WaitPolicy, Question, the envelope, publish(), Bas
|
|
|
291
331
|
Webhook/Log/InMemory announcers, and the Framework interface. No deps.
|
|
292
332
|
agent_wait.langgraph [langgraph] @wait and publish_interrupts() bound to LangGraph.
|
|
293
333
|
agent_wait.pydantic_ai [pydantic-ai] the same two names bound to Pydantic AI's deferred tools.
|
|
334
|
+
agent_wait.strands [strands] the same two names bound to Strands' tool_context.interrupt().
|
|
294
335
|
agent_wait.aws [aws] SnsAnnounce, SqsAnnounce, EventBridgeAnnounce, DynamoDbAnnounce.
|
|
295
336
|
examples/refund_agent a graph, a host written for Lambda behind SQS, and its CDK stack.
|
|
296
337
|
docs/ message contract, architecture, announcer guide, consumer guide.
|
|
297
338
|
```
|
|
298
339
|
|
|
299
340
|
`@wait` and `publish_interrupts` are written once against a three-method `Framework`
|
|
300
|
-
interface; `agent_wait.langgraph` and `agent_wait.
|
|
301
|
-
|
|
341
|
+
interface; `agent_wait.langgraph`, `agent_wait.pydantic_ai` and `agent_wait.strands` are
|
|
342
|
+
implementors of it, thirty to fifty lines each. Announcers are the same split: `BaseAnnounce` is the interface,
|
|
302
343
|
each provider subpackage the implementors. See
|
|
303
344
|
[Architecture](https://skamalj.github.io/agent-wait/architecture/).
|
|
304
345
|
|
|
@@ -12,8 +12,8 @@ deploy. But the same shape covers a vendor callback, a payment processor, a KYC
|
|
|
12
12
|
another agent — anything the run cannot continue without, and nothing inside the process
|
|
13
13
|
can supply.
|
|
14
14
|
|
|
15
|
-
LangGraph has `interrupt()` for this; Pydantic AI has `ApprovalRequired
|
|
16
|
-
run pauses and the question is handed to whatever called it — and that is where the
|
|
15
|
+
LangGraph has `interrupt()` for this; Pydantic AI has `ApprovalRequired`; Strands has
|
|
16
|
+
`tool_context.interrupt()`. In all of them the run pauses and the question is handed to whatever called it — and that is where the
|
|
17
17
|
framework stops. There is no built-in way to tell anyone *else* that a question was asked,
|
|
18
18
|
and no built-in way for anyone else to answer it. The moment the question has to reach
|
|
19
19
|
Slack, an approvals dashboard, a ticket queue or another service, you are writing that
|
|
@@ -27,11 +27,14 @@ it in one envelope, and documents the shape of the answer so the return leg is o
|
|
|
27
27
|
```bash
|
|
28
28
|
pip install "agent-wait[langgraph]" # @wait + publish_interrupts for LangGraph
|
|
29
29
|
pip install "agent-wait[pydantic-ai]" # the same two names for Pydantic AI
|
|
30
|
+
pip install "agent-wait[strands]" # ... and for Strands Agents
|
|
30
31
|
pip install "agent-wait[langgraph,aws]" # + SNS / SQS / EventBridge / DynamoDB announcers
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
The bare `agent-wait` is the framework-free core (policy, envelope, webhook and in-memory
|
|
34
|
-
announcers) and depends on nothing. Each extra pulls in exactly one framework or provider
|
|
35
|
+
announcers) and depends on nothing. Each extra pulls in exactly one framework or provider —
|
|
36
|
+
the full list, with what each framework leaves to you:
|
|
37
|
+
[Frameworks and providers](https://skamalj.github.io/agent-wait/frameworks-and-providers/).
|
|
35
38
|
|
|
36
39
|
## The whole thing
|
|
37
40
|
|
|
@@ -160,6 +163,40 @@ only if the history you pass already holds the tool's return. `requires_approval
|
|
|
160
163
|
tools and `CallDeferred` calls (a job handed to an external system) are published too,
|
|
161
164
|
with the default policy.
|
|
162
165
|
|
|
166
|
+
## The same two names on Strands
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from agent_wait.strands import publish_interrupts, wait
|
|
170
|
+
from strands import Agent, tool
|
|
171
|
+
from strands.session.file_session_manager import FileSessionManager
|
|
172
|
+
from strands.types.tools import ToolContext
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@tool(context=True)
|
|
176
|
+
@wait(FINANCE)
|
|
177
|
+
def issue_refund(order_id: str, amount: int, tool_context: ToolContext) -> str:
|
|
178
|
+
payments.refund(order_id, amount)
|
|
179
|
+
return "refunded"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
agent = Agent(tools=[issue_refund], session_manager=FileSessionManager(session_id=thread_id))
|
|
183
|
+
result = agent("refund order 4471")
|
|
184
|
+
publish_interrupts(result, agent.session_id, announce=[SnsAnnounce(topic_arn)])
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The run stops with `result.stop_reason == "interrupt"`; `question_id` is the interrupt id
|
|
188
|
+
(one per tool call). The answer goes back through the framework's own resume, verbatim:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
agent = Agent(tools=[issue_refund], session_manager=FileSessionManager(session_id=thread_id))
|
|
192
|
+
result = agent([{"interruptResponse": {"interruptId": question_id, "response": answer}}])
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Two rules the framework leaves to you: `@tool(context=True)` with the parameter named
|
|
196
|
+
`tool_context`, and a session manager — that is what keeps the parked question alive
|
|
197
|
+
between the return and the resume. A second answer after the run has moved on raises
|
|
198
|
+
(`ValueError`), so a host that may see duplicates checks `stop_reason` first or catches.
|
|
199
|
+
|
|
163
200
|
## Async mode — the thread does not park
|
|
164
201
|
|
|
165
202
|
```python
|
|
@@ -257,14 +294,15 @@ agent_wait core: WaitPolicy, Question, the envelope, publish(), Bas
|
|
|
257
294
|
Webhook/Log/InMemory announcers, and the Framework interface. No deps.
|
|
258
295
|
agent_wait.langgraph [langgraph] @wait and publish_interrupts() bound to LangGraph.
|
|
259
296
|
agent_wait.pydantic_ai [pydantic-ai] the same two names bound to Pydantic AI's deferred tools.
|
|
297
|
+
agent_wait.strands [strands] the same two names bound to Strands' tool_context.interrupt().
|
|
260
298
|
agent_wait.aws [aws] SnsAnnounce, SqsAnnounce, EventBridgeAnnounce, DynamoDbAnnounce.
|
|
261
299
|
examples/refund_agent a graph, a host written for Lambda behind SQS, and its CDK stack.
|
|
262
300
|
docs/ message contract, architecture, announcer guide, consumer guide.
|
|
263
301
|
```
|
|
264
302
|
|
|
265
303
|
`@wait` and `publish_interrupts` are written once against a three-method `Framework`
|
|
266
|
-
interface; `agent_wait.langgraph` and `agent_wait.
|
|
267
|
-
|
|
304
|
+
interface; `agent_wait.langgraph`, `agent_wait.pydantic_ai` and `agent_wait.strands` are
|
|
305
|
+
implementors of it, thirty to fifty lines each. Announcers are the same split: `BaseAnnounce` is the interface,
|
|
268
306
|
each provider subpackage the implementors. See
|
|
269
307
|
[Architecture](https://skamalj.github.io/agent-wait/architecture/).
|
|
270
308
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-wait"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Human-in-the-loop for LangGraph
|
|
3
|
+
version = "0.7.0"
|
|
4
|
+
description = "Human-in-the-loop for LangGraph, Pydantic AI and Strands 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
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = "MIT"
|
|
@@ -14,6 +14,7 @@ keywords = [
|
|
|
14
14
|
"langgraph",
|
|
15
15
|
"langchain",
|
|
16
16
|
"pydantic-ai",
|
|
17
|
+
"strands",
|
|
17
18
|
"agents",
|
|
18
19
|
"interrupt",
|
|
19
20
|
"durable",
|
|
@@ -41,8 +42,9 @@ dependencies = []
|
|
|
41
42
|
[project.optional-dependencies]
|
|
42
43
|
langgraph = ["langgraph>=1.2,<2"]
|
|
43
44
|
pydantic-ai = ["pydantic-ai-slim>=2.43,<3"]
|
|
45
|
+
strands = ["strands-agents>=1.55,<2"]
|
|
44
46
|
aws = ["boto3>=1.35"]
|
|
45
|
-
all = ["agent-wait[langgraph,pydantic-ai,aws]"]
|
|
47
|
+
all = ["agent-wait[langgraph,pydantic-ai,strands,aws]"]
|
|
46
48
|
|
|
47
49
|
[project.urls]
|
|
48
50
|
Homepage = "https://skamalj.github.io/agent-wait/"
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
publish_interrupts(result, thread_id, announce=[SnsAnnounce(topic_arn)])
|
|
13
13
|
|
|
14
14
|
The core is framework- and provider-neutral: the policy, the question, the envelope,
|
|
15
|
-
`publish()`, and the announcer interface. `agent_wait.langgraph`
|
|
16
|
-
`agent_wait.
|
|
17
|
-
|
|
18
|
-
`pip install agent-wait[langgraph,aws]
|
|
15
|
+
`publish()`, and the announcer interface. `agent_wait.langgraph`, `agent_wait.pydantic_ai`
|
|
16
|
+
and `agent_wait.strands` implement the `Framework` interface for LangGraph, Pydantic AI
|
|
17
|
+
and Strands Agents; `agent_wait.aws` implements announcers for AWS. Install the extras
|
|
18
|
+
you need: `pip install agent-wait[langgraph,aws]`, `[pydantic-ai,aws]`, `[strands,aws]`.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
from .announce import (
|
|
@@ -48,7 +48,7 @@ from .policy import WaitPolicy, parse_duration
|
|
|
48
48
|
from .publish import build_envelope, publish
|
|
49
49
|
from .wait import make_publish_interrupts, make_wait, question_id_for
|
|
50
50
|
|
|
51
|
-
__version__ = "0.
|
|
51
|
+
__version__ = "0.7.0"
|
|
52
52
|
|
|
53
53
|
__all__ = [
|
|
54
54
|
"MAX_QUESTION_BYTES",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Strands Agents implementor of `agent_wait.framework.Framework`, and the bound names.
|
|
2
|
+
|
|
3
|
+
from agent_wait.strands import wait, publish_interrupts
|
|
4
|
+
|
|
5
|
+
Install with `pip install agent-wait[strands]`.
|
|
6
|
+
|
|
7
|
+
## What Strands provides, and what we use
|
|
8
|
+
|
|
9
|
+
- **Park:** a tool declared `@tool(context=True)` receives a `ToolContext`;
|
|
10
|
+
`tool_context.interrupt(name, reason=value)` raises `InterruptException` the first
|
|
11
|
+
time and returns the human's response when the tool runs again after resume. The id
|
|
12
|
+
is `v1:tool_call:<toolUseId>:<uuid5(name)>` -- one per tool call, no collisions.
|
|
13
|
+
- **Read back:** `result.stop_reason == "interrupt"` and `result.interrupts`, each an
|
|
14
|
+
`Interrupt(id, name, reason, response)`. `reason` is what we parked on, verbatim.
|
|
15
|
+
- **Thread:** `agent.session_id` -- the session manager's id when one is attached, a
|
|
16
|
+
random per-instance id otherwise.
|
|
17
|
+
- **Resume** (the host's call, not ours):
|
|
18
|
+
|
|
19
|
+
agent([{"interruptResponse": {"interruptId": question_id, "response": answer}}])
|
|
20
|
+
|
|
21
|
+
The tool runs again, `interrupt()` returns `answer`, and the `@wait` wrapper applies
|
|
22
|
+
its usual rules.
|
|
23
|
+
|
|
24
|
+
## Three things to know
|
|
25
|
+
|
|
26
|
+
- **`@tool(context=True)` and name the parameter `tool_context`.** The wrapper hides it
|
|
27
|
+
from the published `args` by that name; Strands injects it by that name.
|
|
28
|
+
- **A session manager is what makes the wait durable.** Interrupt state lives on the
|
|
29
|
+
agent instance and is persisted by `FileSessionManager` / `S3SessionManager` (or
|
|
30
|
+
your own) between the return and the resume. Without one, the parked question dies
|
|
31
|
+
with the process.
|
|
32
|
+
- **A duplicate answer is refused, not ignored.** Resuming a run that is not parked
|
|
33
|
+
raises `ValueError("... not in interrupt state")`; an unknown id raises `KeyError`;
|
|
34
|
+
a plain prompt to a parked run raises `TypeError`. None of them runs anything. The
|
|
35
|
+
host checks `result.stop_reason` (or its own record) before resuming, or catches.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from collections.abc import Mapping
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from strands.types.tools import ToolContext
|
|
45
|
+
except ImportError as _err: # pragma: no cover - exercised only without the extra
|
|
46
|
+
raise ImportError("agent_wait.strands needs Strands Agents: pip install 'agent-wait[strands]'") from _err
|
|
47
|
+
|
|
48
|
+
from ..framework import Framework
|
|
49
|
+
from ..wait import make_publish_interrupts, make_wait
|
|
50
|
+
|
|
51
|
+
__all__ = ["StrandsFramework", "publish_interrupts", "questions_in", "wait"]
|
|
52
|
+
|
|
53
|
+
INTERRUPT_NAME = "agent_wait"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _tool_context(call_args: Mapping[str, Any]) -> ToolContext[Any] | None:
|
|
57
|
+
for value in call_args.values():
|
|
58
|
+
if isinstance(value, ToolContext):
|
|
59
|
+
return value # pyright: ignore[reportUnknownVariableType]
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StrandsFramework(Framework):
|
|
64
|
+
name = "strands"
|
|
65
|
+
hidden_params = ("tool_context",)
|
|
66
|
+
|
|
67
|
+
def interrupt(self, value: Mapping[str, Any], call_args: Mapping[str, Any]) -> Any:
|
|
68
|
+
ctx = _tool_context(call_args)
|
|
69
|
+
if ctx is None:
|
|
70
|
+
raise TypeError(
|
|
71
|
+
"a @wait tool on Strands needs `@tool(context=True)` and a `tool_context` parameter"
|
|
72
|
+
)
|
|
73
|
+
return ctx.interrupt(INTERRUPT_NAME, reason=dict(value))
|
|
74
|
+
|
|
75
|
+
def interrupts_in(self, result: Any) -> list[tuple[str, Any]]:
|
|
76
|
+
if getattr(result, "stop_reason", None) != "interrupt":
|
|
77
|
+
return []
|
|
78
|
+
interrupts: Any = getattr(result, "interrupts", None) or ()
|
|
79
|
+
return [(str(item.id), item.reason) for item in interrupts]
|
|
80
|
+
|
|
81
|
+
def current_thread_id(self, call_args: Mapping[str, Any]) -> str:
|
|
82
|
+
ctx = _tool_context(call_args)
|
|
83
|
+
session_id: Any = getattr(getattr(ctx, "agent", None), "session_id", None)
|
|
84
|
+
if not session_id:
|
|
85
|
+
raise RuntimeError("@wait(mode='async') on Strands needs the agent's session_id")
|
|
86
|
+
return str(session_id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_framework = StrandsFramework()
|
|
90
|
+
|
|
91
|
+
wait = make_wait(_framework)
|
|
92
|
+
publish_interrupts = make_publish_interrupts(_framework)
|
|
93
|
+
questions_in = publish_interrupts.questions_in # type: ignore[attr-defined]
|
|
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
|
|
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
|