@@ -35,6 +35,8 @@ class _ApprovalRecord:
3535
3636 approved : bool | list [str ] = field (default_factory = list )
3737 rejected : bool | list [str ] = field (default_factory = list )
38+ rejection_messages : dict [str , str ] = field (default_factory = dict )
39+ sticky_rejection_message : str | None = None
3840
3941
4042@dataclass (eq = False )
@@ -207,8 +209,101 @@ def _get_approval_status_for_key(self, approval_key: str, call_id: str) -> bool
207209 # Per-call approvals are scoped to the exact call ID, so other calls require a new decision.
208210 return None
209211
212+ @staticmethod
213+ def _clear_rejection_message (record : _ApprovalRecord , call_id : str | None ) -> None :
214+ if call_id is None :
215+ return
216+ record .rejection_messages .pop (call_id , None )
217+
218+ @staticmethod
219+ def _get_rejection_message_for_key (record : _ApprovalRecord , call_id : str ) -> str | None :
220+ if record .rejected is True :
221+ if call_id in record .rejection_messages :
222+ return record .rejection_messages [call_id ]
223+ return record .sticky_rejection_message
224+ if isinstance (record .rejected , list ) and call_id in record .rejected :
225+ return record .rejection_messages .get (call_id )
226+ return None
227+
228+ def get_rejection_message (
229+ self ,
230+ tool_name : str ,
231+ call_id : str ,
232+ * ,
233+ tool_namespace : str | None = None ,
234+ existing_pending : ToolApprovalItem | None = None ,
235+ tool_lookup_key : FunctionToolLookupKey | None = None ,
236+ ) -> str | None :
237+ """Return a stored rejection message for a tool call if one exists."""
238+ candidates : list [str ] = []
239+ explicit_namespace = (
240+ tool_namespace if isinstance (tool_namespace , str ) and tool_namespace else None
241+ )
242+ pending_namespace = (
243+ self ._resolve_tool_namespace (existing_pending ) if existing_pending is not None else None
244+ )
245+ pending_key = self ._resolve_approval_key (existing_pending ) if existing_pending else None
246+ pending_tool_name = self ._resolve_tool_name (existing_pending ) if existing_pending else None
247+ pending_keys = (
248+ list (self ._resolve_approval_keys (existing_pending ))
249+ if existing_pending is not None
250+ else []
251+ )
252+
253+ if existing_pending and pending_key is not None :
254+ candidates .append (pending_key )
255+ explicit_keys = (
256+ list (
257+ get_function_tool_approval_keys (
258+ tool_name = tool_name ,
259+ tool_namespace = explicit_namespace ,
260+ tool_lookup_key = tool_lookup_key ,
261+ include_legacy_deferred_key = True ,
262+ )
263+ )
264+ if explicit_namespace is not None or tool_lookup_key is not None
265+ else []
266+ )
267+ for explicit_key in explicit_keys :
268+ if explicit_key not in candidates :
269+ candidates .append (explicit_key )
270+ if not explicit_keys and pending_namespace and pending_key is not None :
271+ if pending_key not in candidates :
272+ candidates .append (pending_key )
273+ if (
274+ explicit_namespace is None
275+ and tool_lookup_key is None
276+ and existing_pending is None
277+ and tool_name not in candidates
278+ ):
279+ candidates .append (tool_name )
280+ if existing_pending :
281+ for pending_candidate in pending_keys :
282+ if pending_candidate not in candidates :
283+ candidates .append (pending_candidate )
284+ if (
285+ pending_namespace is None
286+ and pending_tool_name is not None
287+ and pending_tool_name not in candidates
288+ ):
289+ candidates .append (pending_tool_name )
290+
291+ for candidate in candidates :
292+ approval_entry = self ._approvals .get (candidate )
293+ if not approval_entry :
294+ continue
295+ message = self ._get_rejection_message_for_key (approval_entry , call_id )
296+ if message is not None :
297+ return message
298+ return None
299+
210300 def _apply_approval_decision (
211- self , approval_item : ToolApprovalItem , * , always : bool , approve : bool
301+ self ,
302+ approval_item : ToolApprovalItem ,
303+ * ,
304+ always : bool ,
305+ approve : bool ,
306+ rejection_message : str | None = None ,
212307 ) -> None :
213308 """Record an approval or rejection decision."""
214309 approval_keys = self ._resolve_approval_keys (approval_item ) or ("unknown_tool" ,)
@@ -223,6 +318,14 @@ def _apply_approval_decision(
223318 approval_entry .rejected = [] if approve else True
224319 if not approve :
225320 approval_entry .approved = False
321+ if rejection_message is not None and call_id is not None :
322+ approval_entry .rejection_messages [call_id ] = rejection_message
323+ elif call_id is not None :
324+ self ._clear_rejection_message (approval_entry , call_id )
325+ approval_entry .sticky_rejection_message = rejection_message
326+ else :
327+ approval_entry .rejection_messages .clear ()
328+ approval_entry .sticky_rejection_message = None
226329 continue
227330
228331 opposite = approval_entry .rejected if approve else approval_entry .approved
@@ -232,6 +335,13 @@ def _apply_approval_decision(
232335 target = approval_entry .approved if approve else approval_entry .rejected
233336 if isinstance (target , list ) and call_id not in target :
234337 target .append (call_id )
338+ if approve :
339+ self ._clear_rejection_message (approval_entry , call_id )
340+ elif call_id is not None :
341+ if rejection_message is not None :
342+ approval_entry .rejection_messages [call_id ] = rejection_message
343+ else :
344+ self ._clear_rejection_message (approval_entry , call_id )
235345
236346 def approve_tool (self , approval_item : ToolApprovalItem , always_approve : bool = False ) -> None :
237347 """Approve a tool call, optionally for all future calls."""
@@ -241,12 +351,18 @@ def approve_tool(self, approval_item: ToolApprovalItem, always_approve: bool = F
241351 approve = True ,
242352 )
243353
244- def reject_tool (self , approval_item : ToolApprovalItem , always_reject : bool = False ) -> None :
354+ def reject_tool (
355+ self ,
356+ approval_item : ToolApprovalItem ,
357+ always_reject : bool = False ,
358+ rejection_message : str | None = None ,
359+ ) -> None :
245360 """Reject a tool call, optionally for all future calls."""
246361 self ._apply_approval_decision (
247362 approval_item ,
248363 always = always_reject ,
249364 approve = False ,
365+ rejection_message = rejection_message ,
250366 )
251367
252368 def get_approval_status (
@@ -326,6 +442,16 @@ def _rebuild_approvals(self, approvals: dict[str, dict[str, Any]]) -> None:
326442 record = _ApprovalRecord ()
327443 record .approved = record_dict .get ("approved" , [])
328444 record .rejected = record_dict .get ("rejected" , [])
445+ rejection_messages = record_dict .get ("rejection_messages" , {})
446+ if isinstance (rejection_messages , dict ):
447+ record .rejection_messages = {
448+ str (call_id ): message
449+ for call_id , message in rejection_messages .items ()
450+ if isinstance (message , str )
451+ }
452+ sticky_rejection_message = record_dict .get ("sticky_rejection_message" )
453+ if isinstance (sticky_rejection_message , str ):
454+ record .sticky_rejection_message = sticky_rejection_message
329455 self ._approvals [tool_name ] = record
330456
331457 def _fork_with_tool_input (self , tool_input : Any ) -> RunContextWrapper [TContext ]:
0 commit comments