
    iE                    B   S r SSKJr  SSKJrJrJrJrJr  SSK	J
r
JrJr  SSKJr  SSKJr  SSKJr  SSKJrJrJrJrJr  \(       a  SS	KJr  \S
   r  " S S\\   \\   5      rSS jr            SS jr " S S\5      r  " S S\\\   \4   \\\4   5      r!g)z&Tool call limit middleware for agents.    )annotations)TYPE_CHECKING	AnnotatedAnyGenericLiteral)	AIMessageToolCallToolMessage)UntrackedValue)ContextT)NotRequired)AgentMiddleware
AgentStatePrivateStateAttr	ResponseThook_config)Runtimecontinueerrorendc                  .    \ rS rSr% SrS\S'   S\S'   Srg)	ToolCallLimitState"   aC  State schema for ToolCallLimitMiddleware.

Extends AgentState with tool call tracking fields.

The count fields are dictionaries mapping tool names to execution counts.
This allows multiple middleware instances to track different tools independently.
The special key "__all__" is used for tracking all tool calls globally.
z8NotRequired[Annotated[dict[str, int], PrivateStateAttr]]thread_tool_call_countzHNotRequired[Annotated[dict[str, int], UntrackedValue, PrivateStateAttr]]run_tool_call_count N)__name__
__module____qualname____firstlineno____doc____annotations____static_attributes__r       e/home/james-whalen/.local/lib/python3.13/site-packages/langchain/agents/middleware/tool_call_limit.pyr   r   "   s     UTaar&   r   c                    U (       a  SU  S3$ g)ag  Build the error message content for ToolMessage when limit is exceeded.

This message is sent to the model, so it should not reference thread/run concepts
that the model has no notion of.

Args:
    tool_name: Tool name being limited (if specific tool), or None for all tools.

Returns:
    A concise message instructing the model not to call the tool again.
z'Tool call limit exceeded. Do not call 'z' again.z<Tool call limit exceeded. Do not make additional tool calls.r   	tool_names    r'   _build_tool_message_contentr+   0   s     88LLIr&   c                    U(       a  SU S3OSn/ nUb  X:  a  UR                  SU  SU S35        Ub  X:  a  UR                  SU SU S35        SR                  U5      nU S	U S
3$ )a  Build the final AI message content for 'end' behavior.

This message is displayed to the user, so it should include detailed information
about which limits were exceeded.

Args:
    thread_count: Current thread tool call count.
    run_count: Current run tool call count.
    thread_limit: Thread tool call limit (if set).
    run_limit: Run tool call limit (if set).
    tool_name: Tool name being limited (if specific tool), or None for all tools.

Returns:
    A formatted message describing which limits were exceeded.
'z' toolToolzthread limit exceeded (/z calls)zrun limit exceeded (z and z call limit reached: .)appendjoin)thread_count	run_countthread_limit	run_limitr*   	tool_descexceeded_limitslimits_texts           r'   _build_final_ai_message_contentr:   B   s    , *3!I;f%IOL$?!8a~U\]^!6!5i[)GTU,,/K[-k]!<<r&   c                  L   ^  \ rS rSrSr S           SU 4S jjjrSrU =r$ )ToolCallLimitExceededErrord   zException raised when tool call limits are exceeded.

This exception is raised when the configured exit behavior is 'error'
and either the thread or run tool call limit has been exceeded.
c                z   > Xl         X l        X0l        X@l        XPl        [        XX4U5      n[        TU ]  U5        g)aM  Initialize the exception with call count information.

Args:
    thread_count: Current thread tool call count.
    run_count: Current run tool call count.
    thread_limit: Thread tool call limit (if set).
    run_limit: Run tool call limit (if set).
    tool_name: Tool name being limited (if specific tool), or None for all tools.
N)r3   r4   r5   r6   r*   r:   super__init__)selfr3   r4   r5   r6   r*   msg	__class__s          r'   r@   #ToolCallLimitExceededError.__init__k   s@    " )"(""-\i
 	r&   )r4   r6   r3   r5   r*   )N)r3   intr4   rE   r5   
int | Noner6   rF   r*   
str | NonereturnNone)r   r    r!   r"   r#   r@   r%   __classcell__rC   s   @r'   r<   r<   d   sQ     !%  !	
   
 r&   r<   c                     ^  \ rS rSrSr\rSSSSS.         SU 4S jjjr\SS j5       r	SS jr
SS	 jr        SS
 jr\" S/S9      SS j5       rSrU =r$ )ToolCallLimitMiddleware   a\  Track tool call counts and enforces limits during agent execution.

This middleware monitors the number of tool calls made and can terminate or
restrict execution when limits are exceeded. It supports both thread-level
(persistent across runs) and run-level (per invocation) call counting.

Configuration:
    - `exit_behavior`: How to handle when limits are exceeded
      - `"continue"`: Block exceeded tools, let execution continue (default)
      - `"error"`: Raise an exception
      - `"end"`: Stop immediately with a ToolMessage + AI message for the single
        tool call that exceeded the limit (raises `NotImplementedError` if there
        are other pending tool calls (due to parallel tool calling).

Examples:
    Continue execution with blocked tools (default):
    ```python
    from langchain.agents.middleware.tool_call_limit import ToolCallLimitMiddleware
    from langchain.agents import create_agent

    # Block exceeded tools but let other tools and model continue
    limiter = ToolCallLimitMiddleware(
        thread_limit=20,
        run_limit=10,
        exit_behavior="continue",  # default
    )

    agent = create_agent("openai:gpt-4o", middleware=[limiter])
    ```

    Stop immediately when limit exceeded:
    ```python
    # End execution immediately with an AI message
    limiter = ToolCallLimitMiddleware(run_limit=5, exit_behavior="end")

    agent = create_agent("openai:gpt-4o", middleware=[limiter])
    ```

    Raise exception on limit:
    ```python
    # Strict limit with exception handling
    limiter = ToolCallLimitMiddleware(tool_name="search", thread_limit=5, exit_behavior="error")

    agent = create_agent("openai:gpt-4o", middleware=[limiter])

    try:
        result = await agent.invoke({"messages": [HumanMessage("Task")]})
    except ToolCallLimitExceededError as e:
        print(f"Search limit exceeded: {e}")
    ```

Nr   )r*   r5   r6   exit_behaviorc                  > [         TU ]  5         Uc  Uc  Sn[        U5      eSnXF;  a  SU< SU 3n[        U5      eUb  Ub  X2:  a  SU SU S3n[        U5      eXl        X l        X0l        X@l        g)	a*  Initialize the tool call limit middleware.

Args:
    tool_name: Name of the specific tool to limit. If `None`, limits apply
        to all tools. Defaults to `None`.
    thread_limit: Maximum number of tool calls allowed per thread.
        `None` means no limit. Defaults to `None`.
    run_limit: Maximum number of tool calls allowed per run.
        `None` means no limit. Defaults to `None`.
    exit_behavior: How to handle when limits are exceeded.
        - `"continue"`: Block exceeded tools with error messages, let other
          tools continue. Model decides when to end. (default)
        - `"error"`: Raise a `ToolCallLimitExceededError` exception
        - `"end"`: Stop execution immediately with a ToolMessage + AI message
          for the single tool call that exceeded the limit. Raises
          `NotImplementedError` if there are multiple parallel tool
          calls to other tools or multiple pending tool calls.

Raises:
    ValueError: If both limits are `None`, if exit_behavior is invalid,
        or if run_limit exceeds thread_limit.
Nz@At least one limit must be specified (thread_limit or run_limit)r   zInvalid exit_behavior: z. Must be one of zrun_limit (z) cannot exceed thread_limit (zB). The run limit should be less than or equal to the thread limit.)r?   r@   
ValueErrorr*   r5   r6   rO   )rA   r*   r5   r6   rO   rB   valid_behaviorsrC   s          r'   r@    ToolCallLimitMiddleware.__init__   s    < 	I$5TCS/!6/+M+<<MoM^_CS/!#	(=)BZi[(F|n UR R  S/!"("*r&   c                x    U R                   R                  nU R                  (       a  U SU R                   S3$ U$ )zThe name of the middleware instance.

Includes the tool name if specified to allow multiple instances
of this middleware with different tool names.
[])rC   r   r*   )rA   	base_names     r'   nameToolCallLimitMiddleware.name   s8     NN++	>>[$..!133r&   c                    U R                   SL=(       a    US-   U R                   :  =(       d'    U R                  SL=(       a    US-   U R                  :  $ )zCheck if incrementing the counts would exceed any configured limit.

Args:
    thread_count: Current thread call count.
    run_count: Current run call count.

Returns:
    True if either limit would be exceeded by one more call.
N   )r5   r6   )rA   r3   r4   s      r'   _would_exceed_limit+ToolCallLimitMiddleware._would_exceed_limit  sO     !!-V,2BTEVEV2V 
NN$&I9q=4>>+I	
r&   c                P    U R                   SL =(       d    US   U R                   :H  $ )zCheck if a tool call matches this middleware's tool filter.

Args:
    tool_call: The tool call to check.

Returns:
    True if this middleware should track this tool call.
NrX   r)   )rA   	tool_calls     r'   _matches_tool_filter,ToolCallLimitMiddleware._matches_tool_filter  s&     ~~%L6):dnn)LLr&   c                    / n/ nUnUnU H_  nU R                  U5      (       d  M  U R                  Xg5      (       a  UR                  U5        MD  UR                  U5        US-  nUS-  nMa     XEXg4$ )a%  Separate tool calls into allowed and blocked based on limits.

Args:
    tool_calls: List of tool calls to evaluate.
    thread_count: Current thread call count.
    run_count: Current run call count.

Returns:
    Tuple of (allowed_calls, blocked_calls, final_thread_count, final_run_count).
r[   )r`   r\   r1   )	rA   
tool_callsr3   r4   allowed_callsblocked_callstemp_thread_counttemp_run_countr_   s	            r'   _separate_tool_calls,ToolCallLimitMiddleware._separate_tool_calls  s     )+(*("#I,,Y77''(9JJ$$Y/$$Y/!Q&!!# $ ->NNr&   r   )can_jump_toc           
     v   UR                  S/ 5      nU(       d  gSn[        U5       H  n[        U[        5      (       d  M  Un  O   U(       a  UR                  (       d  gU R
                  (       a  U R
                  OSnUR                  S0 5      R                  5       nUR                  S0 5      R                  5       nUR                  US5      n	UR                  US5      n
U R                  UR                  X5      u  ppXU'   U[        U5      -   X'   U(       d  U(       a  UUS.$ gXv   nX   nU R                  S:X  a9  U[        U5      -   n[        UUU R                  U R                  U R
                  S	9e[        U R
                  5      nU Vs/ s H!  n[        UUS
   UR                  S5      SS9PM#     nnU R                  S:X  a  UR                   Vs/ s H)  nU R
                  c  M  US   U R
                  :w  d  M'  UPM+     nnU(       a6  SR                  U Vs1 s H  nUS   iM
     sn5      nSU S3n[!        U5      eU[        U5      -   n[#        UUU R                  U R                  U R
                  5      nUR%                  [        US95        UUSUS.$ UUUS.$ s  snf s  snf s  snf )aD  Increment tool call counts after a model call and check limits.

Args:
    state: The current agent state.
    runtime: The langgraph runtime.

Returns:
    State updates with incremented tool call counts. If limits are exceeded
    and exit_behavior is "end", also includes a jump to end with a ToolMessage
    and AI message for the single exceeded tool call.

Raises:
    ToolCallLimitExceededError: If limits are exceeded and exit_behavior
        is "error".
    NotImplementedError: If limits are exceeded, exit_behavior is "end",
        and there are multiple tool calls.
messagesN__all__r   r   r   )r   r   r   )r3   r4   r5   r6   r*   idrX   )contenttool_call_idrX   statusr   z, zDCannot end execution with other tool calls pending. Found calls to: z-. Use 'continue' or 'error' behavior instead.)ro   )r   r   jump_torl   )r   r   rl   )getreversed
isinstancer	   rc   r*   copyrh   lenrO   r<   r5   r6   r+   r   r2   NotImplementedErrorr:   r1   )rA   stateruntimerl   last_ai_messagemessage	count_keythread_counts
run_countscurrent_thread_countcurrent_run_countrd   re   new_thread_countnew_run_countfinal_thread_countfinal_run_counthypothetical_thread_counttool_msg_contentr_   artificial_messagestcother_tools
tool_namesrB   final_msg_contents                             r'   after_model#ToolCallLimitMiddleware.after_model<  s   0 99Z, )G'9--") *
 o&@&@ '+nnDNN)	 		":B?DDFYY4b9>>@
,00A>&NN9a8 IMHaHa&&(<I
E&6 $4i  -M0B B
 .;+5   +5$/ ((:S=O(O%,6)!......  7t~~F +>
 +	 (&t_]]6*	 + 	 >
 & *444B>> 24V*2N 4   !YY['I[r6
['IJ
''1l2_a  *#..
 );S=O(O% ?)!!!  &&y9J'KL +8'1 /	  '4#-+
 	
_>
 (Js   (J,J12J1J1%J6)rO   r6   r5   r*   )
r*   rG   r5   rF   r6   rF   rO   ExitBehaviorrH   rI   )rH   str)r3   rE   r4   rE   rH   bool)r_   r
   rH   r   )rc   zlist[ToolCall]r3   rE   r4   rE   rH   z/tuple[list[ToolCall], list[ToolCall], int, int])ry   zToolCallLimitState[ResponseT]rz   zRuntime[ContextT]rH   zdict[str, Any] | None)r   r    r!   r"   r#   r   state_schemar@   propertyrX   r\   r`   rh   r   r   r%   rJ   rK   s   @r'   rM   rM      s    3j &L
 !%#' $&03+ 3+ !	3+
 3+ $3+ 
3+ 3+j 	 	
	MO(O8;OHKO	8O> eW%I
,I
 #I
 
	I
 &I
r&   rM   N)r*   rG   rH   r   )r3   rE   r4   rE   r5   rF   r6   rF   r*   rG   rH   r   )"r#   
__future__r   typingr   r   r   r   r   langchain_core.messagesr	   r
   r   "langgraph.channels.untracked_valuer   langgraph.typingr   typing_extensionsr   !langchain.agents.middleware.typesr   r   r   r   r   langgraph.runtimer   r   r   r+   r:   	Exceptionr<   rM   r   r&   r'   <module>r      s    , " B B D D = % )  )12bI.	0B bJ$=== = 	=
 = 	=D! !H~
&y18;<Ix ~
r&   