agent-framework-ag-ui 1.2.1__tar.gz → 1.2.2__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.
Files changed (103) hide show
  1. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/AGENTS.md +4 -0
  2. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/PKG-INFO +2 -2
  3. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/_agent.py +9 -0
  4. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_agent_run.py +219 -25
  5. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_approval_lifecycle.py +38 -3
  6. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_approval_state.py +3 -1
  7. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_message_adapters.py +36 -11
  8. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_run_common.py +3 -3
  9. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_utils.py +25 -0
  10. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_workflow_run.py +6 -22
  11. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/__init__.py +7 -7
  12. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/__main__.py +8 -8
  13. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/pyproject.toml +2 -2
  14. {agent_framework_ag_ui-1.2.1/agent_framework_ag_ui_examples/server → agent_framework_ag_ui-1.2.2/tests/ag_ui/golden}/__init__.py +1 -1
  15. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_a2ui.py +73 -1
  16. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_agent_wrapper_comprehensive.py +43 -26
  17. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_endpoint.py +1675 -552
  18. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_message_adapters.py +67 -0
  19. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_multi_turn.py +73 -1
  20. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_run.py +135 -1
  21. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/.gitignore +0 -0
  22. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/LICENSE +0 -0
  23. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/README.md +0 -0
  24. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/__init__.py +0 -0
  25. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/__init__.py +0 -0
  26. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/_factory.py +0 -0
  27. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/_state.py +0 -0
  28. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_agent.py +0 -0
  29. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_client.py +0 -0
  30. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_endpoint.py +0 -0
  31. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_event_converters.py +0 -0
  32. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_feature_usage.py +0 -0
  33. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_http_service.py +0 -0
  34. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_predictive_state.py +0 -0
  35. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_snapshot_session.py +0 -0
  36. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_snapshots.py +0 -0
  37. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_state.py +0 -0
  38. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_tooling.py +0 -0
  39. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_types.py +0 -0
  40. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_workflow.py +0 -0
  41. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/py.typed +0 -0
  42. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/.env.example +0 -0
  43. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/.vscode/settings.json +0 -0
  44. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/README.md +0 -0
  45. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/__init__.py +0 -0
  46. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/a2ui_agents.py +0 -0
  47. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/document_writer_agent.py +0 -0
  48. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/human_in_the_loop_agent.py +0 -0
  49. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/recipe_agent.py +0 -0
  50. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/research_assistant_agent.py +0 -0
  51. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/simple_agent.py +0 -0
  52. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/subgraphs_agent.py +0 -0
  53. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/task_planner_agent.py +0 -0
  54. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/task_steps_agent.py +0 -0
  55. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/ui_generator_agent.py +0 -0
  56. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/weather_agent.py +0 -0
  57. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/weather_state_agent.py +0 -0
  58. {agent_framework_ag_ui-1.2.1/tests/ag_ui/golden → agent_framework_ag_ui-1.2.2/agent_framework_ag_ui_examples/server}/__init__.py +0 -0
  59. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/server/api/backend_tool_rendering.py +0 -0
  60. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/server/main.py +0 -0
  61. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/README.md +0 -0
  62. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/client.py +0 -0
  63. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/client_advanced.py +0 -0
  64. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/client_with_agent.py +0 -0
  65. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/server.py +0 -0
  66. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/conftest.py +0 -0
  67. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/event_stream.py +0 -0
  68. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/conftest.py +0 -0
  69. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_agentic_chat.py +0 -0
  70. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_backend_tools.py +0 -0
  71. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_deterministic_state.py +0 -0
  72. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_generative_ui_agent.py +0 -0
  73. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_generative_ui_tool.py +0 -0
  74. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_hitl.py +0 -0
  75. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_predictive_state.py +0 -0
  76. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_shared_state.py +0 -0
  77. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_subgraphs.py +0 -0
  78. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_workflow.py +0 -0
  79. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/sse_helpers.py +0 -0
  80. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_ag_ui_client.py +0 -0
  81. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_approval_lifecycle.py +0 -0
  82. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_approval_result_event.py +0 -0
  83. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_approval_state.py +0 -0
  84. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_confirm_changes_snapshot.py +0 -0
  85. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_event_converters.py +0 -0
  86. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_forwarded_props_in_metadata.py +0 -0
  87. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_handoff_replay.py +0 -0
  88. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_http_round_trip.py +0 -0
  89. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_http_service.py +0 -0
  90. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_message_hygiene.py +0 -0
  91. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_predictive_state.py +0 -0
  92. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_public_exports.py +0 -0
  93. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_run_common.py +0 -0
  94. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_service_thread_id.py +0 -0
  95. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_snapshot_session.py +0 -0
  96. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_snapshots.py +0 -0
  97. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_structured_output.py +0 -0
  98. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_subgraphs_example_agent.py +0 -0
  99. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_tooling.py +0 -0
  100. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_types.py +0 -0
  101. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_utils.py +0 -0
  102. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_workflow_agent.py +0 -0
  103. {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_workflow_run.py +0 -0
@@ -40,6 +40,10 @@ AG-UI protocol integration for building agent UIs with the AG-UI standard.
40
40
  - `_approval_lifecycle.py` is the sole owner of approval occurrence registration, trusted aliases, authority
41
41
  validation, claims, terminal outcomes, and retry deduplication. Runner code normalizes AG-UI protocol values and
42
42
  projects lifecycle outcomes but must not maintain a parallel pending-approval registry.
43
+ - Local tool-approval interrupt ids use the Agent Framework `function_call.id` occurrence identity; `toolCallId`
44
+ remains the provider/service `call_id`. Hosted approvals preserve the provider-issued approval request id. The
45
+ lifecycle stores these identities separately so resume responses carry the authoritative approval occurrence id
46
+ without rewriting tool-result correlation ids.
43
47
  - Default stateless conversation history is client-controlled, including historical tool calls and results. Never
44
48
  document conversational tool results as authorization or policy evidence; use deterministic server-side checks,
45
49
  server-validated approvals, or scoped authoritative snapshots.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-framework-ag-ui
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: AG-UI protocol integration for Agent Framework
5
5
  Project-URL: homepage, https://aka.ms/agent-framework
6
6
  Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.10
21
21
  Requires-Dist: ag-ui-protocol<0.2,>=0.1.19
22
- Requires-Dist: agent-framework-core<2,>=1.15.0
22
+ Requires-Dist: agent-framework-core<2,>=1.17.0
23
23
  Requires-Dist: fastapi<0.140.0,>=0.121.0
24
24
  Requires-Dist: httpx<1,>=0.28.1
25
25
  Requires-Dist: sse-starlette<4,>=3.4.5
@@ -351,6 +351,15 @@ class A2UIAgent:
351
351
  def context_providers(self) -> Any:
352
352
  return getattr(self.inner_agent, "context_providers", [])
353
353
 
354
+ @property
355
+ def service_session_state_keys(self) -> Any:
356
+ return getattr(self.inner_agent, "service_session_state_keys", ())
357
+
358
+ def __getattr__(self, name: str) -> Any:
359
+ if name == "create_conversation":
360
+ return getattr(self.inner_agent, name)
361
+ raise AttributeError(name)
362
+
354
363
  # -- public run -------------------------------------------------------
355
364
 
356
365
  def run(self, messages: Any = None, *, stream: bool = False, **kwargs: Any) -> Any:
@@ -36,6 +36,7 @@ from agent_framework import (
36
36
  MESSAGE_INJECTION_PENDING_MESSAGES_STATE_KEY,
37
37
  Message,
38
38
  SupportsAgentRun,
39
+ WorkflowAgent,
39
40
  )
40
41
  from agent_framework._middleware import FunctionMiddlewarePipeline
41
42
  from agent_framework._tools import (
@@ -101,6 +102,8 @@ from ._snapshots import (
101
102
  )
102
103
  from ._snapshot_session import ThreadSnapshotSession, _event_messages_to_snapshot_dicts
103
104
  from ._utils import (
105
+ _approval_interrupt_id,
106
+ _function_call_server_label,
104
107
  canonical_function_arguments,
105
108
  convert_agui_tools_to_agent_framework,
106
109
  generate_event_id,
@@ -714,13 +717,6 @@ def _pending_approval_server_label(entry: ApprovalOccurrence) -> str | None:
714
717
  return entry.server_label
715
718
 
716
719
 
717
- def _function_call_server_label(function_call: Content | None) -> str | None:
718
- if function_call is None:
719
- return None
720
- server_label = function_call.additional_properties.get("server_label")
721
- return server_label if isinstance(server_label, str) and server_label else None
722
-
723
-
724
720
  def _function_call_execution_owner(
725
721
  function_call: Content,
726
722
  tools: list[Any] | None,
@@ -881,10 +877,11 @@ def _register_server_generated_approval_response(
881
877
  name=response.function_call.name,
882
878
  arguments=arguments,
883
879
  aliases=[str(response.function_call.call_id)] if response.function_call.call_id else None,
880
+ response_id=str(response_id),
884
881
  server_label=_function_call_server_label(response.function_call),
885
882
  )
886
883
  if response.approved is not True:
887
- lifecycle.claim_batch(
884
+ batch = lifecycle.claim_batch(
888
885
  thread_id=thread_id,
889
886
  decisions=[
890
887
  ResumeDecision(
@@ -895,6 +892,14 @@ def _register_server_generated_approval_response(
895
892
  )
896
893
  ],
897
894
  )
895
+ if batch.authorized_executions:
896
+ intent = batch.authorized_executions[0]
897
+ lifecycle.begin_execution(intent, owner=intent.owner)
898
+ lifecycle.settle_forwarded(
899
+ intent,
900
+ [response],
901
+ owner=intent.owner,
902
+ )
898
903
  return None
899
904
  if execution_owner is ApprovalExecutionOwner.UNAVAILABLE:
900
905
  return None
@@ -1384,9 +1389,10 @@ def _canonical_approval_resume_messages(
1384
1389
  original_arguments=pending_arguments,
1385
1390
  )
1386
1391
  )
1392
+ response_id = pending_entry.response_id or interrupt_id
1387
1393
  function_approvals = [
1388
1394
  {
1389
- "id": interrupt_id,
1395
+ "id": response_id,
1390
1396
  "call_id": pending_entry.identity.call_id,
1391
1397
  "name": _pending_approval_name(pending_entry) or "",
1392
1398
  "approved": accepted,
@@ -1423,7 +1429,7 @@ def _canonical_approval_resume_messages(
1423
1429
  call_id=sibling_call_id,
1424
1430
  name=function_call.name,
1425
1431
  arguments=sibling_arguments,
1426
- aliases=[sibling_call_id],
1432
+ response_id=str(response_id),
1427
1433
  server_label=_function_call_server_label(function_call),
1428
1434
  )
1429
1435
  lifecycle_decisions.append(
@@ -1493,6 +1499,7 @@ async def _resolve_approval_responses(
1493
1499
  tools: list[Any],
1494
1500
  agent: SupportsAgentRun,
1495
1501
  run_kwargs: dict[str, Any],
1502
+ invocation_session: AgentSession,
1496
1503
  thread_id: str = "",
1497
1504
  validated_approved_responses: list[Content] | None = None,
1498
1505
  *,
@@ -1512,6 +1519,7 @@ async def _resolve_approval_responses(
1512
1519
  tools: List of available tools
1513
1520
  agent: The agent instance (to get client and config)
1514
1521
  run_kwargs: Kwargs for tool execution
1522
+ invocation_session: Restored session for middleware and tool execution.
1515
1523
  thread_id: The conversation thread ID used to scope registry keys.
1516
1524
  validated_approved_responses: Optional collector for validated local
1517
1525
  approval responses, including controls removed because the matching
@@ -1534,6 +1542,7 @@ async def _resolve_approval_responses(
1534
1542
  valid_response_content_ids: set[int] | None = None
1535
1543
  pending_local_response_content_ids: set[int] | None = None
1536
1544
  validated_forwarded_approvals: list[Content] = []
1545
+ deferred_response_content_ids: set[int] = set()
1537
1546
  response_content_ids_to_strip: set[int] = set()
1538
1547
  valid_response_content_ids = set()
1539
1548
  pending_local_response_content_ids = set()
@@ -1621,7 +1630,7 @@ async def _resolve_approval_responses(
1621
1630
  else:
1622
1631
  primary_response.function_call.additional_properties.pop("server_label", None)
1623
1632
  if (
1624
- primary_response.approved is True
1633
+ (primary_response.approved is True or pending_entry.owner is ApprovalExecutionOwner.DEFERRED)
1625
1634
  and lifecycle is not None
1626
1635
  and authorized_executions is not None
1627
1636
  and primary_response.function_call is not None
@@ -1634,10 +1643,11 @@ async def _resolve_approval_responses(
1634
1643
  continue
1635
1644
  intents_by_response_content_id[id(primary_response)] = intent
1636
1645
  valid_response_content_ids.add(id(primary_response))
1637
- if (
1638
- primary_response.approved is True
1639
- and intent is not None
1640
- and intent.owner in {ApprovalExecutionOwner.HOSTED, ApprovalExecutionOwner.DEFERRED}
1646
+ if pending_entry.owner is ApprovalExecutionOwner.DEFERRED:
1647
+ deferred_response_content_ids.add(id(primary_response))
1648
+ if intent is not None and (
1649
+ intent.owner is ApprovalExecutionOwner.DEFERRED
1650
+ or (primary_response.approved is True and intent.owner is ApprovalExecutionOwner.HOSTED)
1641
1651
  ):
1642
1652
  validated_forwarded_approvals.append(primary_response)
1643
1653
  if not server_label:
@@ -1702,7 +1712,7 @@ async def _resolve_approval_responses(
1702
1712
  fcc_todo = {
1703
1713
  response_id: response
1704
1714
  for response_id, response in fcc_todo.items()
1705
- if id(response) in valid_response_content_ids
1715
+ if id(response) in valid_response_content_ids and id(response) not in deferred_response_content_ids
1706
1716
  }
1707
1717
  if not fcc_todo:
1708
1718
  return []
@@ -1748,6 +1758,7 @@ async def _resolve_approval_responses(
1748
1758
  tools=tools,
1749
1759
  middleware_pipeline=middleware_pipeline,
1750
1760
  config=config,
1761
+ invocation_session=invocation_session,
1751
1762
  )
1752
1763
  except Exception as exc:
1753
1764
  logger.exception("Failed to execute approved tool call; injecting error result: %s", exc)
@@ -2109,12 +2120,20 @@ def _text_events_to_snapshot_messages(events: list[BaseEvent]) -> list[dict[str,
2109
2120
  return [message for message in messages if message.get("content")]
2110
2121
 
2111
2122
 
2112
- def _restore_session_continuation_state(session: AgentSession, snapshot: AGUIThreadSnapshot | None) -> None:
2123
+ def _restore_session_continuation_state(
2124
+ session: AgentSession,
2125
+ snapshot: AGUIThreadSnapshot | None,
2126
+ *,
2127
+ restore_service_session_id: bool,
2128
+ excluded_state_keys: set[str],
2129
+ ) -> None:
2113
2130
  """Restore typed private state from trusted snapshot storage."""
2114
2131
  if snapshot is None or snapshot.session_state is None:
2115
2132
  return
2116
2133
  serialized_state = copy.deepcopy(snapshot.session_state)
2117
2134
  service_session_id = serialized_state.pop(_PROVIDER_SERVICE_SESSION_ID_STATE_KEY, None)
2135
+ for key in excluded_state_keys:
2136
+ serialized_state.pop(key, None)
2118
2137
  try:
2119
2138
  restored = AgentSession.from_dict(
2120
2139
  {
@@ -2130,7 +2149,7 @@ def _restore_session_continuation_state(session: AgentSession, snapshot: AGUIThr
2130
2149
  session.session_id,
2131
2150
  )
2132
2151
  return
2133
- if service_session_id is not None:
2152
+ if restore_service_session_id and service_session_id is not None:
2134
2153
  session.service_session_id = restored.service_session_id
2135
2154
  session.state.update(restored.state)
2136
2155
 
@@ -2176,14 +2195,24 @@ def _request_state_protected_keys(agent: SupportsAgentRun) -> set[str]:
2176
2195
  InMemoryHistoryProvider.DEFAULT_SOURCE_ID,
2177
2196
  MESSAGE_INJECTION_PENDING_MESSAGES_STATE_KEY,
2178
2197
  *(provider.source_id for provider in context_providers),
2198
+ *_provider_service_session_state_keys(agent),
2179
2199
  }
2180
2200
 
2181
2201
 
2202
+ def _provider_service_session_state_keys(agent: SupportsAgentRun) -> set[str]:
2203
+ """Return provider-owned session-state keys that must not cross stateless runs."""
2204
+ keys = getattr(agent, "service_session_state_keys", ())
2205
+ if not isinstance(keys, (list, tuple, set, frozenset)):
2206
+ return set()
2207
+ return {key for key in keys if isinstance(key, str)}
2208
+
2209
+
2182
2210
  def _serialize_session_continuation_state(
2183
2211
  session: AgentSession,
2184
2212
  agent: SupportsAgentRun,
2185
2213
  *,
2186
2214
  shared_state_keys: set[str],
2215
+ include_service_session_id: bool,
2187
2216
  ) -> dict[str, Any] | None:
2188
2217
  """Serialize server-owned state while preserving each AG-UI State Authority."""
2189
2218
  context_providers = cast(list[Any], getattr(agent, "context_providers", []))
@@ -2193,13 +2222,16 @@ def _serialize_session_continuation_state(
2193
2222
  _PROVIDER_SERVICE_SESSION_ID_STATE_KEY,
2194
2223
  *(provider.source_id for provider in context_providers if isinstance(provider, HistoryProvider)),
2195
2224
  }
2225
+ if not include_service_session_id:
2226
+ excluded_keys.update(_provider_service_session_state_keys(agent))
2196
2227
  continuation_state = {key: value for key, value in session.state.items() if key not in excluded_keys}
2197
- if not continuation_state and session.service_session_id is None:
2228
+ service_session_id = session.service_session_id if include_service_session_id else None
2229
+ if not continuation_state and service_session_id is None:
2198
2230
  return None
2199
2231
 
2200
2232
  serialized_session = AgentSession(
2201
2233
  session_id=session.session_id,
2202
- service_session_id=session.service_session_id,
2234
+ service_session_id=service_session_id,
2203
2235
  )
2204
2236
  serialized_session.state.update(continuation_state)
2205
2237
  serialized_payload = serialized_session.to_dict()
@@ -2214,6 +2246,7 @@ def _safe_serialize_session_continuation_state(
2214
2246
  agent: SupportsAgentRun,
2215
2247
  *,
2216
2248
  shared_state_keys: set[str],
2249
+ include_service_session_id: bool,
2217
2250
  ) -> dict[str, Any] | None:
2218
2251
  """Return JSON-safe continuation state without failing a completed run."""
2219
2252
  try:
@@ -2221,6 +2254,7 @@ def _safe_serialize_session_continuation_state(
2221
2254
  session,
2222
2255
  agent,
2223
2256
  shared_state_keys=shared_state_keys,
2257
+ include_service_session_id=include_service_session_id,
2224
2258
  )
2225
2259
  if serialized_state is None:
2226
2260
  return None
@@ -2256,6 +2290,121 @@ def _split_service_session_input(
2256
2290
  return snapshot_messages[len(stored_snapshot_messages) :], snapshot_messages
2257
2291
 
2258
2292
 
2293
+ def _legacy_tool_message_approval_resume(
2294
+ messages: Sequence[Mapping[str, Any]],
2295
+ *,
2296
+ lifecycle: ApprovalLifecycle,
2297
+ thread_id: str,
2298
+ submitted_messages: Sequence[Mapping[str, Any]] | None = None,
2299
+ ) -> tuple[list[dict[str, Any]], set[int], str | None] | None:
2300
+ """Translate unambiguous legacy tool-result approvals to canonical resume entries."""
2301
+ entries: list[dict[str, Any]] = []
2302
+ translated_message_ids: set[int] = set()
2303
+ error: str | None = None
2304
+ seen_interrupt_ids: set[str] = set()
2305
+ retained_occurrences_by_call_id: dict[str, list[ApprovalOccurrence]] = {}
2306
+ pending_local_occurrences_by_call_id: dict[str, list[ApprovalOccurrence]] = {}
2307
+ for occurrence in lifecycle.occurrences_for_thread(thread_id=thread_id):
2308
+ retained_occurrences_by_call_id.setdefault(occurrence.identity.call_id, []).append(occurrence)
2309
+ if occurrence.status is ApprovalStatus.PENDING and occurrence.server_label is None:
2310
+ pending_local_occurrences_by_call_id.setdefault(occurrence.identity.call_id, []).append(occurrence)
2311
+
2312
+ submitted = messages if submitted_messages is None else submitted_messages
2313
+ trailing_tool_message_ids: set[int] = set()
2314
+ for message in reversed(submitted):
2315
+ if str(message.get("role", "")).lower() != "tool":
2316
+ break
2317
+ trailing_tool_message_ids.add(id(message))
2318
+
2319
+ latest_function_call_by_id: dict[str, tuple[str, str] | None] = {}
2320
+ for message in messages:
2321
+ if str(message.get("role", "")).lower() != "assistant":
2322
+ continue
2323
+ raw_tool_calls = message.get("tool_calls") or message.get("toolCalls")
2324
+ if not isinstance(raw_tool_calls, list):
2325
+ continue
2326
+ for raw_tool_call in raw_tool_calls:
2327
+ if not isinstance(raw_tool_call, Mapping):
2328
+ continue
2329
+ function = raw_tool_call.get("function")
2330
+ if isinstance(function, Mapping) and function.get("name") == "confirm_changes" and raw_tool_call.get("id"):
2331
+ latest_function_call_by_id[str(raw_tool_call["id"])] = None
2332
+ continue
2333
+ if not isinstance(function, Mapping) or not raw_tool_call.get("id") or not function.get("name"):
2334
+ continue
2335
+ call_id = str(raw_tool_call["id"])
2336
+ parsed_call = Content.from_function_call(
2337
+ call_id=call_id,
2338
+ name=str(function["name"]),
2339
+ arguments=function.get("arguments"),
2340
+ )
2341
+ latest_function_call_by_id[call_id] = (
2342
+ str(function["name"]),
2343
+ canonical_function_arguments(parsed_call) or "{}",
2344
+ )
2345
+
2346
+ for message in submitted:
2347
+ if id(message) not in trailing_tool_message_ids:
2348
+ continue
2349
+ call_id = message.get("tool_call_id") or message.get("toolCallId") or message.get("actionExecutionId")
2350
+ if not call_id:
2351
+ continue
2352
+ raw_content = message.get("content")
2353
+ if raw_content is None:
2354
+ raw_content = message.get("result")
2355
+ if isinstance(raw_content, str):
2356
+ try:
2357
+ payload = json.loads(raw_content)
2358
+ except json.JSONDecodeError:
2359
+ continue
2360
+ else:
2361
+ payload = raw_content
2362
+ if not isinstance(payload, Mapping) or "accepted" not in payload:
2363
+ continue
2364
+ call_id_string = str(call_id)
2365
+ if call_id_string in latest_function_call_by_id and latest_function_call_by_id[call_id_string] is None:
2366
+ continue
2367
+ matching_occurrences = retained_occurrences_by_call_id.get(call_id_string, [])
2368
+ pending_occurrences = pending_local_occurrences_by_call_id.get(call_id_string, [])
2369
+ if not pending_occurrences:
2370
+ continue
2371
+ translated_message_ids.add(id(message))
2372
+ submitted_call = latest_function_call_by_id.get(call_id_string)
2373
+ if submitted_call != (
2374
+ pending_occurrences[0].name,
2375
+ pending_occurrences[0].arguments,
2376
+ ):
2377
+ error = (
2378
+ f"Legacy AG-UI tool-message approval call_id '{call_id}' does not match the pending tool operation. "
2379
+ "Retry with the canonical approval interrupt id."
2380
+ )
2381
+ continue
2382
+ if len(matching_occurrences) != 1 or len(pending_occurrences) != 1:
2383
+ error = (
2384
+ f"Legacy AG-UI tool-message approval call_id '{call_id}' does not identify exactly one retained "
2385
+ "pending local occurrence. Retry with the canonical approval interrupt id."
2386
+ )
2387
+ continue
2388
+ interrupt_id = pending_occurrences[0].identity.interrupt_id
2389
+ if interrupt_id in seen_interrupt_ids:
2390
+ error = (
2391
+ f"Legacy AG-UI tool-message approval repeats call_id '{call_id}'. "
2392
+ "Retry with one canonical resume entry."
2393
+ )
2394
+ continue
2395
+ seen_interrupt_ids.add(interrupt_id)
2396
+ entries.append({"interruptId": interrupt_id, "status": "resolved", "payload": dict(payload)})
2397
+ logger.warning(
2398
+ "Translated a legacy AG-UI tool-message approval for call_id=%s to interrupt id=%s; "
2399
+ "clients must send canonical resume entries because this compatibility path will be removed.",
2400
+ call_id,
2401
+ interrupt_id,
2402
+ )
2403
+ if not entries and not translated_message_ids:
2404
+ return None
2405
+ return entries, translated_message_ids, error
2406
+
2407
+
2259
2408
  async def run_agent_stream(
2260
2409
  input_data: dict[str, Any],
2261
2410
  agent: SupportsAgentRun,
@@ -2389,6 +2538,21 @@ async def run_agent_stream(
2389
2538
  client_tools = convert_agui_tools_to_agent_framework(input_data.get("tools"))
2390
2539
  server_tools = collect_server_tools(agent)
2391
2540
  tools = merge_tools(server_tools, client_tools)
2541
+ workflow_agent_owns_approval = isinstance(agent, WorkflowAgent)
2542
+ if resume_payload is None:
2543
+ legacy_resume = _legacy_tool_message_approval_resume(
2544
+ snapshot_seed_messages if snapshot_seed_messages is not None else raw_messages,
2545
+ lifecycle=approval_state_store.lifecycle,
2546
+ thread_id=approval_thread_id,
2547
+ submitted_messages=raw_messages,
2548
+ )
2549
+ if legacy_resume is not None:
2550
+ resume_payload, translated_message_ids, legacy_resume_error = legacy_resume
2551
+ raw_messages[:] = [message for message in raw_messages if id(message) not in translated_message_ids]
2552
+ if legacy_resume_error is not None:
2553
+ yield RunStartedEvent(run_id=run_id, thread_id=thread_id)
2554
+ yield RunErrorEvent(message=legacy_resume_error, code="APPROVAL_RESUME_INVALID")
2555
+ return
2392
2556
  approval_resume_messages, handled_resume_ids, cancelled_resume_ids, resume_error = (
2393
2557
  _canonical_approval_resume_messages(
2394
2558
  resume_payload,
@@ -2396,7 +2560,8 @@ async def run_agent_stream(
2396
2560
  expected_interrupt_ids=stored_pending_approval_interrupt_ids or None,
2397
2561
  lifecycle=approval_state_store.lifecycle,
2398
2562
  tools=tools,
2399
- has_deferred_owner=approval_state_store.has_tool_approval_state(approval_thread_id),
2563
+ has_deferred_owner=approval_state_store.has_tool_approval_state(approval_thread_id)
2564
+ or workflow_agent_owns_approval,
2400
2565
  authorized_executions=authorized_executions,
2401
2566
  retained_results=retained_approval_results,
2402
2567
  snapshot_reconciliations=approval_snapshot_reconciliations,
@@ -2425,6 +2590,20 @@ async def run_agent_stream(
2425
2590
  await snapshot_session.clear_interrupts(interrupt_ids=retired_interrupt_ids or cancelled_resume_ids or None)
2426
2591
  yield resume_error
2427
2592
  return
2593
+ if cancelled_resume_ids and isinstance(agent, WorkflowAgent):
2594
+ cancelled_workflow_request_ids: set[str] = set()
2595
+ for interrupt_id in cancelled_resume_ids:
2596
+ occurrence = approval_state_store.lifecycle.occurrence_for_alias(
2597
+ thread_id=approval_thread_id,
2598
+ interrupt_id=interrupt_id,
2599
+ )
2600
+ cancelled_workflow_request_ids.add(
2601
+ occurrence.response_id if occurrence and occurrence.response_id else interrupt_id
2602
+ )
2603
+ await agent.workflow.cancel_pending_requests(
2604
+ cancelled_workflow_request_ids,
2605
+ tools=tools,
2606
+ )
2428
2607
  if cancelled_resume_ids and handled_resume_ids == cancelled_resume_ids:
2429
2608
  yield RunStartedEvent(run_id=run_id, thread_id=thread_id)
2430
2609
  _clear_tool_approval_state(approval_state_store, approval_thread_id)
@@ -2532,6 +2711,11 @@ async def run_agent_stream(
2532
2711
 
2533
2712
  # Create session (with service session support)
2534
2713
  if config.use_service_session:
2714
+ if isinstance(default_options, dict) and default_options.get("store") is False:
2715
+ raise ValueError(
2716
+ "use_service_session=True requires provider storage. Set agent default_options['store']=True "
2717
+ "or disable use_service_session."
2718
+ )
2535
2719
  if not config.service_session_id_from_thread_id and not snapshot_session.enabled:
2536
2720
  raise ValueError(
2537
2721
  "use_service_session=True requires snapshot persistence unless service_session_id_from_thread_id=True."
@@ -2557,7 +2741,12 @@ async def run_agent_stream(
2557
2741
  session = created_session
2558
2742
  else:
2559
2743
  session = AgentSession(session_id=thread_id)
2560
- _restore_session_continuation_state(session, stored_snapshot)
2744
+ _restore_session_continuation_state(
2745
+ session,
2746
+ stored_snapshot,
2747
+ restore_service_session_id=config.use_service_session,
2748
+ excluded_state_keys=set() if config.use_service_session else _provider_service_session_state_keys(agent),
2749
+ )
2561
2750
  protected_session_state_keys = _request_state_protected_keys(agent)
2562
2751
  session.state.update(
2563
2752
  {
@@ -2645,6 +2834,7 @@ async def run_agent_stream(
2645
2834
  tools_for_execution,
2646
2835
  agent,
2647
2836
  run_kwargs,
2837
+ session,
2648
2838
  approval_thread_id,
2649
2839
  validated_approved_responses,
2650
2840
  lifecycle=approval_state_store.lifecycle,
@@ -2693,6 +2883,7 @@ async def run_agent_stream(
2693
2883
  session,
2694
2884
  agent,
2695
2885
  shared_state_keys=set(flow.current_state).difference(protected_session_state_keys),
2886
+ include_service_session_id=config.use_service_session,
2696
2887
  ),
2697
2888
  )
2698
2889
  _save_tool_approval_state(session, approval_state_store, approval_thread_id)
@@ -2788,12 +2979,12 @@ async def run_agent_stream(
2788
2979
  # Register pending approval requests so we can validate responses later
2789
2980
  if content_type == "function_approval_request":
2790
2981
  if content.id and content.function_call and content.function_call.name:
2791
- canonical_interrupt_id = content.function_call.call_id or content.id
2982
+ server_label = _function_call_server_label(content.function_call)
2983
+ canonical_interrupt_id = _approval_interrupt_id(content)
2792
2984
  provider_approval_thread_id = approval_state_thread_id(
2793
2985
  scope=approval_scope,
2794
2986
  thread_id=provider_thread_id or thread_id,
2795
2987
  )
2796
- server_label = _function_call_server_label(content.function_call)
2797
2988
  already_approved_requests = _stored_already_approved_requests_for_visible_approval(
2798
2989
  session,
2799
2990
  str(content.id),
@@ -2802,7 +2993,8 @@ async def run_agent_stream(
2802
2993
  execution_owner = _function_call_execution_owner(
2803
2994
  content.function_call,
2804
2995
  tools,
2805
- has_deferred_owner=_TOOL_APPROVAL_STATE_KEY in session.state,
2996
+ has_deferred_owner=_TOOL_APPROVAL_STATE_KEY in session.state
2997
+ or workflow_agent_owns_approval,
2806
2998
  )
2807
2999
  registration_kwargs = {
2808
3000
  "thread_ids": [approval_thread_id, provider_approval_thread_id],
@@ -2810,6 +3002,7 @@ async def run_agent_stream(
2810
3002
  "arguments": canonical_function_arguments(content.function_call) or "{}",
2811
3003
  "request_id": str(content.id),
2812
3004
  "interrupt_id": str(canonical_interrupt_id),
3005
+ "call_id": str(content.function_call.call_id or canonical_interrupt_id),
2813
3006
  "already_approved_requests": already_approved_requests,
2814
3007
  }
2815
3008
  approval_state_store.register(
@@ -3051,6 +3244,7 @@ async def run_agent_stream(
3051
3244
  session,
3052
3245
  agent,
3053
3246
  shared_state_keys=set(flow.current_state).difference(protected_session_state_keys),
3247
+ include_service_session_id=config.use_service_session,
3054
3248
  ),
3055
3249
  )
3056
3250
  _save_tool_approval_state(session, approval_state_store, approval_thread_id)
@@ -181,6 +181,7 @@ class ApprovalOccurrence:
181
181
  owner: ApprovalExecutionOwner
182
182
  scope: str | None = None
183
183
  aliases: tuple[str, ...] = ()
184
+ response_id: str | None = None
184
185
  already_approved_requests: tuple[dict[str, Any], ...] = ()
185
186
  server_label: str | None = None
186
187
  idempotency_key: str | None = None
@@ -276,6 +277,7 @@ class ApprovalLifecycle:
276
277
  name: str,
277
278
  arguments: str,
278
279
  aliases: list[str] | None = None,
280
+ response_id: str | None = None,
279
281
  already_approved_requests: list[dict[str, Any]] | None = None,
280
282
  server_label: str | None = None,
281
283
  idempotency_key: str | None = None,
@@ -293,6 +295,7 @@ class ApprovalLifecycle:
293
295
  owner=owner,
294
296
  scope=scope,
295
297
  aliases=aliases,
298
+ response_id=response_id,
296
299
  already_approved_requests=already_approved_requests,
297
300
  server_label=server_label,
298
301
  idempotency_key=idempotency_key,
@@ -310,6 +313,7 @@ class ApprovalLifecycle:
310
313
  owner: ApprovalExecutionOwner,
311
314
  scope: str | None = None,
312
315
  aliases: list[str] | None = None,
316
+ response_id: str | None = None,
313
317
  already_approved_requests: list[dict[str, Any]] | None = None,
314
318
  server_label: str | None = None,
315
319
  idempotency_key: str | None = None,
@@ -342,6 +346,7 @@ class ApprovalLifecycle:
342
346
  or occurrence.arguments != arguments
343
347
  or occurrence.owner is not owner
344
348
  or occurrence.scope != scope
349
+ or occurrence.response_id != response_id
345
350
  or occurrence.idempotency_key != idempotency_key
346
351
  or occurrence.already_approved_requests != tuple(already_approved_requests or ())
347
352
  or occurrence.server_label != server_label
@@ -371,6 +376,7 @@ class ApprovalLifecycle:
371
376
  owner=owner,
372
377
  scope=scope,
373
378
  aliases=occurrence_aliases,
379
+ response_id=response_id,
374
380
  already_approved_requests=tuple(already_approved_requests or ()),
375
381
  server_label=server_label,
376
382
  idempotency_key=idempotency_key,
@@ -542,6 +548,19 @@ class ApprovalLifecycle:
542
548
  continue
543
549
  occurrence.decision = decision
544
550
  if not decision.accepted:
551
+ if occurrence.owner is ApprovalExecutionOwner.DEFERRED:
552
+ occurrence.status = ApprovalStatus.CLAIMED
553
+ self._emit_event("claim", occurrence)
554
+ intents.append(
555
+ AuthorizedExecution(
556
+ identity=occurrence.identity,
557
+ name=occurrence.name,
558
+ arguments=occurrence.arguments,
559
+ owner=occurrence.owner,
560
+ idempotency_key=occurrence.idempotency_key,
561
+ )
562
+ )
563
+ continue
545
564
  result = Content.from_function_result(
546
565
  call_id=occurrence.identity.call_id,
547
566
  result="Error: Tool call invocation was rejected by user.",
@@ -840,6 +859,11 @@ class ApprovalLifecycle:
840
859
  )
841
860
  if occurrence.status is not ApprovalStatus.EXECUTING:
842
861
  raise ApprovalSettlementConflictError(f"Approval occurrence is not executing: {occurrence.status}.")
862
+ if occurrence.decision is not None and not occurrence.decision.accepted:
863
+ occurrence.status = ApprovalStatus.PENDING
864
+ occurrence.pending_since = self._clock()
865
+ self._emit_event("rejection_recovery", occurrence)
866
+ return None
843
867
  if intent.idempotency_key is not None and intent.idempotency_key == occurrence.idempotency_key:
844
868
  occurrence.status = ApprovalStatus.CLAIMED
845
869
  return intent
@@ -901,19 +925,30 @@ class ApprovalLifecycle:
901
925
  result
902
926
  for result in results
903
927
  if result.type == "function_approval_response"
904
- and result.approved is True
928
+ and isinstance(result.approved, bool)
905
929
  and result.function_call is not None
906
930
  and result.function_call.call_id == occurrence.identity.call_id
907
931
  ]
908
932
  if len(replayable_results) + len(forwarded_responses) != 1:
909
933
  raise ValueError("A hosted approval must record exactly one outcome for its original call.")
934
+ is_rejection = len(forwarded_responses) == 1 and forwarded_responses[0].approved is False
935
+ if is_rejection:
936
+ rejection_result = Content.from_function_result(
937
+ call_id=occurrence.identity.call_id,
938
+ result="Error: Tool call invocation was rejected by user.",
939
+ )
940
+ replayable_results = [ReplayableToolResult(content=rejection_result)]
941
+ result_group = (rejection_result,)
942
+ occurrence.status = ApprovalStatus.REJECTED
943
+ else:
944
+ result_group = tuple(results)
945
+ occurrence.status = ApprovalStatus.SETTLED
910
946
  occurrence.replayable_results = replayable_results
911
- occurrence.status = ApprovalStatus.SETTLED
912
947
  self._remove_pending_aliases(occurrence)
913
948
  outcome = ApprovalOutcome(
914
949
  identity=occurrence.identity,
915
950
  replayable_results=tuple(replayable_results),
916
- result_group=tuple(results),
951
+ result_group=result_group,
917
952
  snapshot_reconciliation=self._snapshot_reconciliation(occurrence),
918
953
  )
919
954
  occurrence.outcome = outcome
@@ -84,6 +84,7 @@ class InMemoryAGUIApprovalStateStore:
84
84
  request_id: str,
85
85
  interrupt_id: str,
86
86
  owner: ApprovalExecutionOwner,
87
+ call_id: str | None = None,
87
88
  scope: ApprovalScope | None = None,
88
89
  already_approved_requests: list[dict[str, Any]] | None = None,
89
90
  server_label: str | None = None,
@@ -95,10 +96,11 @@ class InMemoryAGUIApprovalStateStore:
95
96
  scope=scope,
96
97
  thread_ids=unique_thread_ids,
97
98
  interrupt_id=interrupt_id,
98
- call_id=interrupt_id,
99
+ call_id=call_id or interrupt_id,
99
100
  name=name,
100
101
  arguments=arguments,
101
102
  aliases=[request_id],
103
+ response_id=request_id,
102
104
  already_approved_requests=already_approved_requests,
103
105
  server_label=server_label,
104
106
  )