diff --git a/docs/ai-python/content/docs/basics/human-in-the-loop.mdx b/docs/ai-python/content/docs/basics/human-in-the-loop.mdx index 9a7b2c37..0424706b 100644 --- a/docs/ai-python/content/docs/basics/human-in-the-loop.mdx +++ b/docs/ai-python/content/docs/basics/human-in-the-loop.mdx @@ -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) @@ -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) diff --git a/examples/agents/custom_hook.py b/examples/agents/custom_hook.py index 9883224c..6e5baf3d 100644 --- a/examples/agents/custom_hook.py +++ b/examples/agents/custom_hook.py @@ -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( diff --git a/examples/agents/tool_approval.py b/examples/agents/tool_approval.py index 642b0e25..fa483862 100644 --- a/examples/agents/tool_approval.py +++ b/examples/agents/tool_approval.py @@ -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( @@ -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}") diff --git a/examples/apps/coding_agent/main.py b/examples/apps/coding_agent/main.py index ab8a6ca6..3c977b26 100644 --- a/examples/apps/coding_agent/main.py +++ b/examples/apps/coding_agent/main.py @@ -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): diff --git a/examples/apps/web_agent/backend/main.py b/examples/apps/web_agent/backend/main.py index 2e295367..670faaf6 100644 --- a/examples/apps/web_agent/backend/main.py +++ b/examples/apps/web_agent/backend/main.py @@ -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 diff --git a/skills/ai-python-serverless-execution/SKILL.md b/skills/ai-python-serverless-execution/SKILL.md index 9364b9f0..27b0e6d2 100644 --- a/skills/ai-python-serverless-execution/SKILL.md +++ b/skills/ai-python-serverless-execution/SKILL.md @@ -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) diff --git a/skills/ai-python-ui-adapter/SKILL.md b/skills/ai-python-ui-adapter/SKILL.md index c8aa5b45..f3b0a98c 100644 --- a/skills/ai-python-ui-adapter/SKILL.md +++ b/skills/ai-python-ui-adapter/SKILL.md @@ -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 diff --git a/src/ai/agents/agent.py b/src/ai/agents/agent.py index 4ce6c666..386adb16 100644 --- a/src/ai/agents/agent.py +++ b/src/ai/agents/agent.py @@ -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 @@ -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. """ diff --git a/src/ai/agents/hooks.py b/src/ai/agents/hooks.py index 4afb6e05..e421a4aa 100644 --- a/src/ai/agents/hooks.py +++ b/src/ai/agents/hooks.py @@ -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, ) diff --git a/src/ai/agents/ui/ai_sdk/approvals.py b/src/ai/agents/ui/ai_sdk/approvals.py index cb2afbaa..4384f479 100644 --- a/src/ai/agents/ui/ai_sdk/approvals.py +++ b/src/ai/agents/ui/ai_sdk/approvals.py @@ -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, ) diff --git a/src/ai/agents/ui/ai_sdk/outbound_messages.py b/src/ai/agents/ui/ai_sdk/outbound_messages.py index f29b741b..08404efc 100644 --- a/src/ai/agents/ui/ai_sdk/outbound_messages.py +++ b/src/ai/agents/ui/ai_sdk/outbound_messages.py @@ -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( { diff --git a/src/ai/agents/ui/ai_sdk/outbound_stream.py b/src/ai/agents/ui/ai_sdk/outbound_stream.py index 7e379918..808de5d5 100644 --- a/src/ai/agents/ui/ai_sdk/outbound_stream.py +++ b/src/ai/agents/ui/ai_sdk/outbound_stream.py @@ -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) @@ -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) diff --git a/src/ai/telemetry/span.py b/src/ai/telemetry/span.py index 0c7ccc22..45147b4d 100644 --- a/src/ai/telemetry/span.py +++ b/src/ai/telemetry/span.py @@ -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" diff --git a/src/ai/types/events.py b/src/ai/types/events.py index 9ad607f1..ea05dd18 100644 --- a/src/ai/types/events.py +++ b/src/ai/types/events.py @@ -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 @@ -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 @@ -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) diff --git a/src/ai/types/messages.py b/src/ai/types/messages.py index a0f548e0..0a848440 100644 --- a/src/ai/types/messages.py +++ b/src/ai/types/messages.py @@ -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 diff --git a/tests/agents/test_hooks.py b/tests/agents/test_hooks.py index 9af4a007..726a2393 100644 --- a/tests/agents/test_hooks.py +++ b/tests/agents/test_hooks.py @@ -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"} ) @@ -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 @@ -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"] @@ -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 @@ -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}) @@ -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") diff --git a/tests/agents/test_run_blocked.py b/tests/agents/test_run_blocked.py index 2377ffdd..b1e9c159 100644 --- a/tests/agents/test_run_blocked.py +++ b/tests/agents/test_run_blocked.py @@ -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. """ @@ -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) @@ -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() @@ -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) @@ -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}) @@ -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", ) diff --git a/tests/agents/test_validation_error_approval.py b/tests/agents/test_validation_error_approval.py index ea05e48b..c5051d63 100644 --- a/tests/agents/test_validation_error_approval.py +++ b/tests/agents/test_validation_error_approval.py @@ -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( @@ -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( @@ -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, diff --git a/tests/agents/ui/ai_sdk/test_approvals.py b/tests/agents/ui/ai_sdk/test_approvals.py index 5c1761dc..ec3d00f4 100644 --- a/tests/agents/ui/ai_sdk/test_approvals.py +++ b/tests/agents/ui/ai_sdk/test_approvals.py @@ -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" @@ -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 @@ -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 diff --git a/tests/agents/ui/ai_sdk/test_inbound_messages.py b/tests/agents/ui/ai_sdk/test_inbound_messages.py index b657a9ee..0e8c1df4 100644 --- a/tests/agents/ui/ai_sdk/test_inbound_messages.py +++ b/tests/agents/ui/ai_sdk/test_inbound_messages.py @@ -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: diff --git a/tests/agents/ui/ai_sdk/test_outbound_messages.py b/tests/agents/ui/ai_sdk/test_outbound_messages.py index 2617cac9..4529b077 100644 --- a/tests/agents/ui/ai_sdk/test_outbound_messages.py +++ b/tests/agents/ui/ai_sdk/test_outbound_messages.py @@ -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", ) ], @@ -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", ) ], @@ -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, ) @@ -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" diff --git a/tests/agents/ui/ai_sdk/test_outbound_stream.py b/tests/agents/ui/ai_sdk/test_outbound_stream.py index 2d8c1c10..ea40d303 100644 --- a/tests/agents/ui/ai_sdk/test_outbound_stream.py +++ b/tests/agents/ui/ai_sdk/test_outbound_stream.py @@ -185,7 +185,7 @@ async def test_finish_metadata_tracks_tool_and_internal_messages() -> None: id="hook-1", hook_id="approve_tc1", hook_type="ToolApproval", - status="deferred", + status="pending", ) internal = messages_.Message( id="internal-1", @@ -333,7 +333,7 @@ async def test_tool_result_without_streaming_emits_input_start() -> None: async def test_approval_request_hook_emits_approval_event() -> None: - """HookEvent with deferred status emits a UIToolApprovalRequestEvent.""" + """HookEvent with pending status emits a UIToolApprovalRequestEvent.""" out = await _collect( [ # Streaming tool events first @@ -355,7 +355,7 @@ async def test_approval_request_hook_emits_approval_event() -> None: messages_.HookPart( hook_id="approve_tc1", hook_type="ToolApproval", - status="deferred", + status="pending", tool_call_id="tc1", ) ], @@ -363,7 +363,7 @@ async def test_approval_request_hook_emits_approval_event() -> None: hook=messages_.HookPart( hook_id="approve_tc1", hook_type="ToolApproval", - status="deferred", + status="pending", tool_call_id="tc1", ), ), diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 49931e1a..6480d9ca 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -98,7 +98,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": ai.resolve_hook("test_hook", {"approved": True, "reason": "ok"}) assert len(hook_calls) == 1