Skip to content

Commit 59e7eb4

Browse files
committed
Add methods to HookPart and make them canonical
1 parent faafcc0 commit 59e7eb4

12 files changed

Lines changed: 83 additions & 41 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ approval = await ai.hook(
150150
metadata={"tool": "send_email"},
151151
)
152152

153-
ai.resolve_hook("approve_send_email", {"granted": True, "reason": "approved"})
153+
# From a HookEvent handler, resolve it with the resolve() method:
154+
event.hook.resolve({"granted": True, "reason": "approved"})
154155
```
155156

156157
## Examples

docs/ai-python/content/docs/basics/durable-execution.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ interleaves model events and tool results while the model is still producing
102102
output.
103103

104104
Consider using the serverless hook flow for approvals. When a deferred `HookEvent`
105-
appears, call `ai.defer_hook(event.hook)`, persist `stream.messages`,
105+
appears, call `event.hook.defer()`, persist `stream.messages`,
106106
and return the approval request to the client. On the next workflow turn, call
107107
`ai.resolve_hook(...)` inside the `agent.run(...)` block before iterating the
108108
stream. The SDK replays the interrupted assistant turn and continues when the

docs/ai-python/content/docs/basics/human-in-the-loop.mdx

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ async def notify_mothership(message: str) -> str:
2121
```
2222

2323
When the model calls the tool, the agent emits a hook event. Resolve it with
24-
`ai.resolve_hook`:
24+
the `resolve()` method:
2525

2626
```python
2727
async with agent.run(model, messages) as stream:
@@ -31,25 +31,23 @@ async with agent.run(model, messages) as stream:
3131
and event.hook.status == "pending"
3232
):
3333
print(event.hook.hook_id, event.hook.metadata)
34-
ai.resolve_hook(
35-
event.hook,
34+
event.hook.resolve(
3635
ai.tools.ToolApproval(granted=True, reason="approved"),
3736
)
3837
```
3938

4039
Return a denial by resolving the hook with `granted=False`:
4140

4241
```python
43-
ai.resolve_hook(
44-
"approve_tool_call_id_here",
42+
event.hook.resolve(
4543
ai.tools.ToolApproval(granted=False, reason="not allowed"),
4644
)
4745
```
4846

4947
Cancel a live hook when the waiting workflow should stop:
5048

5149
```python
52-
await ai.cancel_hook("approve_tool_call_id_here", reason="client disconnected")
50+
await event.hook.cancel(reason="client disconnected")
5351
```
5452

5553
Hook operations target the current `HookRegistry`, which is set inside the
@@ -106,13 +104,10 @@ approval = await ai.hook(
106104
)
107105
```
108106

109-
Resolve the hook from another part of your application:
107+
Resolve the hook from your event handler:
110108

111109
```python
112-
ai.resolve_hook(
113-
"some_hook",
114-
{"foo": 999},
115-
)
110+
event.hook.resolve({"foo": 999})
116111
```
117112

118113
## Resume in serverless flows
@@ -127,34 +122,29 @@ Using the serverless flow only requires you to update the `agent.run` callsite
127122
```python
128123
# start (or replay pre-hook part of) a run
129124
async with agent.run(model, messages) as stream:
130-
# pre-register tool approvals before iterating the stream
131-
for approval in approvals:
132-
ai.resolve_hook(
133-
approval.hook_id,
134-
ai.tools.ToolApproval.model_validate(approval.data)
135-
)
125+
# apply approvals from the resumed request before iterating the stream
126+
ai.agents.ui.ai_sdk.apply_approvals(approvals)
136127

137128
async for event in stream:
138129
if (
139130
isinstance(event, ai.events.HookEvent)
140131
and event.hook.status == "pending"
141132
):
142133
# interrupt the loop
143-
ai.defer_hook(event.hook)
134+
event.hook.defer()
144135

145136
```
146137

147-
When handling the hook event, call `defer_hook` to interrupt the loop
138+
When handling the hook event, call `event.hook.defer()` to interrupt the loop
148139
and surface your tool approvals (or custom hook-related work) to the client.
149140

150141
The `ToolRunner` will handle multiple concurrent aborts by waiting for all
151142
scheduled tasks to complete or get aborted. That way you can run approval-gated
152143
tools on serverless concurrently.
153144

154145
Once the client has gathered payloads for all hooks, it will re-enter via the
155-
same endpoint, which will then pre-register hook resolutions inside the
156-
`agent.run` block, before iterating the stream. The loop will replay results
157-
of LLM calls and tool results
158-
from the first run without redoing non-deterministic and duplicating side-effects.
159-
When it hits the hook for which it has a pre-registered resolution, it will
160-
continue through without suspending.
146+
same endpoint, which will then apply the approvals inside the `agent.run`
147+
block, before iterating the stream. The loop will replay results of LLM calls
148+
and tool results from the first run without redoing non-deterministic and
149+
duplicating side-effects. When it hits the hook for which it has a
150+
pre-registered resolution, it will continue through without suspending.

docs/ai-python/content/docs/reference/ai/index.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,13 @@ approval = await ai.hook(
348348
Hook exports:
349349

350350
- `hook`: Emit a deferred hook event and wait for a matching resolution.
351-
- `resolve_hook`: Resolve a live or future hook.
351+
- `resolve_hook`: Resolve a live or future hook by ID or `HookPart`.
352352
- `defer_hook`: Mark a serialized deferred hook as aborted.
353-
- `cancel_hook`: Cancel a live hook by label.
353+
- `cancel_hook`: Cancel a live hook by ID or `HookPart`.
354+
355+
`HookPart` also has convenience methods for event-driven code:
356+
`hook_part.resolve(...)`, `await hook_part.cancel(...)`, and
357+
`hook_part.defer()`.
354358

355359
## Errors
356360

docs/ai-python/content/docs/reference/messages.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ Tool and runtime parts:
6161
- `BuiltinToolReturnPart`: A provider-executed tool result.
6262
- `HookPart`: A hook suspension, resolution, or cancellation.
6363

64+
`HookPart` has convenience methods for hook event handlers:
65+
66+
```python
67+
hook_part.resolve(data)
68+
await hook_part.cancel(reason="client disconnected")
69+
hook_part.defer()
70+
```
71+
6472
`ToolResultPart.get_model_input()` returns the value sent back to the model. For
6573
most tools this is the same as `result`. Aggregator-backed tools can store a
6674
rich `result` while sending a simpler model-facing value.

examples/agents/custom_hook.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,12 @@ async def main() -> None:
4949
and event.hook.status == "pending"
5050
):
5151
print(f"\n[deferred] {event.hook.hook_id}")
52-
ai.resolve_hook(
53-
event.hook,
52+
event.hook.resolve(
5453
DeploymentReview(
5554
approved=True,
5655
reviewer="ops",
5756
reason="change window is open",
58-
),
57+
)
5958
)
6059
elif (
6160
isinstance(event, ai.events.HookEvent)

examples/agents/tool_approval.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@ async def live(messages: list[ai.messages.Message]) -> None:
3131
and event.hook.status == "pending"
3232
):
3333
print(f"\n[deferred] {event.hook.hook_id}")
34-
ai.resolve_hook(
35-
event.hook,
34+
event.hook.resolve(
3635
ai.tools.ToolApproval(
3736
granted=True,
3837
reason="approved while the run is still active",
39-
),
38+
)
4039
)
4140
print()
4241

@@ -63,7 +62,7 @@ async def stateless(
6362
):
6463
deferred.append(event.hook)
6564
print(f"\n[deferred] {event.hook.hook_id}")
66-
ai.defer_hook(event.hook)
65+
event.hook.defer()
6766

6867
return stream.messages, deferred, "".join(text)
6968

examples/apps/coding_agent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Minimal coding-agent TUI built with the AI SDK for Python and
77
- streamed markdown replies (and reasoning, when the model emits it)
88
- every bash call is gated behind a `ToolApproval` hook: the run
99
suspends, a y/n prompt appears above the composer, and the decision
10-
is resolved in-process with `ai.resolve_hook()`
10+
is resolved in-process
1111
- messages typed while the agent is busy are queued and run in order;
1212
Esc interrupts the current turn
1313

examples/apps/coding_agent/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
streamed markdown, and an approval prompt. The user types a message,
55
the agent streams its reply, and every bash call suspends the run on a
66
``ToolApproval`` hook until the operator answers y/n — the decision is
7-
resolved in-process with ``ai.resolve_hook()``.
7+
resolved in-process.
88
99
Run it from this directory:
1010

examples/apps/web_agent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ tool is gated behind user confirmation before execution.
1717

1818
1. LLM emits a call to the gated tool
1919
2. The runtime emits a `HookEvent` with a deferred `HookPart`; the
20-
backend aborts the deferred hook (`ai.defer_hook`) so the turn
20+
backend defers the hook (`event.hook.defer()`) so the turn
2121
ends and the deferred approval streams to the client
2222
3. The frontend renders Approve / Reject buttons via the
2323
`<Confirmation>` component (from AI Elements)

0 commit comments

Comments
 (0)