1313# limitations under the License.
1414from __future__ import annotations
1515
16+ import json
1617import logging
1718from typing import Any
1819from typing import AsyncGenerator
2627from ...agents .readonly_context import ReadonlyContext
2728from ...events .event import Event
2829from ...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
3231from ._base_llm_processor import BaseLlmRequestProcessor
3332from .functions import REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
3433
3534if TYPE_CHECKING :
3635 pass
3736
3837
39- logger = logging .getLogger (" google_adk." + __name__ )
38+ logger = logging .getLogger (' google_adk.' + __name__ )
4039
4140
4241def _parse_tool_confirmation (response : dict [str , Any ]) -> ToolConfirmation :
43- """Parses ToolConfirmation from a function response dict."""
44- return ToolConfirmation .from_response_dict (response )
42+ """Parse ToolConfirmation from a function response dict.
4543
44+ Handles both the direct dict format and the ADK client's
45+ ``{'response': json_string}`` wrapper format.
4646
47- async def _resolve_confirmation_targets (
48- invocation_context : InvocationContext ,
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 (
4954 events : list [Event ],
5055 confirmation_fc_ids : set [str ],
5156 confirmations_by_fc_id : dict [str , ToolConfirmation ],
52- tools_dict : dict [str , BaseTool ],
5357) -> tuple [dict [str , ToolConfirmation ], dict [str , types .FunctionCall ]]:
54- """Find original function calls for confirmed tools and validate them .
58+ """Find original function calls for confirmed tools.
5559
5660 Scans events for ``adk_request_confirmation`` function calls whose IDs
5761 are in *confirmation_fc_ids*, extracts the ``originalFunctionCall`` from
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.
62+ their args, and maps each confirmation to the original FC ID.
6163
6264 Args:
63- invocation_context: Current invocation context.
6465 events: Session events to scan.
6566 confirmation_fc_ids: IDs of ``adk_request_confirmation`` function calls.
6667 confirmations_by_fc_id: Mapping of confirmation FC ID ->
6768 ``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.
7673 """
7774 tool_confirmation_dict : dict [str , ToolConfirmation ] = {}
7875 original_fcs_dict : dict [str , types .FunctionCall ] = {}
7976
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-
9077 for event in events :
9178 event_function_calls = event .get_function_calls ()
9279 if not event_function_calls :
9380 continue
9481
9582 for function_call in event_function_calls :
96- if not function_call . id or function_call .id not in confirmation_fc_ids :
83+ if function_call .id not in confirmation_fc_ids :
9784 continue
9885
9986 args = function_call .args
100- if not args or " originalFunctionCall" not in args :
87+ if ' originalFunctionCall' not in args :
10188 continue
10289 original_function_call = types .FunctionCall (
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 ,
90+ ** args ['originalFunctionCall' ]
13891 )
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-
18092 tool_confirmation_dict [original_function_call .id ] = (
18193 confirmations_by_fc_id [function_call .id ]
18294 )
@@ -203,9 +115,10 @@ async def run_async(
203115 # Step 1: Find the last user-authored event and parse confirmation
204116 # responses from it.
205117 confirmations_by_fc_id : dict [str , ToolConfirmation ] = {}
118+ confirmation_event_index = - 1
206119 for k in range (len (events ) - 1 , - 1 , - 1 ):
207120 event = events [k ]
208- if not event .author or event .author != " user" :
121+ if not event .author or event .author != ' user' :
209122 continue
210123 responses = event .get_function_responses ()
211124 if not responses :
@@ -214,45 +127,29 @@ async def run_async(
214127 for function_response in responses :
215128 if function_response .name != REQUEST_CONFIRMATION_FUNCTION_CALL_NAME :
216129 continue
217- if not function_response .id or function_response .response is None :
218- continue
219130 confirmations_by_fc_id [function_response .id ] = _parse_tool_confirmation (
220131 function_response .response
221132 )
133+ confirmation_event_index = k
222134 break
223135
224136 if not confirmations_by_fc_id :
225137 return
226138
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-
237139 # Step 2: Resolve confirmation targets using extracted helper.
238140 confirmation_fc_ids = set (confirmations_by_fc_id .keys ())
239141 tools_to_resume_with_confirmation , tools_to_resume_with_args = (
240- await _resolve_confirmation_targets (
241- invocation_context ,
242- events ,
243- confirmation_fc_ids ,
244- confirmations_by_fc_id ,
245- tools_dict ,
142+ _resolve_confirmation_targets (
143+ events , confirmation_fc_ids , confirmations_by_fc_id
246144 )
247145 )
248146
249147 if not tools_to_resume_with_confirmation :
250148 return
251149
252150 # Step 3: Remove tools that have already been confirmed (dedup).
253- for event in reversed (events ):
254- if event .author == "user" :
255- break
151+ for i in range (len (events ) - 1 , confirmation_event_index , - 1 ):
152+ event = events [i ]
256153 fr_list = event .get_function_responses ()
257154 if not fr_list :
258155 continue
@@ -270,9 +167,14 @@ async def run_async(
270167 # Step 4: Re-execute the confirmed tools.
271168 if function_response_event := await functions .handle_function_call_list_async (
272169 invocation_context ,
273- list (tools_to_resume_with_args .values ()),
274- tools_dict ,
275- set (tools_to_resume_with_confirmation .keys ()),
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 (),
276178 tools_to_resume_with_confirmation ,
277179 ):
278180 yield function_response_event
0 commit comments