1313# limitations under the License.
1414from __future__ import annotations
1515
16- import json
1716import logging
1817from typing import Any
1918from typing import AsyncGenerator
2726from ...agents .readonly_context import ReadonlyContext
2827from ...events .event import Event
2928from ...models .llm_request import LlmRequest
29+ from ...tools .base_tool import BaseTool
3030from ...tools .tool_confirmation import ToolConfirmation
31+ from ...tools .tool_context import ToolContext
3132from ._base_llm_processor import BaseLlmRequestProcessor
3233from .functions import REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
3334
3435if TYPE_CHECKING :
3536 pass
3637
3738
38- logger = logging .getLogger (' google_adk.' + __name__ )
39+ logger = logging .getLogger (" google_adk." + __name__ )
3940
4041
4142def _parse_tool_confirmation (response : dict [str , Any ]) -> ToolConfirmation :
42- """Parse ToolConfirmation from a function response dict.
43+ """Parses ToolConfirmation from a function response dict."""
44+ return ToolConfirmation .from_response_dict (response )
4345
44- Handles both the direct dict format and the ADK client's
45- ``{'response': json_string}`` wrapper format.
4646
47- """
48- if response and len (response .values ()) == 1 and 'response' in response .keys ():
49- return ToolConfirmation .model_validate (json .loads (response ['response' ]))
50- return ToolConfirmation .model_validate (response )
51-
52-
53- def _resolve_confirmation_targets (
47+ async def _resolve_confirmation_targets (
48+ invocation_context : InvocationContext ,
5449 events : list [Event ],
5550 confirmation_fc_ids : set [str ],
5651 confirmations_by_fc_id : dict [str , ToolConfirmation ],
52+ tools_dict : dict [str , BaseTool ],
5753) -> tuple [dict [str , ToolConfirmation ], dict [str , types .FunctionCall ]]:
58- """Find original function calls for confirmed tools.
54+ """Find original function calls for confirmed tools and validate them .
5955
6056 Scans events for ``adk_request_confirmation`` function calls whose IDs
6157 are in *confirmation_fc_ids*, extracts the ``originalFunctionCall`` from
62- their args, and maps each confirmation to the original FC ID.
58+ their args, validates that they are registered, actually require confirmation,
59+ and match the original function calls in history, and maps each confirmation
60+ to the original FC ID.
6361
6462 Args:
63+ invocation_context: Current invocation context.
6564 events: Session events to scan.
6665 confirmation_fc_ids: IDs of ``adk_request_confirmation`` function calls.
6766 confirmations_by_fc_id: Mapping of confirmation FC ID ->
6867 ``ToolConfirmation``.
68+ tools_dict: Dictionary of registered tools.
6969
7070 Returns:
7171 Tuple of ``(tool_confirmation_dict, original_fcs_dict)`` where both
7272 are keyed by the ORIGINAL function call IDs.
73+
74+ Raises:
75+ ValueError: If validation of any confirmation target fails.
7376 """
7477 tool_confirmation_dict : dict [str , ToolConfirmation ] = {}
7578 original_fcs_dict : dict [str , types .FunctionCall ] = {}
7679
80+ history_fcs = {
81+ fc .id : (fc , ev )
82+ for ev in events
83+ for fc in ev .get_function_calls ()
84+ if fc .id and fc .name != REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
85+ }
86+ history_fr_events = {
87+ fr .id : ev for ev in events for fr in ev .get_function_responses () if fr .id
88+ }
89+
7790 for event in events :
7891 event_function_calls = event .get_function_calls ()
7992 if not event_function_calls :
8093 continue
8194
8295 for function_call in event_function_calls :
83- if function_call .id not in confirmation_fc_ids :
96+ if not function_call . id or function_call .id not in confirmation_fc_ids :
8497 continue
8598
8699 args = function_call .args
87- if ' originalFunctionCall' not in args :
100+ if not args or " originalFunctionCall" not in args :
88101 continue
89102 original_function_call = types .FunctionCall (
90- ** args ['originalFunctionCall' ]
103+ ** args ["originalFunctionCall" ]
104+ )
105+ if not original_function_call .id :
106+ raise ValueError ("Original function call ID is missing." )
107+ tool_name = original_function_call .name
108+ if not tool_name :
109+ raise ValueError ("Original function call name is missing." )
110+
111+ # Check 1: Is the tool registered?
112+ original_fc_info = history_fcs .get (original_function_call .id )
113+ if not original_fc_info :
114+ raise ValueError (
115+ f"Original function call for ID '{ original_function_call .id } ' not"
116+ " found in session history."
117+ )
118+ original_fc_in_history , original_fc_event = original_fc_info
119+
120+ # If this tool call was authored by another agent, skip it to let that
121+ # agent's processor handle it.
122+ agent = invocation_context .agent
123+ if agent and original_fc_event .author != agent .name :
124+ continue
125+
126+ tool = tools_dict .get (tool_name )
127+ if not tool :
128+ raise ValueError (
129+ f"Tool '{ original_function_call .name } ' is not registered."
130+ )
131+
132+ # Check 2: Does the tool require confirmation for these arguments?
133+ # We check if it is either statically required, or if it was dynamically
134+ # requested in the session history.
135+ temp_tool_context = ToolContext (
136+ invocation_context = invocation_context ,
137+ function_call_id = original_function_call .id ,
91138 )
139+ requires_confirmation = await tool .check_require_confirmation (
140+ original_function_call .args or {}, temp_tool_context
141+ )
142+
143+ requested_in_history = False
144+ if not requires_confirmation :
145+ # Search the history for the response event of the original tool call
146+ original_response_event = history_fr_events .get (
147+ original_function_call .id
148+ )
149+ if (
150+ original_response_event
151+ and original_response_event .actions .requested_tool_confirmations
152+ ):
153+ requested_in_history = (
154+ original_function_call .id
155+ in original_response_event .actions .requested_tool_confirmations
156+ )
157+
158+ if not requires_confirmation and not requested_in_history :
159+ raise ValueError (
160+ f"Tool '{ original_function_call .name } ' does not require"
161+ " confirmation."
162+ )
163+
164+ # Check 3: Does the original function call match name and arguments?
165+ if original_fc_in_history .name != original_function_call .name :
166+ raise ValueError (
167+ f"Function call name mismatch for ID '{ original_function_call .id } ':"
168+ f" history has '{ original_fc_in_history .name } ', confirmation has"
169+ f" '{ original_function_call .name } '."
170+ )
171+
172+ hist_args = original_fc_in_history .args or {}
173+ conf_args = original_function_call .args or {}
174+ if hist_args != conf_args :
175+ raise ValueError (
176+ "Function call arguments mismatch for ID"
177+ f" '{ original_function_call .id } '."
178+ )
179+
92180 tool_confirmation_dict [original_function_call .id ] = (
93181 confirmations_by_fc_id [function_call .id ]
94182 )
@@ -115,10 +203,9 @@ async def run_async(
115203 # Step 1: Find the last user-authored event and parse confirmation
116204 # responses from it.
117205 confirmations_by_fc_id : dict [str , ToolConfirmation ] = {}
118- confirmation_event_index = - 1
119206 for k in range (len (events ) - 1 , - 1 , - 1 ):
120207 event = events [k ]
121- if not event .author or event .author != ' user' :
208+ if not event .author or event .author != " user" :
122209 continue
123210 responses = event .get_function_responses ()
124211 if not responses :
@@ -127,29 +214,45 @@ async def run_async(
127214 for function_response in responses :
128215 if function_response .name != REQUEST_CONFIRMATION_FUNCTION_CALL_NAME :
129216 continue
217+ if not function_response .id or function_response .response is None :
218+ continue
130219 confirmations_by_fc_id [function_response .id ] = _parse_tool_confirmation (
131220 function_response .response
132221 )
133- confirmation_event_index = k
134222 break
135223
136224 if not confirmations_by_fc_id :
137225 return
138226
227+ # Resolve all canonical tools and build tools_dict
228+ tools_dict = {}
229+ if agent is not None and hasattr (agent , "canonical_tools" ):
230+ tools_dict = {
231+ tool .name : tool
232+ for tool in await agent .canonical_tools (
233+ ReadonlyContext (invocation_context )
234+ )
235+ }
236+
139237 # Step 2: Resolve confirmation targets using extracted helper.
140238 confirmation_fc_ids = set (confirmations_by_fc_id .keys ())
141239 tools_to_resume_with_confirmation , tools_to_resume_with_args = (
142- _resolve_confirmation_targets (
143- events , confirmation_fc_ids , confirmations_by_fc_id
240+ await _resolve_confirmation_targets (
241+ invocation_context ,
242+ events ,
243+ confirmation_fc_ids ,
244+ confirmations_by_fc_id ,
245+ tools_dict ,
144246 )
145247 )
146248
147249 if not tools_to_resume_with_confirmation :
148250 return
149251
150252 # Step 3: Remove tools that have already been confirmed (dedup).
151- for i in range (len (events ) - 1 , confirmation_event_index , - 1 ):
152- event = events [i ]
253+ for event in reversed (events ):
254+ if event .author == "user" :
255+ break
153256 fr_list = event .get_function_responses ()
154257 if not fr_list :
155258 continue
@@ -167,14 +270,9 @@ async def run_async(
167270 # Step 4: Re-execute the confirmed tools.
168271 if function_response_event := await functions .handle_function_call_list_async (
169272 invocation_context ,
170- tools_to_resume_with_args .values (),
171- {
172- tool .name : tool
173- for tool in await agent .canonical_tools (
174- ReadonlyContext (invocation_context )
175- )
176- },
177- tools_to_resume_with_confirmation .keys (),
273+ list (tools_to_resume_with_args .values ()),
274+ tools_dict ,
275+ set (tools_to_resume_with_confirmation .keys ()),
178276 tools_to_resume_with_confirmation ,
179277 ):
180278 yield function_response_event
0 commit comments