Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/ai-python/content/docs/basics/human-in-the-loop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async with agent.run(model, messages) as stream:
async for event in stream:
if (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
print(event.hook.hook_id, event.hook.metadata)

Expand Down Expand Up @@ -101,7 +101,7 @@ async with agent.run(model, messages) as stream:
async for event in stream:
if (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
# interrupt the loop
ai.defer_hook(event.hook)
Expand Down
2 changes: 1 addition & 1 deletion examples/agents/custom_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async def main() -> None:
print(event.chunk, end="", flush=True)
elif (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
print(f"\n[deferred] {event.hook.hook_id}")
ai.resolve_hook(
Expand Down
4 changes: 2 additions & 2 deletions examples/agents/tool_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async def live(messages: list[ai.messages.Message]) -> None:
print(event.chunk, end="", flush=True)
elif (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
print(f"\n[deferred] {event.hook.hook_id}")
ai.resolve_hook(
Expand Down Expand Up @@ -58,7 +58,7 @@ async def stateless(
print(event.chunk, end="", flush=True)
elif (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
deferred.append(event.hook)
print(f"\n[deferred] {event.hook.hook_id}")
Expand Down
2 changes: 1 addition & 1 deletion examples/apps/coding_agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ async def _run_turn(self) -> None:

def _on_hook(self, hook: ai.messages.HookPart[Any]) -> None:
"""Mount or dismiss an approval prompt for a hook signal."""
if hook.status == "deferred":
if hook.status == "pending":
prompt = ApprovalPrompt(hook)
self.query_one("#dock").mount(prompt, before=self.composer)
if not isinstance(self.focused, ApprovalPrompt):
Expand Down
2 changes: 1 addition & 1 deletion examples/apps/web_agent/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async def process() -> AsyncGenerator[ai.events.AgentEvent]:
async for event in result:
if (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
ai.defer_hook(event.hook)
yield event
Expand Down
2 changes: 1 addition & 1 deletion skills/ai-python-serverless-execution/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async with agent.run(model, messages) as stream:
async for event in stream:
if (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
deferred_hooks.append(event.hook)
ai.defer_hook(event.hook)
Expand Down
2 changes: 1 addition & 1 deletion skills/ai-python-ui-adapter/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def body():
async for event in stream:
if (
isinstance(event, ai.events.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
ai.defer_hook(event.hook)
yield event
Expand Down
4 changes: 2 additions & 2 deletions src/ai/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,7 @@ def _track(self, event: events_.AgentEvent) -> events_.AgentEvent:
if isinstance(event, events_.RunBlocked):
self._blocked = True
elif isinstance(event, events_.HookEvent):
if event.hook.status == "deferred":
if event.hook.status == "pending":
self._deferred_hooks[event.hook.hook_id] = event.hook
else:
# A blocked run can only resume via a hook resolution
Expand Down Expand Up @@ -1148,7 +1148,7 @@ def deferred_tool_result(
)

The hook itself is surfaced separately via the ``HookPart`` already
emitted by ``ai.hook()`` (status=``"deferred"``) which downstream
emitted by ``ai.hook()`` (status=``"pending"``) which downstream
consumers (e.g. the ai-sdk UI bridge) use to render the actual
approval state.
"""
Expand Down
4 changes: 2 additions & 2 deletions src/ai/agents/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ async def _hook_impl(call: middleware_.HookContext) -> pydantic.BaseModel:
_live_hooks[label] = (future, hook_metadata, rt)
rt.track_hook_label(label)

# Emit deferred signal.
# Emit pending signal.
hook_part: messages_.HookPart[Any] = messages_.HookPart(
hook_id=label,
hook_type=payload.__name__,
status="deferred",
status="pending",
metadata=hook_metadata,
tool_call_id=call.tool_call_id,
)
Expand Down
2 changes: 1 addition & 1 deletion src/ai/agents/ui/ai_sdk/approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def hook_part_from_tool_part(tp: ToolPart) -> messages_.HookPart[Any] | None:
return messages_.HookPart(
hook_id=approval.id,
hook_type=TOOL_APPROVAL_HOOK_TYPE,
status="deferred",
status="pending",
metadata=metadata,
tool_call_id=tp.tool_call_id,
)
Expand Down
2 changes: 1 addition & 1 deletion src/ai/agents/ui/ai_sdk/outbound_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def merge_approval_signals(
is_automatic = part.metadata.get("isAutomatic")
is_automatic = is_automatic if isinstance(is_automatic, bool) else None
match part.status:
case "deferred":
case "pending":
updates["state"] = "approval-requested"
updates["approval"] = ui_messages.UIToolApproval.model_validate(
{
Expand Down
4 changes: 2 additions & 2 deletions src/ai/agents/ui/ai_sdk/outbound_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ def on_tool_result(
if part.tool_call_id in self.emitted_tool_results:
continue
# Hook deferral placeholders are internal bookkeeping: the
# corresponding HookPart(deferred) drives the UI state.
# corresponding pending HookPart drives the UI state.
if part.is_hook_deferred:
continue
self.emitted_tool_results.add(part.tool_call_id)
Expand Down Expand Up @@ -490,7 +490,7 @@ def on_hook(
is_automatic = hook_part.metadata.get("isAutomatic")
is_automatic = is_automatic if isinstance(is_automatic, bool) else None
match hook_part.status:
case "deferred":
case "pending":
if tc_id in self.emitted_approval_requests:
return out
self.emitted_approval_requests.add(tc_id)
Expand Down
2 changes: 1 addition & 1 deletion src/ai/telemetry/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class HookSpanData:
label: str
hook_type: str
metadata: dict[str, Any]
status: Literal["deferred", "resolved", "cancelled"] = "deferred"
status: Literal["pending", "resolved", "cancelled"] = "pending"

span_name: ClassVar[str] = "hook"

Expand Down
6 changes: 3 additions & 3 deletions src/ai/types/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ class RunBlocked(pydantic.BaseModel):

There is no mirror "unblocked" event because it would be redundant:
a blocked run can only resume via a hook resolution (or
cancellation), so the next ``HookEvent`` with a non-``deferred``
cancellation), so the next ``HookEvent`` with a non-``pending``
status *is* the unblock signal. Note the converse does not hold —
a ``ToolCallResult`` carrying an ``is_hook_deferred`` placeholder
(serverless abort) arrives while the run stays blocked, and the run
Expand Down Expand Up @@ -368,7 +368,7 @@ class RunStateTracker:

The fold reads three things:

* hook state from :class:`HookEvent` (``deferred`` adds, ``resolved``
* hook state from :class:`HookEvent` (``pending`` adds, ``resolved``
/ ``cancelled`` removes);
* model-stream activity from :class:`StreamStart` / :class:`StreamEnd`;
* in-flight tool calls from the assistant message on
Expand Down Expand Up @@ -416,7 +416,7 @@ def feed(self, event: AgentEvent) -> RunBlocked | None:
r.tool_call_id for r in event.results
)
case HookEvent():
if event.hook.status == "deferred":
if event.hook.status == "pending":
self._deferred[event.hook.hook_id] = event.hook
else:
self._deferred.pop(event.hook.hook_id, None)
Expand Down
2 changes: 1 addition & 1 deletion src/ai/types/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ class HookPart[T](pydantic.BaseModel):
id: str = pydantic.Field(default_factory=lambda: generate_id("part"))
hook_id: str
hook_type: str
status: Literal["deferred", "resolved", "cancelled"]
status: Literal["pending", "resolved", "cancelled"]
metadata: dict[str, Any] = pydantic.Field(default_factory=dict)
resolution: T | None = None
tool_call_id: str | None = None
Expand Down
12 changes: 6 additions & 6 deletions tests/agents/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def loop(
if not isinstance(event, agent_events_.HookEvent):
continue
# When we see the deferred hook, resolve it.
if event.hook.status == "deferred":
if event.hook.status == "pending":
ai.resolve_hook(
"confirm_1", {"approved": True, "reason": "looks good"}
)
Expand Down Expand Up @@ -85,7 +85,7 @@ async def loop(
async for event in stream:
if not isinstance(event, agent_events_.HookEvent):
continue
if event.hook.status == "deferred":
if event.hook.status == "pending":
await ai.cancel_hook("cancel_me", reason="denied")

assert was_cancelled
Expand Down Expand Up @@ -169,7 +169,7 @@ async def loop(
if not isinstance(event, agent_events_.HookEvent):
continue
hooks.append(event.hook)
if event.hook.status == "deferred":
if event.hook.status == "pending":
ai.resolve_hook("emit_test", {"approved": False})

resolved = [h for h in hooks if h.status == "resolved"]
Expand Down Expand Up @@ -205,7 +205,7 @@ async def loop(
async for event in stream:
if isinstance(event, agent_events_.HookEvent):
hooks.append(event.hook)
if event.hook.status == "deferred":
if event.hook.status == "pending":
ai.defer_hook(event.hook)

assert len(hooks) >= 1
Expand Down Expand Up @@ -240,7 +240,7 @@ async def test_live_hook_span(recorder: Recorder) -> None:
async for event in stream:
if (
isinstance(event, agent_events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
ai.resolve_hook(my_agent.label, {"approved": True})

Expand Down Expand Up @@ -277,7 +277,7 @@ async def loop(
async for event in stream:
if (
isinstance(event, agent_events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
await ai.cancel_hook(my_agent.label, reason="denied")

Expand Down
12 changes: 6 additions & 6 deletions tests/agents/test_run_blocked.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
awaiting a hook. ``RunStateTracker`` folds the event stream and a
``RunBlocked`` event is emitted when the run blocks; there is no
mirror event — a blocked run can only resume via a hook resolution,
so the non-``deferred`` ``HookEvent`` is the unblock signal.
so the non-``pending`` ``HookEvent`` is the unblock signal.
``AgentStream`` folds both into its ``blocked`` / ``deferred_hooks``
properties.
"""
Expand Down Expand Up @@ -96,7 +96,7 @@ def index(pred: Any) -> int:

deferred_idx = index(
lambda e: isinstance(e, events_.HookEvent)
and e.hook.status == "deferred"
and e.hook.status == "pending"
)
resolved_idx = index(
lambda e: isinstance(e, events_.HookEvent)
Expand Down Expand Up @@ -132,7 +132,7 @@ async def slow(x: int) -> str:
delivered.append(event)
if (
isinstance(event, events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
assert not stream.blocked
release.set()
Expand Down Expand Up @@ -213,7 +213,7 @@ async def test_abort_leaves_blocked() -> None:
blocked_events.append(event)
if (
isinstance(event, events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
ai.defer_hook(event.hook)

Expand Down Expand Up @@ -304,7 +304,7 @@ async def self_gated(x: int) -> str:
assert not isinstance(event, events_.RunBlocked)
if (
isinstance(event, events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
assert event.hook.tool_call_id is None
ai.resolve_hook("self_gate", {"approved": True})
Expand All @@ -325,7 +325,7 @@ def test_tracker_fold_sequence() -> None:
hook: messages_.HookPart[Any] = messages_.HookPart(
hook_id="h1",
hook_type="ToolApproval",
status="deferred",
status="pending",
tool_call_id="tc-1",
)

Expand Down
6 changes: 3 additions & 3 deletions tests/agents/test_validation_error_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def test_invalid_args_with_approval_returns_error_result() -> None:
async for event in stream:
if (
isinstance(event, events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
hook_events.append(event)
ai.resolve_hook(
Expand Down Expand Up @@ -113,7 +113,7 @@ async def test_invalid_args_skips_approval_hook() -> None:
async for event in stream:
if (
isinstance(event, events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
hook_fired = True
ai.resolve_hook(
Expand Down Expand Up @@ -154,7 +154,7 @@ async def test_completely_invalid_json_with_approval() -> None:
async for event in stream:
if (
isinstance(event, events_.HookEvent)
and event.hook.status == "deferred"
and event.hook.status == "pending"
):
ai.resolve_hook(
event.hook.hook_id,
Expand Down
6 changes: 3 additions & 3 deletions tests/agents/ui/ai_sdk/test_approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_tool_call_id_for_returns_field() -> None:
hook: messages_.HookPart[Any] = messages_.HookPart(
hook_id="my_custom_gate",
hook_type="ToolApproval",
status="deferred",
status="pending",
tool_call_id="tc_42",
)
assert approvals.tool_call_id_for(hook) == "tc_42"
Expand All @@ -42,7 +42,7 @@ def test_tool_call_id_for_field_ignored_on_non_approval() -> None:
hook: messages_.HookPart[Any] = messages_.HookPart(
hook_id="confirm_something",
hook_type="Confirmation",
status="deferred",
status="pending",
tool_call_id="tc_42",
)
assert approvals.tool_call_id_for(hook) is None
Expand All @@ -52,7 +52,7 @@ def test_tool_call_id_for_rejects_non_approval_type() -> None:
hook: messages_.HookPart[Any] = messages_.HookPart(
hook_id="approve_tc_42",
hook_type="SomethingElse",
status="deferred",
status="pending",
tool_call_id="tc_42",
)
assert approvals.tool_call_id_for(hook) is None
Expand Down
2 changes: 1 addition & 1 deletion tests/agents/ui/ai_sdk/test_inbound_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def test_to_messages_keeps_deferred_approval_tombstone() -> None:
hook_part = messages[1].parts[0]
assert isinstance(hook_part, messages_.HookPart)
assert hook_part.hook_type == "ToolApproval"
assert hook_part.status == "deferred"
assert hook_part.status == "pending"


def test_to_messages_drops_resolved_approval_tombstone() -> None:
Expand Down
8 changes: 4 additions & 4 deletions tests/agents/ui/ai_sdk/test_outbound_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def test_merge_approval_signals_deferred_then_resolved() -> None:
messages_.HookPart(
hook_id="approve_tc1",
hook_type="ToolApproval",
status="deferred",
status="pending",
tool_call_id="tc1",
)
],
Expand Down Expand Up @@ -360,7 +360,7 @@ def test_to_ui_messages_internal_role_merges_approval() -> None:
messages_.HookPart(
hook_id="approve_tc1",
hook_type="ToolApproval",
status="deferred",
status="pending",
tool_call_id="tc1",
)
],
Expand Down Expand Up @@ -630,7 +630,7 @@ def _parked_turn(*, hook_id: str, tool_call_id: str) -> list[messages_.Message]:
id="part-1",
hook_id=hook_id,
hook_type="ToolApproval",
status="deferred",
status="pending",
metadata={"tool": "bash", "kwargs": {"command": "ls"}},
tool_call_id=tool_call_id,
)
Expand Down Expand Up @@ -664,5 +664,5 @@ def test_deferred_approval_hook_roundtrips_tool_call_id() -> None:
if isinstance(p, messages_.HookPart)
]
assert hook.hook_id == hook_id
assert hook.status == "deferred"
assert hook.status == "pending"
assert hook.tool_call_id == "tc-1"
Loading
Loading