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.
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/AGENTS.md +4 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/PKG-INFO +2 -2
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/_agent.py +9 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_agent_run.py +219 -25
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_approval_lifecycle.py +38 -3
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_approval_state.py +3 -1
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_message_adapters.py +36 -11
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_run_common.py +3 -3
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_utils.py +25 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_workflow_run.py +6 -22
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/__init__.py +7 -7
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/__main__.py +8 -8
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/pyproject.toml +2 -2
- {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
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_a2ui.py +73 -1
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_agent_wrapper_comprehensive.py +43 -26
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_endpoint.py +1675 -552
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_message_adapters.py +67 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_multi_turn.py +73 -1
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_run.py +135 -1
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/.gitignore +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/LICENSE +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/README.md +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/__init__.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/__init__.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/_factory.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/_state.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_agent.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_client.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_endpoint.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_event_converters.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_feature_usage.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_http_service.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_predictive_state.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_snapshot_session.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_snapshots.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_state.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_tooling.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_types.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_workflow.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/py.typed +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/.env.example +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/.vscode/settings.json +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/README.md +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/agents/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui_examples/server/main.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/README.md +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/client.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/client_advanced.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/client_with_agent.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/getting_started/server.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/conftest.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/event_stream.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/conftest.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_hitl.py +0 -0
- {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
- {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
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_subgraphs.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/golden/test_scenario_workflow.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/sse_helpers.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_ag_ui_client.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_approval_lifecycle.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_approval_result_event.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_approval_state.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_confirm_changes_snapshot.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_event_converters.py +0 -0
- {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
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_handoff_replay.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_http_round_trip.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_http_service.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_message_hygiene.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_predictive_state.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_public_exports.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_run_common.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_service_thread_id.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_snapshot_session.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_snapshots.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_structured_output.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_subgraphs_example_agent.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_tooling.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_types.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_utils.py +0 -0
- {agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/tests/ag_ui/test_workflow_agent.py +0 -0
- {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.
|
|
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.
|
|
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
|
{agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_a2ui/_agent.py
RENAMED
|
@@ -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:
|
{agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_agent_run.py
RENAMED
|
@@ -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":
|
|
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
|
-
|
|
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
|
|
1639
|
-
|
|
1640
|
-
|
|
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(
|
|
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
|
-
|
|
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=
|
|
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(
|
|
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
|
-
|
|
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
|
|
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=
|
|
951
|
+
result_group=result_group,
|
|
917
952
|
snapshot_reconciliation=self._snapshot_reconciliation(occurrence),
|
|
918
953
|
)
|
|
919
954
|
occurrence.outcome = outcome
|
{agent_framework_ag_ui-1.2.1 → agent_framework_ag_ui-1.2.2}/agent_framework_ag_ui/_approval_state.py
RENAMED
|
@@ -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
|
)
|