agentapprovalprotocol 0.3.1__tar.gz → 0.3.3__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.
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/PKG-INFO +1 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/pyproject.toml +1 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/_gate.py +1 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/langgraph.py +33 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_adk.py +3 -3
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_claude_agent_sdk.py +1 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_crewai.py +5 -5
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_gate.py +1 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_langgraph.py +70 -2
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_openai_agents.py +4 -4
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_pydantic_ai.py +1 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/uv.lock +1 -1
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/.gitignore +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/README.md +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/__init__.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/_client.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/_wire.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/adk.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/claude_agent_sdk.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/crewai.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/openai_agents.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/py.typed +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/pydantic_ai.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/receiver.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/testing.py +0 -0
- {agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/tests/test_wire_and_receiver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentapprovalprotocol
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Put an AI agent's tool calls behind an Agent Approval Protocol (AAP) approval provider.
|
|
5
5
|
Project-URL: Homepage, https://agentapprovalprotocol.io
|
|
6
6
|
Project-URL: Source, https://github.com/agentapprovalprotocol/agentapprovalprotocol
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/_gate.py
RENAMED
|
@@ -117,7 +117,7 @@ def _settled(request: Request) -> Deny | None:
|
|
|
117
117
|
"""The deny for a terminal request, or None when it may be consumed."""
|
|
118
118
|
if request.status == DENIED:
|
|
119
119
|
note = request.decision.note if request.decision else None
|
|
120
|
-
return Deny(f"
|
|
120
|
+
return Deny(f"A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: {note}" if note else "A reviewer at the AAP (Agent Approval Protocol) provider denied this call.", DENIED, request.id)
|
|
121
121
|
if request.status == CANCELLED:
|
|
122
122
|
return Deny(CANCELLED_TEXT, CANCELLED, request.id)
|
|
123
123
|
if request.status == EXPIRED:
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/langgraph.py
RENAMED
|
@@ -13,11 +13,13 @@ Install with ``pip install "agentapprovalprotocol[langgraph]"``.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
|
+
import contextvars
|
|
16
17
|
from typing import Any, Awaitable, Callable, Iterable, Literal, Mapping, Sequence
|
|
17
18
|
|
|
18
19
|
from langchain.agents.middleware import AgentMiddleware
|
|
19
20
|
from langchain_core.messages import AIMessage, ToolMessage
|
|
20
21
|
from langchain_core.runnables.config import var_child_runnable_config
|
|
22
|
+
from langgraph.errors import GraphBubbleUp
|
|
21
23
|
from langgraph.prebuilt.tool_node import ToolCallRequest
|
|
22
24
|
from langgraph.types import Command, interrupt
|
|
23
25
|
|
|
@@ -27,6 +29,15 @@ from ._wire import Request
|
|
|
27
29
|
|
|
28
30
|
INTERRUPT_KEY = "aap_request_id"
|
|
29
31
|
|
|
32
|
+
# Set while an approved tool runs. A call made inside it, such as one by an
|
|
33
|
+
# agent the tool invokes, waits instead of pausing: on resume LangGraph would
|
|
34
|
+
# run the outer tool again, and its approval is already consumed.
|
|
35
|
+
_running = contextvars.ContextVar("aap_running_tool", default=False)
|
|
36
|
+
|
|
37
|
+
# Deep Agents' delegation tool. It changes nothing itself, so once every
|
|
38
|
+
# subagent is gated it runs without approval, and its subagents can pause.
|
|
39
|
+
DEEP_AGENTS_TASK_TOOL = "task"
|
|
40
|
+
|
|
30
41
|
Handler = Callable[[ToolCallRequest], "ToolMessage | Command"]
|
|
31
42
|
AsyncHandler = Callable[[ToolCallRequest], Awaitable["ToolMessage | Command"]]
|
|
32
43
|
|
|
@@ -103,6 +114,7 @@ class ApprovalMiddleware(AgentMiddleware):
|
|
|
103
114
|
self._async_gate = async_gate
|
|
104
115
|
self._make = lambda client_cls, gate_cls: gate_cls(client_cls(base_url, token), mode=mode, **gate_options)
|
|
105
116
|
self.mode = mode
|
|
117
|
+
self._ungated: set[str] = set()
|
|
106
118
|
|
|
107
119
|
@property
|
|
108
120
|
def gate(self) -> Gate:
|
|
@@ -132,9 +144,11 @@ class ApprovalMiddleware(AgentMiddleware):
|
|
|
132
144
|
context=self.context,
|
|
133
145
|
resume=resume,
|
|
134
146
|
)
|
|
135
|
-
return call, thread is not None
|
|
147
|
+
return call, thread is not None and not _running.get()
|
|
136
148
|
|
|
137
149
|
def wrap_tool_call(self, request: ToolCallRequest, handler: Handler) -> ToolMessage | Command:
|
|
150
|
+
if request.tool_call["name"] in self._ungated:
|
|
151
|
+
return handler(request)
|
|
138
152
|
call, can_pause = self._call(request)
|
|
139
153
|
verdict: Verdict = self.gate.check(call, can_pause=can_pause)
|
|
140
154
|
while isinstance(verdict, Pending):
|
|
@@ -142,17 +156,24 @@ class ApprovalMiddleware(AgentMiddleware):
|
|
|
142
156
|
verdict = self.gate.check(call, can_pause=can_pause)
|
|
143
157
|
if isinstance(verdict, Deny):
|
|
144
158
|
return _denied(request, verdict)
|
|
159
|
+
running = _running.set(True)
|
|
145
160
|
try:
|
|
146
161
|
result = handler(request)
|
|
162
|
+
except GraphBubbleUp:
|
|
163
|
+
raise # an interrupt or a command for the parent graph, not a failure
|
|
147
164
|
except BaseException:
|
|
148
165
|
if self.gate.report_outcomes:
|
|
149
166
|
verdict.report("failed")
|
|
150
167
|
raise
|
|
168
|
+
finally:
|
|
169
|
+
_running.reset(running)
|
|
151
170
|
if self.gate.report_outcomes:
|
|
152
171
|
verdict.report("failed" if _failed(result) else "succeeded")
|
|
153
172
|
return result
|
|
154
173
|
|
|
155
174
|
async def awrap_tool_call(self, request: ToolCallRequest, handler: AsyncHandler) -> ToolMessage | Command:
|
|
175
|
+
if request.tool_call["name"] in self._ungated:
|
|
176
|
+
return await handler(request)
|
|
156
177
|
call, can_pause = self._call(request)
|
|
157
178
|
verdict: Verdict = await self.async_gate.check(call, can_pause=can_pause)
|
|
158
179
|
while isinstance(verdict, Pending):
|
|
@@ -161,12 +182,17 @@ class ApprovalMiddleware(AgentMiddleware):
|
|
|
161
182
|
if isinstance(verdict, Deny):
|
|
162
183
|
return _denied(request, verdict)
|
|
163
184
|
assert isinstance(verdict, Allow)
|
|
185
|
+
running = _running.set(True)
|
|
164
186
|
try:
|
|
165
187
|
result = await handler(request)
|
|
188
|
+
except GraphBubbleUp:
|
|
189
|
+
raise # an interrupt or a command for the parent graph, not a failure
|
|
166
190
|
except BaseException:
|
|
167
191
|
if self.async_gate.report_outcomes:
|
|
168
192
|
await verdict.report("failed")
|
|
169
193
|
raise
|
|
194
|
+
finally:
|
|
195
|
+
_running.reset(running)
|
|
170
196
|
if self.async_gate.report_outcomes:
|
|
171
197
|
await verdict.report("failed" if _failed(result) else "succeeded")
|
|
172
198
|
return result
|
|
@@ -242,6 +268,11 @@ def deep_agent_subagents(middleware: ApprovalMiddleware, subagents: Sequence[Map
|
|
|
242
268
|
the default general-purpose subagent with the same spec plus approvals.
|
|
243
269
|
Compiled and async subagents run their own graphs and are refused: add
|
|
244
270
|
the middleware to those graphs yourself.
|
|
271
|
+
|
|
272
|
+
With every subagent gated, the middleware stops gating the ``task`` tool
|
|
273
|
+
that delegates to them. ``task`` changes nothing itself, and gating it
|
|
274
|
+
would stop its subagents' calls from pausing: on resume LangGraph runs
|
|
275
|
+
``task`` again, and its approval would already be consumed.
|
|
245
276
|
"""
|
|
246
277
|
from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT
|
|
247
278
|
|
|
@@ -252,4 +283,5 @@ def deep_agent_subagents(middleware: ApprovalMiddleware, subagents: Sequence[Map
|
|
|
252
283
|
out.append({**spec, "middleware": [*spec.get("middleware", []), middleware]})
|
|
253
284
|
if not any(spec.get("name") == GENERAL_PURPOSE_SUBAGENT["name"] for spec in out):
|
|
254
285
|
out.append({**GENERAL_PURPOSE_SUBAGENT, "middleware": [middleware]})
|
|
286
|
+
middleware._ungated.add(DEEP_AGENTS_TASK_TOOL)
|
|
255
287
|
return out
|
|
@@ -119,7 +119,7 @@ async def test_denied_call_returns_the_reason_to_the_model():
|
|
|
119
119
|
runner, _ = build(provider, LlmAgent(name="support", model=Scripted(turn(REFUND), say("Understood.")), tools=[refund]))
|
|
120
120
|
_, events = await start(runner)
|
|
121
121
|
assert ran == []
|
|
122
|
-
assert responses(events, "refund") == [{"error": "
|
|
122
|
+
assert responses(events, "refund") == [{"error": "A reviewer at the AAP (Agent Approval Protocol) provider denied this call."}]
|
|
123
123
|
assert last_text(events) == "Understood."
|
|
124
124
|
|
|
125
125
|
|
|
@@ -204,7 +204,7 @@ async def test_async_denial_wakes_and_returns_the_reason():
|
|
|
204
204
|
await resumer(runner)(provider.only().body["context"]["resume"], await provider.async_client().get(request_id))
|
|
205
205
|
events = await session_events(runner, session_id)
|
|
206
206
|
assert ran == []
|
|
207
|
-
assert responses(events, "refund")[-1] == {"error": "
|
|
207
|
+
assert responses(events, "refund")[-1] == {"error": "A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: Not this one."}
|
|
208
208
|
assert last_text(events) == "Understood."
|
|
209
209
|
|
|
210
210
|
|
|
@@ -312,7 +312,7 @@ async def test_invented_tool_names_are_gated():
|
|
|
312
312
|
runner, _ = build(provider, LlmAgent(name="support", model=Scripted(turn(("c1", "wire_money", {"to": "x"})), say("Ok.")), tools=[refund]))
|
|
313
313
|
_, events = await start(runner)
|
|
314
314
|
assert provider.only().body["tool"] == "wire_money"
|
|
315
|
-
assert responses(events, "wire_money") == [{"error": "
|
|
315
|
+
assert responses(events, "wire_money") == [{"error": "A reviewer at the AAP (Agent Approval Protocol) provider denied this call."}]
|
|
316
316
|
|
|
317
317
|
|
|
318
318
|
async def test_gemini_built_in_tools_are_refused():
|
|
@@ -198,7 +198,7 @@ async def test_denied_call_returns_the_reason_to_the_model(cli):
|
|
|
198
198
|
(result,) = await run(hooks(provider).options(base))
|
|
199
199
|
assert ran == [] and result.result == "Understood."
|
|
200
200
|
(denied,) = api.tool_results()
|
|
201
|
-
assert denied["is_error"] and "
|
|
201
|
+
assert denied["is_error"] and "A reviewer at the AAP (Agent Approval Protocol) provider denied this call." in json.dumps(denied["content"])
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
@needs_cli
|
|
@@ -157,7 +157,7 @@ def test_denied_call_returns_the_reason_to_the_model():
|
|
|
157
157
|
hooks(provider)
|
|
158
158
|
crew(llm)[0].kickoff()
|
|
159
159
|
assert ran == []
|
|
160
|
-
assert llm.tool_results() == ["
|
|
160
|
+
assert llm.tool_results() == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: Not this one."]
|
|
161
161
|
|
|
162
162
|
|
|
163
163
|
def test_text_tool_calls_are_gated():
|
|
@@ -166,7 +166,7 @@ def test_text_tool_calls_are_gated():
|
|
|
166
166
|
hooks(provider)
|
|
167
167
|
out = crew(llm)[0].kickoff()
|
|
168
168
|
assert ran == [] and out.raw == "Not refunded."
|
|
169
|
-
assert "
|
|
169
|
+
assert "A reviewer at the AAP (Agent Approval Protocol) provider denied this call." in llm.tool_results()[-1]
|
|
170
170
|
|
|
171
171
|
provider.auto = "approved"
|
|
172
172
|
llm = Scripted(model="scripted", native=False, turns=[react("refund", REFUND[2]), "Thought: done\nFinal Answer: Refunded."])
|
|
@@ -204,7 +204,7 @@ def test_parallel_calls_get_their_own_decisions():
|
|
|
204
204
|
hooks(provider)
|
|
205
205
|
crew(llm)[0].kickoff()
|
|
206
206
|
assert ran == [{"payment_id": "payment_456", "amount": 100}]
|
|
207
|
-
assert sorted(llm.tool_results()) == ["
|
|
207
|
+
assert sorted(llm.tool_results()) == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: Too large.", "refunded payment_456"]
|
|
208
208
|
assert len(provider.requests) == 2
|
|
209
209
|
|
|
210
210
|
|
|
@@ -223,7 +223,7 @@ def test_delegated_coworker_calls_are_gated():
|
|
|
223
223
|
Crew(agents=[lead, billing], tasks=[task]).kickoff()
|
|
224
224
|
assert ran == []
|
|
225
225
|
assert sorted(s.body["tool"] for s in provider.requests.values()) == ["delegate_work_to_coworker", "refund"]
|
|
226
|
-
assert billing_llm.tool_results() == ["
|
|
226
|
+
assert billing_llm.tool_results() == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: No refunds today."]
|
|
227
227
|
assert "The refund was denied." in lead_llm.tool_results()[0]
|
|
228
228
|
delegated = next(s for s in provider.requests.values() if s.body["tool"] == "delegate_work_to_coworker")
|
|
229
229
|
assert delegated.outcome["status"] == "succeeded"
|
|
@@ -235,7 +235,7 @@ def test_agent_kickoff_is_gated():
|
|
|
235
235
|
hooks(provider)
|
|
236
236
|
Agent(role="Support", goal="Refunds", backstory="b", llm=llm, tools=[refund]).kickoff("Refund payment_123")
|
|
237
237
|
assert ran == [] and len(provider.requests) == 1
|
|
238
|
-
assert llm.tool_results() == ["
|
|
238
|
+
assert llm.tool_results() == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call."]
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
def test_mcp_tools_send_the_server_and_original_name():
|
|
@@ -42,7 +42,7 @@ def test_denial_carries_the_reviewers_note():
|
|
|
42
42
|
provider.decide(pending.request_id, "denied", "Refunds over 40 GBP need a manager.")
|
|
43
43
|
verdict = gate.check(call())
|
|
44
44
|
assert isinstance(verdict, Deny) and verdict.status == "denied"
|
|
45
|
-
assert verdict.reason == "
|
|
45
|
+
assert verdict.reason == "A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: Refunds over 40 GBP need a manager."
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_same_call_cannot_run_twice_across_replicas():
|
|
@@ -67,7 +67,7 @@ def test_denied_call_returns_the_reason_to_the_model():
|
|
|
67
67
|
result = agent(provider, turn(REFUND), AIMessage("Understood.")).invoke({"messages": [HumanMessage("Refund it")]})
|
|
68
68
|
assert ran == []
|
|
69
69
|
message = next(m for m in result["messages"] if isinstance(m, ToolMessage))
|
|
70
|
-
assert message.status == "error" and message.content == "
|
|
70
|
+
assert message.status == "error" and message.content == "A reviewer at the AAP (Agent Approval Protocol) provider denied this call."
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def test_async_mode_pauses_ignores_forged_resumes_and_wakes_on_the_decision():
|
|
@@ -150,7 +150,75 @@ def test_deep_agent_subagent_calls_are_gated():
|
|
|
150
150
|
graph = create_deep_agent(model=model, tools=[refund], middleware=[middleware], subagents=deep_agent_subagents(middleware))
|
|
151
151
|
graph.invoke({"messages": [HumanMessage("Refund payment_123")]})
|
|
152
152
|
assert ran == [{"payment_id": "payment_123", "amount": 4900}]
|
|
153
|
-
|
|
153
|
+
# task only delegates; the subagent's own call is the one sent for approval.
|
|
154
|
+
assert [s.body["tool"] for s in provider.requests.values()] == ["refund"]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.skipif(sys.version_info < (3, 11), reason="Deep Agents needs Python 3.11")
|
|
158
|
+
def test_deep_agent_subagent_calls_pause_in_async_mode():
|
|
159
|
+
from deepagents import create_deep_agent
|
|
160
|
+
|
|
161
|
+
provider = FakeProvider(auto=None)
|
|
162
|
+
middleware = ApprovalMiddleware(gate=Gate(provider.client(), mode="async"), async_gate=AsyncGate(provider.async_client(), mode="async"))
|
|
163
|
+
model = Scripted(
|
|
164
|
+
messages=iter(
|
|
165
|
+
[
|
|
166
|
+
turn(("t1", "task", {"description": "Refund payment_123", "subagent_type": "general-purpose"})),
|
|
167
|
+
turn(REFUND),
|
|
168
|
+
AIMessage("Refunded."),
|
|
169
|
+
AIMessage("The subagent refunded it."),
|
|
170
|
+
]
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
graph = create_deep_agent(model=model, tools=[refund], middleware=[middleware], subagents=deep_agent_subagents(middleware), checkpointer=InMemorySaver())
|
|
174
|
+
config = {"configurable": {"thread_id": "t3"}}
|
|
175
|
+
paused = graph.invoke({"messages": [HumanMessage("Refund payment_123")]}, config)
|
|
176
|
+
(pending,) = paused["__interrupt__"]
|
|
177
|
+
assert pending.value["tool"] == "refund" and ran == []
|
|
178
|
+
|
|
179
|
+
provider.decide(pending.value["aap_request_id"])
|
|
180
|
+
receiver = Receiver(secret=SECRET, client=provider.async_client(), resumers={"langgraph": resumer(graph)})
|
|
181
|
+
assert asyncio.run(receiver.wake(pending.value["aap_request_id"]))
|
|
182
|
+
# task runs again on resume, and the subagent continues from its own checkpoint.
|
|
183
|
+
assert ran == [{"payment_id": "payment_123", "amount": 4900}]
|
|
184
|
+
assert [(s.body["tool"], s.outcome["status"]) for s in provider.requests.values()] == [("refund", "succeeded")]
|
|
185
|
+
state = graph.get_state(config)
|
|
186
|
+
assert not state.interrupts and state.values["messages"][-1].content == "The subagent refunded it."
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_calls_inside_a_running_tool_wait_in_async_mode():
|
|
190
|
+
provider = FakeProvider(auto=None)
|
|
191
|
+
middleware = ApprovalMiddleware(gate=Gate(provider.client(), mode="async"), async_gate=AsyncGate(provider.async_client(), mode="async"))
|
|
192
|
+
inner = create_agent(Scripted(messages=iter([turn(REFUND), AIMessage("Refunded.")])), tools=[refund], middleware=[middleware])
|
|
193
|
+
|
|
194
|
+
def billing(request: str) -> str:
|
|
195
|
+
"""Ask the billing agent to do something."""
|
|
196
|
+
return inner.invoke({"messages": [HumanMessage(request)]})["messages"][-1].content
|
|
197
|
+
|
|
198
|
+
def approve_the_waiting_refund(request):
|
|
199
|
+
if request.method == "GET":
|
|
200
|
+
for stored in provider.requests.values():
|
|
201
|
+
if stored.body["tool"] == "refund":
|
|
202
|
+
provider._decide(stored, "approved", None)
|
|
203
|
+
|
|
204
|
+
provider.intercept = approve_the_waiting_refund
|
|
205
|
+
outer = create_agent(
|
|
206
|
+
Scripted(messages=iter([turn(("b1", "billing", {"request": "Refund payment_123"})), AIMessage("Billing refunded it.")])),
|
|
207
|
+
tools=[billing],
|
|
208
|
+
middleware=[middleware],
|
|
209
|
+
checkpointer=InMemorySaver(),
|
|
210
|
+
)
|
|
211
|
+
config = {"configurable": {"thread_id": "t4"}}
|
|
212
|
+
(pending,) = outer.invoke({"messages": [HumanMessage("Refund payment_123")]}, config)["__interrupt__"]
|
|
213
|
+
assert pending.value["tool"] == "billing"
|
|
214
|
+
|
|
215
|
+
provider.decide(pending.value["aap_request_id"])
|
|
216
|
+
receiver = Receiver(secret=SECRET, client=provider.async_client(), resumers={"langgraph": resumer(outer)})
|
|
217
|
+
assert asyncio.run(receiver.wake(pending.value["aap_request_id"]))
|
|
218
|
+
# The refund waited inside the approved billing call: pausing would run billing again, refused as consumed.
|
|
219
|
+
assert ran == [{"payment_id": "payment_123", "amount": 4900}]
|
|
220
|
+
assert sorted((s.body["tool"], s.outcome["status"]) for s in provider.requests.values()) == [("billing", "succeeded"), ("refund", "succeeded")]
|
|
221
|
+
assert outer.get_state(config).values["messages"][-1].content == "Billing refunded it."
|
|
154
222
|
|
|
155
223
|
|
|
156
224
|
@pytest.mark.skipif(sys.version_info < (3, 11), reason="Deep Agents needs Python 3.11")
|
|
@@ -90,7 +90,7 @@ async def test_denied_call_returns_the_reason_to_the_model():
|
|
|
90
90
|
agent = approvals(provider).protect(Agent(name="support", model=model, tools=[refund]))
|
|
91
91
|
await Runner.run(agent, "Refund it")
|
|
92
92
|
assert ran == []
|
|
93
|
-
assert outputs(model) == ["
|
|
93
|
+
assert outputs(model) == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call."]
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
async def test_a_replayed_call_does_not_run_again():
|
|
@@ -181,7 +181,7 @@ async def test_async_mode_denial_returns_the_reason():
|
|
|
181
181
|
receiver = Receiver(secret=SECRET, client=provider.async_client(), resumers={"openai-agents": gate.resumer(agent)})
|
|
182
182
|
assert await receiver.wake(stored.id)
|
|
183
183
|
assert ran == [] and model.remaining_steps == 0
|
|
184
|
-
assert outputs(model) == ["
|
|
184
|
+
assert outputs(model) == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: Not today."]
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
async def test_parallel_calls_wait_for_their_own_decisions():
|
|
@@ -204,7 +204,7 @@ async def test_parallel_calls_wait_for_their_own_decisions():
|
|
|
204
204
|
await receiver.wake(ids["payment_123"])
|
|
205
205
|
assert ran == [second]
|
|
206
206
|
assert model.remaining_steps == 0
|
|
207
|
-
assert sorted(outputs(model)) == ["
|
|
207
|
+
assert sorted(outputs(model)) == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call. Their note: Not this one.", "refunded payment_456"]
|
|
208
208
|
assert len(provider.requests) == 2
|
|
209
209
|
|
|
210
210
|
|
|
@@ -341,7 +341,7 @@ async def test_local_mcp_denial_does_not_call_the_server():
|
|
|
341
341
|
agent = approvals(provider).protect(Agent(name="support", model=model, mcp_servers=[crm]))
|
|
342
342
|
await Runner.run(agent, "Update the contact")
|
|
343
343
|
assert crm.calls == []
|
|
344
|
-
assert outputs(model) == ["
|
|
344
|
+
assert outputs(model) == ["A reviewer at the AAP (Agent Approval Protocol) provider denied this call."]
|
|
345
345
|
|
|
346
346
|
|
|
347
347
|
@pytest.mark.parametrize("status", ["approved", "denied"])
|
|
@@ -106,7 +106,7 @@ async def test_denied_call_returns_the_reason_to_the_model():
|
|
|
106
106
|
result = await agent(provider, turn(REFUND), done("Understood.")).run("Refund it")
|
|
107
107
|
assert ran == []
|
|
108
108
|
(part,) = returns(result.all_messages())
|
|
109
|
-
assert part.outcome == "failed" and part.content == "
|
|
109
|
+
assert part.outcome == "failed" and part.content == "A reviewer at the AAP (Agent Approval Protocol) provider denied this call."
|
|
110
110
|
assert result.output == "Understood."
|
|
111
111
|
|
|
112
112
|
|
|
File without changes
|
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/__init__.py
RENAMED
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/_client.py
RENAMED
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/_wire.py
RENAMED
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/adk.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/crewai.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/py.typed
RENAMED
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/pydantic_ai.py
RENAMED
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/receiver.py
RENAMED
|
File without changes
|
{agentapprovalprotocol-0.3.1 → agentapprovalprotocol-0.3.3}/src/agentapprovalprotocol/testing.py
RENAMED
|
File without changes
|
|
File without changes
|