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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/specs/004-python-function-calling-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
| AG-UI provider boundary | Completed local approval controls from AG-UI request and snapshot replay are absent from raw chat-client input while deferred and hosted approvals keep their respective in-run/provider paths. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_does_not_forward_resolved_local_approval_control_to_chat_client`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_canonical_resume_preserves_hosted_approval_for_provider`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_removes_duplicate_completed_controls`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_pairs_reused_call_ids_by_occurrence`, `packages/ag-ui/tests/ag_ui/test_run.py::test_canonical_hosted_approval_resume_rejects_edited_arguments_without_mutating_pending` |
| AG-UI standard approval payload | Agent and workflow tool approvals emit canonical `tool_call` interrupts. `approved` plus full-replacement `editedArgs` executes once and replays idempotently, while legacy `accepted` plus direct partial edits remains supported. Hosted approvals remain decision-only. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_applies_standard_full_replacement_edited_args`, `test_endpoint_agent_approval_replayed_standard_edited_resume_is_idempotent`, `test_endpoint_agent_approval_resume_entry_applies_edited_arguments`, `test_workflow_endpoint_emits_canonical_tool_approval_interrupt`, `test_workflow_endpoint_accepts_canonical_tool_approval_resume`, `test_workflow_endpoint_applies_canonical_approval_edited_args`, `test_workflow_endpoint_accepts_legacy_partial_approval_edits`, `test_workflow_endpoint_hosted_approval_rejects_argument_edits` |
| AG-UI cancellation | A cancelled interrupt executes zero times and completes normally, including an identical retry during retained cancellation state; resolved siblings in the same complete resume still execute once. Workflow cancellation clears both runner correlation and the owning agent executor's pending request so later approvals remain resumable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_cancelled_resume_entry_completes_without_execution`, `test_endpoint_agent_approval_replayed_cancellation_completes_idempotently`, `test_endpoint_agent_approval_mixed_cancelled_and_resolved_resume_executes_resolved_tool`, `test_endpoint_workflow_request_info_cancelled_resume_completes_normally`, `test_workflow_endpoint_cancelled_agent_approval_does_not_block_next_approval` |
| AG-UI shared workflow interrupt ownership | A direct shared `Workflow` request-info interrupt can only be resolved or cancelled by the Snapshot Scope and AG-UI thread that created it. Ownership follows the authoritative pending request occurrence, and explicitly threaded cold checkpoint resumes fail closed when ownership is unavailable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_workflow_request_info_rejects_resume_from_different_thread`, `test_endpoint_workflow_request_info_rejects_resume_from_different_scope`, `test_endpoint_workflow_request_info_rejects_cancellation_from_different_thread`, `test_endpoint_workflow_request_info_remains_owned_after_client_disconnect`, `test_endpoint_workflow_request_info_rejects_unowned_pending_interrupt`, `test_endpoint_workflow_checkpoint_resume_rejects_threaded_resume_after_restart` |
| AG-UI approval retention and capacity | Pending authority expires automatically, indeterminate outcomes remain non-retryable until their safety window permits reclamation, and one trusted scope cannot consume another scope's occurrence quota. | `packages/ag-ui/tests/ag_ui/test_approval_lifecycle.py::test_abandoned_pending_occurrence_expires_and_releases_capacity`, `test_indeterminate_occurrence_is_reclaimed_after_its_safety_window`, `test_capacity_is_enforced_per_trusted_scope` |
| AG-UI local executor unavailable on resume | A claimed local occurrence whose executor disappeared releases its unstarted claim, reports temporary unavailability, and remains safely retryable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_remains_retryable_when_local_tool_is_temporarily_unavailable` |
| AG-UI forwarded execution interruption | A provider failure, cancellation, or stream close after forwarding an approval recovers the open occurrence as indeterminate when no idempotency key proves retry safety. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_hosted_approval_becomes_indeterminate_when_provider_stream_fails` |
Expand Down Expand Up @@ -511,6 +512,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
| AG-UI approval-time follow-up | The full grouped user-input pause remains in message history and emits no synthetic `TOOL_CALL_RESULT`. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_follow_up_group_remains_in_history_without_live_tool_result` |
| AG-UI approval execution failure | A grouped executor failure becomes one deterministic terminal error result for the approved call. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_execution_failure_emits_one_terminal_error_result` |
| AG-UI no-approval path | Ordinary tool results do not gain an extra approval result event. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_no_approval_path_emits_no_approval_specific_duplicate_result` |
| AG-UI client-tool request isolation | Client tool declarations are validated before use and remain request-scoped; a rejected collision or earlier successful request cannot change a later request's server-tool execution. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_failed_client_tool_collision_does_not_affect_next_request`, `test_endpoint_client_tools_do_not_persist_into_next_request` |
| AG-UI `confirm_changes` snapshot | An accepted synthetic confirmation is replaced only when its original function call has a real result; rejection is cleaned explicitly, and missing accepted results remain inert. | `packages/ag-ui/tests/ag_ui/test_confirm_changes_snapshot.py` |
| AG-UI malformed `confirm_changes` metadata | Non-list tool-call metadata and malformed argument JSON are ignored without guessing a target call. | `test_confirm_changes_target_ignores_non_list_tool_calls`, `test_confirm_changes_target_rejects_malformed_arguments_json` |
| Compaction pair integrity | Adjacent and non-adjacent pairs, including assistant-embedded results and completed reused-id occurrences, remain atomic without pairing ambiguous or out-of-order ids. | `packages/core/tests/core/test_compaction.py::test_group_annotations_keep_tool_call_and_tool_result_atomic`, `test_group_annotations_include_reasoning_in_tool_call_group`, `test_group_annotations_pair_nonadjacent_function_result_by_call_id`, `test_group_annotations_pair_multiple_nonadjacent_results_with_declaration`, `test_group_annotations_pair_completed_reused_call_id_occurrences`, `test_group_annotations_close_assistant_embedded_result_before_reused_call_id`, `test_sliding_window_does_not_retain_orphan_result_after_assistant_embedded_result`, `test_sliding_window_keeps_reused_call_id_occurrences_atomic`, `test_group_annotations_do_not_pair_ambiguous_duplicate_call_ids` |
Expand Down
3 changes: 3 additions & 0 deletions python/packages/ag-ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ AG-UI protocol integration for building agent UIs with the AG-UI standard.
- `_approval_lifecycle.py` is the sole owner of approval occurrence registration, trusted aliases, authority
validation, claims, terminal outcomes, and retry deduplication. Runner code normalizes AG-UI protocol values and
projects lifecycle outcomes but must not maintain a parallel pending-approval registry.
- Default stateless conversation history is client-controlled, including historical tool calls and results. Never
document conversational tool results as authorization or policy evidence; use deterministic server-side checks,
server-validated approvals, or scoped authoritative snapshots.
- `confirm_changes` snapshot cleanup resolves the synthetic confirmation back to its original `function_call_id`;
it must never concatenate unrelated tool results or record accepted changes without a matching real result.
- SSE keepalive is endpoint-owned transport behavior configured through
Expand Down
16 changes: 16 additions & 0 deletions python/packages/ag-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,22 @@ The `dependencies` parameter accepts any FastAPI dependency, enabling integratio

For a complete authentication example, see [getting_started/server.py](getting_started/server.py).

### Conversation and Tool Result Trust

In the default stateless mode, the AG-UI client sends the conversation history for each run. Treat that history as
untrusted input, including client-supplied `assistant` tool calls and `tool` results. A historical tool result is not
proof that the server emitted the matching call or executed the named backend tool.

Do not use conversation history, tool results, or the model's decision to call a tool as an authorization,
entitlement, approval, or policy signal. Enforce security decisions deterministically in authenticated server code,
such as endpoint dependencies, tool middleware, or the server-validated human-in-the-loop approval flow. Tool
implementations must also authorize the current principal before accessing protected data or performing sensitive
actions.

For applications that need server-authoritative thread history, configure scoped AG-UI Thread Snapshots. Snapshot
mode only accepts user turns and results for backend-issued tool calls when extending stored history. It complements
endpoint authentication and authorization; it does not replace them.

## AG-UI Thread Snapshots

AG-UI Thread Snapshot persistence is opt-in and disabled by default. Existing endpoints keep their current behavior
Expand Down
3 changes: 1 addition & 2 deletions python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
from ._approval_state import _APPROVAL_SCOPE_INPUT_KEY, InMemoryAGUIApprovalStateStore, approval_state_thread_id
from ._message_adapters import normalize_agui_input_messages
from ._predictive_state import PredictiveStateHandler
from ._tooling import collect_server_tools, merge_tools, register_additional_client_tools
from ._tooling import collect_server_tools, merge_tools
from ._run_common import (
FlowState,
_approval_interrupt_for_function_call, # type: ignore
Expand Down Expand Up @@ -2303,7 +2303,6 @@ async def run_agent_stream(
approval_snapshot_reconciliations: list[ApprovalSnapshotReconciliation] = []
client_tools = convert_agui_tools_to_agent_framework(input_data.get("tools"))
server_tools = collect_server_tools(agent)
register_additional_client_tools(agent, client_tools)
tools = merge_tools(server_tools, client_tools)
approval_resume_messages, handled_resume_ids, cancelled_resume_ids, resume_error = (
_canonical_approval_resume_messages(
Expand Down
21 changes: 0 additions & 21 deletions python/packages/ag-ui/agent_framework_ag_ui/_tooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import logging
from typing import TYPE_CHECKING, Any

from agent_framework import BaseChatClient
from agent_framework._tools import _append_unique_tools # pyright: ignore[reportPrivateUsage]

if TYPE_CHECKING:
Expand Down Expand Up @@ -71,26 +70,6 @@ def collect_server_tools(agent: SupportsAgentRun) -> list[Any]:
return server_tools


def register_additional_client_tools(agent: SupportsAgentRun, client_tools: list[Any] | None) -> None:
"""Register client tools as additional declaration-only tools to avoid server execution.

Args:
agent: Agent instance to register tools on. Works with Agent
or any agent with a client attribute.
client_tools: List of client tools to register.
"""
if not client_tools:
return

client = getattr(agent, "client", None)
if client is None:
return

if isinstance(client, BaseChatClient) and client.function_invocation_configuration is not None: # type: ignore[attr-defined]
client.function_invocation_configuration["additional_tools"] = client_tools # type: ignore[attr-defined]
logger.debug(f"[TOOLS] Registered {len(client_tools)} client tools as additional_tools (declaration-only)")


def _has_approval_tools(tools: list[Any]) -> bool:
"""Check if any tools require approval."""
return any(getattr(tool, "approval_mode", None) == "always_require" for tool in tools)
Expand Down
Loading
Loading