
    PmjLa                       d Z ddlmZ ddlZddlZddlmZmZ ddlm	Z	m
Z
 ddlmZ ddlmZ  eh d          Z eh d	          Z ed
           G d d                      ZdZdZ ed
           G d d                      Z ed
           G d d                      Z ed
           G d d                      Zd3dZd4dZ G d d           Zd5d"Zd6d#Zd7d&Zd8d(Zd9d)Zd:d.Z d;d/Z!d;d0Z"d<d1Z#d=d2Z$dS )>a"  Pure tool-call loop guardrail primitives.

The controller in this module is intentionally side-effect free: it tracks
per-turn tool-call observations and returns decisions. Runtime code owns whether
those decisions become warning guidance, synthetic tool results, or controlled
turn halts.
    )annotationsN)	dataclassfield)AnyMapping)safe_json_loads)file_mutation_result_landed>   	read_file
web_searchweb_extractsearch_filessession_searchbrowser_consolebrowser_snapshotbrowser_get_imagesmcp_filesystem_read_filemcp_filesystem_search_filesmcp_filesystem_get_file_infomcp_filesystem_directory_treemcp_filesystem_list_directorymcp_filesystem_read_text_file"mcp_filesystem_read_multiple_files(mcp_filesystem_list_directory_with_sizes>   todopatchmemorycronjobprocessterminal
write_filebrowser_typeexecute_codesend_messageskill_managebrowser_clickbrowser_pressdelegate_taskbrowser_scrollbrowser_navigateT)frozenc                     e Zd ZU dZdZded<   dZded<   dZded	<   d
Zded<   dZ	ded<   dZ
ded<   dZded<   d
Zded<    ed           Zded<    ed           Zded<    ed           Zded<   ed!d            Zd S )"ToolCallGuardrailConfiga  Thresholds for per-turn tool-call loop detection.

    Warnings are enabled by default and never prevent tool execution. Hard stops
    are explicit opt-in so interactive CLI/TUI sessions get a gentle nudge unless
    the user enables circuit-breaker behavior in config.yaml.
    Tboolwarnings_enabledFhard_stop_enabled   intexact_failure_warn_after   exact_failure_block_after   same_tool_failure_warn_after   same_tool_failure_halt_afterno_progress_warn_afterno_progress_block_afterc                     t           S N)IDEMPOTENT_TOOL_NAMES     ;/home/thesage/.hermes/hermes-agent/agent/tool_guardrails.py<lambda>z ToolCallGuardrailConfig.<lambda>P   s    EZ r?   )default_factoryzfrozenset[str]idempotent_toolsc                     t           S r<   )MUTATING_TOOL_NAMESr>   r?   r@   rA   z ToolCallGuardrailConfig.<lambda>Q   s    CV r?   mutating_toolsc                     t                      S r<   )LoopCapConfigr>   r?   r@   rA   z ToolCallGuardrailConfig.<lambda>R   s
    } r?   'LoopCapConfig'	loop_capsdataMapping[str, Any] | Nonereturn'ToolCallGuardrailConfig'c                   t          |t                    s
 |             S |                    d          }t          |t                    si }|                    d          }t          |t                    si } |             } | t          |                    d          |j                  t          |                    d          |j                  t          |                    d|                    d                    |j                  t          |                    d|                    d                    |j                  t          |                    d	|                    d
                    |j	                  t          |                    d|                    d                    |j
                  t          |                    d|                    d                    |j                  t          |                    d	|                    d                    |j                  t                              |                    d                    	  	        S )zABuild config from the `tool_loop_guardrails` config.yaml section.
warn_afterhard_stop_afterr.   r/   exact_failurer2   same_tool_failurer6   idempotent_no_progressr9   r4   r8   r:   rJ   )	r.   r/   r2   r6   r9   r4   r8   r:   rJ   )
isinstancer   get_as_boolr.   r/   _positive_intr2   r6   r9   r4   r8   r:   rH   from_mapping)clsrK   rP   rQ   defaultss        r@   rY   z$ToolCallGuardrailConfig.from_mappingT   s)    $(( 	355LXXl++
*g.. 	J((#455/733 	! O355s%dhh/A&B&BHD]^^&txx0C'D'DhF`aa%29S0T0TUU1& & *72DHH=[4\4\]]5* * $17BZ9[9[\\/$ $ '4##OTXX>Y5Z5Z[[2' ' *7##$7B`9a9abb5* * %2##$<dhhG`>a>abb0% % $00+1F1FGG7
 
 
 	
r?   N)rK   rL   rM   rN   )__name__
__module____qualname____doc__r.   __annotations__r/   r2   r4   r6   r8   r9   r:   r   rC   rF   rJ   classmethodrY   r>   r?   r@   r,   r,   ?   s7          "!!!!#####$%%%%%%&&&&&() ))))() ))))"######$$$$$',u=Z=Z'['['[[[[[%*U;V;V%W%W%WNWWWW!&7N7N!O!O!OIOOOO)
 )
 )
 [)
 )
 )
r?   r,   2   c                  H    e Zd ZU dZeZded<   eZded<   e	dd	            Z
d
S )rH   a'  Per-turn caps on runaway-prone tool calls.

    Inspired by Claude Code v2.1.212 (Week 29, July 2026), which added caps on
    WebSearch calls and subagent spawns to stop runaway search / delegation
    loops. Here the caps count *within a single agent loop* (one turn): the
    counters reset in ``reset_for_turn`` at the start of every
    ``run_conversation``, so a legitimate multi-turn session is never starved,
    but a single turn that spirals into an unbounded search / delegation loop
    is stopped.

    Semantics differ from the per-turn loop *detector* above (which keys on
    repeated identical/failing calls): these caps are a hard ceiling on the
    total count of a tool within the turn and fire regardless of
    ``hard_stop_enabled``. A value of ``0`` disables the cap (unlimited).
    r1   max_web_searchesmax_subagentsrK   rL   rM   rI   c                   t          |t                    s
 |             S  |             } | t          |                    d          |j                  t          |                    d          |j                            S )zABuild config from the ``tool_loop_guardrails.loop_caps`` section.rd   re   )rd   re   )rU   r   _non_negative_intrV   rd   re   )rZ   rK   r[   s      r@   rY   zLoopCapConfig.from_mapping   s     $(( 	355L355s.+,,h.G  ,))8+A 	
 
 
 	
r?   N)rK   rL   rM   rI   )r\   r]   r^   r_   "_DEFAULT_MAX_WEB_SEARCHES_PER_TURNrd   r`   _DEFAULT_MAX_SUBAGENTS_PER_TURNre   ra   rY   r>   r?   r@   rH   rH      sc            ?>>>>8M8888
 
 
 [
 
 
r?   rH   c                  H    e Zd ZU dZded<   ded<   edd	            ZddZdS )ToolCallSignaturezDStable, non-reversible identity for a tool name plus canonical args.str	tool_name	args_hashargsrL   rM   'ToolCallSignature'c                X    t          |pi           } | |t          |                    S )Nrm   rn   )canonical_tool_args_sha256)rZ   rm   ro   	canonicals       r@   	from_callzToolCallSignature.from_call   s0    '
33	sY')2D2DEEEEr?   dict[str, str]c                     | j         | j        dS )z3Return public metadata without raw argument values.rr   rr   selfs    r@   to_metadatazToolCallSignature.to_metadata   s    !^$.IIIr?   N)rm   rl   ro   rL   rM   rp   )rM   rw   )r\   r]   r^   r_   r`   ra   rv   r{   r>   r?   r@   rk   rk      sj         NNNNNNNNF F F [FJ J J J J Jr?   rk   c                      e Zd ZU dZdZded<   dZded<   dZded<   dZded<   d	Z	d
ed<   dZ
ded<   edd            Zedd            ZddZdS )ToolGuardrailDecisionz8Decision returned by the tool-call guardrail controller.allowrl   actioncode messagerm   r   r1   countNzToolCallSignature | None	signaturerM   r-   c                    | j         dv S )N>   warnr~   r   ry   s    r@   allows_executionz&ToolGuardrailDecision.allows_execution       {///r?   c                    | j         dv S )N>   haltblockr   ry   s    r@   should_haltz!ToolGuardrailDecision.should_halt   r   r?   dict[str, Any]c                    | j         | j        | j        | j        | j        d}| j        | j                                        |d<   |S )N)r   r   r   rm   r   r   )r   r   r   rm   r   r   r{   )rz   rK   s     r@   r{   z!ToolGuardrailDecision.to_metadata   sN    kI|Z 
  
 >% $ : : < <Dr?   )rM   r-   )rM   r   )r\   r]   r^   r_   r   r`   r   r   rm   r   r   propertyr   r   r{   r>   r?   r@   r}   r}      s         BBFDGIENNNN*.I....0 0 0 X0 0 0 0 X0
 
 
 
 
 
r?   r}   ro   Mapping[str, Any]rM   rl   c                    t          | t                    s$t          dt          |           j                   t          j        | dddt                    S )z5Return sorted compact JSON for parsed tool arguments.z!tool args must be a mapping, got FT,:ensure_ascii	sort_keys
separatorsdefault)rU   r   	TypeErrortyper\   jsondumpsrl   ro   s    r@   rs   rs      s]    dG$$ SQDJJ<OQQRRR:   r?   rm   result
str | Nonetuple[bool, str]c                    |dS t          | |          rdS | dk    rKt          |          }t          |t                    r%|                    d          }||dk    rdd| dfS dS | d	k    rUt          |          }t          |t                    r1|                    d
          du rd|                    dd          v rdS |dd                                         }d|v sd|v s|                    d          rdS dS )a  Safety-fallback classifier used only when callers don't pass ``failed``.

    Mirrors ``agent.display._detect_tool_failure`` exactly so the guardrail
    never disagrees with the CLI's user-visible ``[error]`` tag. Production
    callers in ``run_agent.py`` always pass an explicit ``failed=`` derived
    from ``_detect_tool_failure``; this function exists so standalone callers
    (tests, tooling) still get consistent behavior.
    N)Fr   r   	exit_coder   Tz [exit ]r   successFzexceed the limiterrorr   )Tz [full]i  z"error"z"failed"Error)Tz [error])r	   r   rU   dictrV   lower
startswith)rm   r   rK   r   r   s        r@   classify_tool_failurer      s6    ~y"9f55 yJv&&dD!! 	4--I$a3y33333yHv&&dD!! 	'xx	""e++0BdhhwXZF[F[0[0[&4C4L  EEZ500F4E4Eg4N4N09r?   c                  b    e Zd ZdZdddZddZed d
            Zd!dZddd"dZ	d#dZ
d$dZdS )%ToolCallGuardrailControllerzCPer-turn controller for repeated failed/non-progressing tool calls.NconfigToolCallGuardrailConfig | Nonec                X    |pt                      | _        |                                  d S r<   )r,   r   reset_for_turn)rz   r   s     r@   __init__z$ToolCallGuardrailController.__init__  s-    9 7 9 9r?   rM   Nonec                Z    i | _         i | _        i | _        d | _        d| _        d| _        d S )Nr   )_exact_failure_counts_same_tool_failure_counts_no_progress_halt_decision_turn_web_search_count_turn_subagent_country   s    r@   r   z*ToolCallGuardrailController.reset_for_turn  s:    CE"9;&FH<@ '(#$%!!!r?   ToolGuardrailDecision | Nonec                    | j         S r<   )r   ry   s    r@   halt_decisionz)ToolCallGuardrailController.halt_decision#  s    ""r?   rm   rl   ro   rL   r}   c           	     t   t                               |t          |                    }|                     |t          |          |          }||S | j        j        st          ||          S | j                            |d          }|| j        j	        k    r%t          ddd| d| d|||          }|| _
        |S |                     |          rV| j                            |          }|:|\  }}	|	| j        j        k    r%t          dd	d| d
|	 d||	|          }|| _
        |S t          ||          S )Nrm   r   r   r   repeated_exact_failure_blockzBlocked z: the same tool call failed zd times with identical arguments. Stop retrying it unchanged; change strategy or explain the blocker.r   r   r   rm   r   r   idempotent_no_progress_blockz/: this read-only call returned the same result z^ times. Stop repeating it unchanged; use the result already provided or try a different query.)rk   rv   _coerce_args_check_loop_capr   r/   r}   r   rV   r4   r   _is_idempotentr   r:   )
rz   rm   ro   r   	cap_blockexact_countdecisionrecord_result_hashrepeat_counts
             r@   before_callz'ToolCallGuardrailController.before_call'  s   %//	<;M;MNN	 ((L4F4F	RR	 {, 	S(9	RRRR044YBB$+???,3>y > >k > > > $!#  H #+DOy)) 	$&**955F!-3*l4;#FFF4&;Xy X X&2X X X #,*"+     H +3D'#O$yINNNNr?   )failedr   r   r   bool | Nonec          	        t          |          }t                              ||          }|t          ||          \  }}|r4| j                            |d          dz   }|| j        |<   | j                            |d            | j                            |d          dz   }|| j        |<   | j	        j
        r5|| j	        j        k    r%t          ddd| d| d|||          }	|	| _        |	S | j	        j        r+|| j	        j        k    rt          d	d
| d| d|||          S | j	        j        r3|| j	        j        k    r#t          d	dt#          ||          |||          S t          |||          S | j                            |d            | j                            |d            |                     |          s,| j                            |d            t          ||          S t'          |          }
| j                            |          }d}||d         |
k    r|d         dz   }|
|f| j        |<   | j	        j        r+|| j	        j        k    rt          d	d| d| d|||          S t          |||          S )Nr      r   same_tool_failure_haltzStopped z: it failed z[ times this turn. Stop retrying the same failing tool path and choose a different approach.r   r   repeated_exact_failure_warning has failed z times with identical arguments. This looks like a loop; inspect the error and change strategy instead of retrying it unchanged.same_tool_failure_warning)rm   r   r   r   idempotent_no_progress_warningz returned the same result z^ times. Use the result already provided or change the query instead of repeating it unchanged.)r   rk   rv   r   r   rV   r   popr   r   r/   r8   r}   r   r.   r2   r6   _tool_failure_recovery_hintr   r   r9   )rz   rm   ro   r   r   r   _r   
same_countr   result_hashpreviousr   s                r@   
after_callz&ToolCallGuardrailController.after_call^  sA    D!!%//	4@@	>-i@@IFA /	f488AFFJK4?D&y1!!)T2227;;IqIIAMJ8BD*95{,  t{?g1g1g0!1d9 d d* d d d ($'
 
 
 '/#{+ t{?c0c0c,!9$ < <+ < < < (%'    {+ 
dk>f0f0f,!47	:NN'$'    )9K[deeee"&&y$777&**9d;;;""9-- 	S!!)T222(9	RRRR"6**$((33HQK;$>$>#A;?L(3\'B)$;' 	LDK<^,^,^(5  . .L . . . $"#    %yXabbbbr?   r-   c                >    || j         j        v rdS || j         j        v S )NF)r   rF   rC   )rz   rm   s     r@   r   z*ToolCallGuardrailController._is_idempotent  s'    2225DK888r?   r   r   rk   c           	        | j         j        }|dk    rM|j        }|r2| j        |k    r't	          ddd| d|| j        |          }|| _        |S | xj        dz  c_        dS |d	k    rf|j        }|sdS t          |          }| j        |k    r/t	          dd
d| j         d| d|| j        |          }|| _        |S | xj        |z  c_        dS dS )aN  Enforce and advance the per-turn runaway-loop counters.

        Returns a ``block`` decision when the cap is already reached, otherwise
        increments the relevant counter for the allowed call and returns
        ``None``. A cap of 0 disables that limit entirely. Counters reset each
        turn via ``reset_for_turn``.
        r   r   loop_web_search_capz/Blocked web_search: this turn has already made z web searches, the per-turn limit. This looks like a runaway search loop. Work with the results you already have and give the user your answer.r   r   Nr'   loop_subagent_capz5Blocked delegate_task: this turn has already spawned z subagents (limit zl). This looks like a runaway delegation loop. Finish the work with the results you have and answer the user.)	r   rJ   rd   r   r}   r   re   _subagent_spawn_countr   )rz   rm   ro   r   capscapr   spawn_counts           r@   r   z+ToolCallGuardrailController._check_loop_cap  sY    {$$$'C  t2c990".># > > >
 (5'   '/#''1,''4''$C t/55K(C//0",N4N NHKN N N
 (3'   '/#%%4%%4tr?   r<   )r   r   )rM   r   )rM   r   )rm   rl   ro   rL   rM   r}   )
rm   rl   ro   rL   r   r   r   r   rM   r}   )rm   rl   rM   r-   )rm   rl   ro   r   r   rk   rM   r   )r\   r]   r^   r_   r   r   r   r   r   r   r   r   r>   r?   r@   r   r     s        MM    	& 	& 	& 	& # # # X#5O 5O 5O 5Oz #Zc Zc Zc Zc Zc Zcx9 9 9 9
< < < < < <r?   r   r   c                b    t          j        | j        |                                 dd          S )zCBuild a synthetic role=tool content string for a blocked tool call.)r   	guardrailF)r   )r   r   r   r{   )r   s    r@   toolguard_synthetic_resultr     s?    :%!--//	
 	
    r?   c           	         |j         dvs|j        s| S |j         dk    rdnd}d| d|j         d|j         d|j         d		}| pd
|z   S )z;Append runtime guidance to the current tool result content.>   r   r   r   zTool loop hard stopzTool loop warningz

[z: z; count=z; r   r   )r   r   r   r   )r   r   labelsuffixs       r@   append_toolguard_guidancer   	  s    ...h6F.%-_%>%>!!DWE	H 	H 	H=	H 	H"*.	H 	H4<4D	H 	H 	H  LbF""r?   r   r1   c                2    |  d| d}| dk    r|dz   S |dz   S )zDAction-oriented guidance for recovering from repeated tool failures.r   z times this turn. This looks like a loop. Do not switch to text-only replies; keep using tools, but diagnose before retrying. First inspect the latest error/output and verify your assumptions. r   zFor terminal failures, run a small diagnostic such as `pwd && ls -la` in the same tool, then try an absolute path, a simpler command, a different working directory, or a different tool such as read_file/write_file/patch.zTry different arguments, a narrower query/path, an absolute path when relevant, or a different tool that can make progress. If the blocker is external, report the blocker after one diagnostic attempt instead of repeating the same failing path.r>   )rm   r   commons      r@   r   r     sa      	N 	N% 	N 	N 	N 
 JY
 	

 	_ r?   rL   c                4    t          | t                    r| ni S r<   )rU   r   r   s    r@   r   r   )  s    dG,,444"4r?   c                    t          | pd          }|?	 t          j        |dddt                    }n## t          $ r t          |          }Y nw xY w| pd}t          |          S )Nr   FTr   r   )r   r   r   rl   r   rt   )r   parsedru   s      r@   r   r   -  s    V\r**F		$
"%  II  	$ 	$ 	$FIII	$ Lb	9s   4 AAvaluer   r   r-   c                    | |S t          | t                    r| S t          | t          t          f          rt          |           S t          | t                    r2|                                                                 }|dv rdS |dv rdS |S )N>   1onyestrueenabledT>   0noofffalsedisabledF)rU   r-   r1   floatrl   stripr   )r   r   lowereds      r@   rW   rW   ?  s    }% %#u&& E{{% ++--%%'';;;4===5Nr?   c                r    | |S 	 t          |           }n# t          t          f$ r |cY S w xY w|dk    r|n|S )Nr   r1   r   
ValueErrorr   r   r   s      r@   rX   rX   O  sY    }Uz"   q[[66g-    ,,c                r    | |S 	 t          |           }n# t          t          f$ r |cY S w xY w|dk    r|n|S )ziParse a session-cap value. 0 is a valid (disable) value; negatives and
    junk fall back to the default.Nr   r  r  s      r@   rg   rg   Y  s[     }Uz"   q[[66g-r  c                    t          | t                    r|                     d          nd}t          |t                    r|rt	          |          S dS )aU  How many subagents a single delegate_task call spawns.

    delegate_task runs in one of two modes: a batch (``tasks`` is a non-empty
    list, one child per item) or a single task (``goal``). Count the batch size
    when present, otherwise 1, so the session subagent cap reflects real spawns
    rather than delegate_task invocations.
    tasksNr   )rU   r   rV   listlen)ro   r  s     r@   r   r   e  sS     ",D'!:!:DDHHWE% 5 5zz1r?   c                v    t          j        |                     dd                                                    S )Nzutf-8surrogatepass)hashlibsha256encode	hexdigest)r   s    r@   rt   rt   s  s.    
 >%,,w@@AAKKMMMr?   )ro   r   rM   rl   )rm   rl   r   r   rM   r   )r   r}   rM   rl   )r   rl   r   r}   rM   rl   )rm   rl   r   r1   rM   rl   )ro   rL   rM   r   )r   r   rM   rl   )r   r   r   r-   rM   r-   )r   r   r   r1   rM   r1   )ro   r   rM   r1   )r   rl   rM   rl   )%r_   
__future__r   r  r   dataclassesr   r   typingr   r   utilsr    agent.tool_result_classificationr	   	frozensetr=   rE   r,   rh   ri   rH   rk   r}   rs   r   r   r   r   r   r   r   rW   rX   rg   r   rt   r>   r?   r@   <module>r     s!    # " " " " "   ( ( ( ( ( ( ( (         ! ! ! ! ! ! H H H H H H "	    *  i    , $>
 >
 >
 >
 >
 >
 >
 >
N &( ""$  $!
 !
 !
 !
 !
 !
 !
 !
H $J J J J J J J J  $       >
 
 
 
       Fj j j j j j j jZ   	# 	# 	# 	#   (5 5 5 5   $    . . . .	. 	. 	. 	.   N N N N N Nr?   