
    iA                       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  eh d          Z eh d          Z ed	
           G d d                      Z ed	
           G d d                      Z ed	
           G d d                      Zd+dZd,dZ G d d          Zd-dZd.dZd/d!Zd0d"Zd1d'Zd2d)Zd3d*ZdS )4a"  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>   	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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/ubuntu/.hermes/hermes-agent/agent/tool_guardrails.py<lambda>z ToolCallGuardrailConfig.<lambda>O   s    EZ r>   )default_factoryzfrozenset[str]idempotent_toolsc                     t           S r;   )MUTATING_TOOL_NAMESr=   r>   r?   r@   z ToolCallGuardrailConfig.<lambda>P   s    CV r>   mutating_toolsdataMapping[str, Any] | Nonereturn'ToolCallGuardrailConfig'c                z   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                            S )zABuild config from the `tool_loop_guardrails` config.yaml section.
warn_afterhard_stop_afterr-   r.   exact_failurer1   same_tool_failurer5   idempotent_no_progressr8   r3   r7   r9   )r-   r.   r1   r5   r8   r3   r7   r9   )
isinstancer   get_as_boolr-   r.   _positive_intr1   r5   r8   r3   r7   r9   )clsrF   rK   rL   defaultss        r?   from_mappingz$ToolCallGuardrailConfig.from_mappingR   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% %/
 
 
 	
r>   N)rF   rG   rH   rI   )__name__
__module____qualname____doc__r-   __annotations__r.   r1   r3   r5   r7   r8   r9   r   rB   rE   classmethodrV   r=   r>   r?   r+   r+   >   s          "!!!!#####$%%%%%%&&&&&() ))))() ))))"######$$$$$',u=Z=Z'['['[[[[[%*U;V;V%W%W%WNWWWW(
 (
 (
 [(
 (
 (
r>   r+   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argsrG   rH   'ToolCallSignature'c                X    t          |pi           } | |t          |                    S )Nr`   ra   )canonical_tool_args_sha256)rT   r`   rb   	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.re   re   selfs    r?   to_metadatazToolCallSignature.to_metadata   s    !^$.IIIr>   N)r`   r_   rb   rG   rH   rc   )rH   rj   )rW   rX   rY   rZ   r[   r\   ri   rn   r=   r>   r?   r^   r^   ~   sj         NNNNNNNNF F F [FJ J J J J Jr>   r^   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.allowr_   actioncode messager`   r   r0   countNzToolCallSignature | None	signaturerH   r,   c                    | j         dv S )N>   warnrq   rr   rl   s    r?   allows_executionz&ToolGuardrailDecision.allows_execution       {///r>   c                    | j         dv S )N>   haltblockrz   rl   s    r?   should_haltz!ToolGuardrailDecision.should_halt   r|   r>   dict[str, Any]c                    | j         | j        | j        | j        | j        d}| j        | j                                        |d<   |S )N)rr   rs   ru   r`   rv   rw   )rr   rs   ru   r`   rv   rw   rn   )rm   rF   s     r?   rn   z!ToolGuardrailDecision.to_metadata   sN    kI|Z 
  
 >% $ : : < <Dr>   )rH   r,   )rH   r   )rW   rX   rY   rZ   rr   r[   rs   ru   r`   rv   rw   propertyr{   r   rn   r=   r>   r?   rp   rp      s         BBFDGIENNNN*.I....0 0 0 X0 0 0 0 X0
 
 
 
 
 
r>   rp   rb   Mapping[str, Any]rH   r_   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)rP   r   	TypeErrortyperW   jsondumpsr_   rb   s    r?   rf   rf      s]    dG$$ SQDJJ<OQQRRR:   r>   r`   result
str | Nonetuple[bool, str]c                   |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)Frt   r   	exit_coder   Tz [exit ]r   successFzexceed the limiterrorrt   )Tz [full]i  z"error"z"failed"Error)Tz [error])r   rP   dictrQ   lower
startswith)r`   r   rF   r   r   s        r?   classify_tool_failurer      s    ~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                  Z    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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)rm   r   s     r?   __init__z$ToolCallGuardrailController.__init__   s-    9 7 9 9r>   rH   Nonec                >    i | _         i | _        i | _        d | _        d S r;   )_exact_failure_counts_same_tool_failure_counts_no_progress_halt_decisionrl   s    r?   r   z*ToolCallGuardrailController.reset_for_turn   s(    CE"9;&FH<@r>   ToolGuardrailDecision | Nonec                    | j         S r;   )r   rl   s    r?   halt_decisionz)ToolCallGuardrailController.halt_decision   s    ""r>   r`   r_   rb   rG   rp   c           	     $   t                               |t          |                    }| 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r`   rw   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.rr   rs   ru   r`   rv   rw   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.)r^   ri   _coerce_argsr   r.   rp   r   rQ   r3   r   _is_idempotentr   r9   )	rm   r`   rb   rw   exact_countdecisionrecord_result_hashrepeat_counts	            r?   before_callz'ToolCallGuardrailController.before_call   sz   %//	<;M;MNN	{, 	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          ||          \  }}|r,| 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        r+|| j	        j        k    rt          d	d| d| d|||          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   ry   repeated_exact_failure_warningz 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_warningzJ times this turn. This looks like a loop; change approach before retrying.)r`   rv   rw   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   r^   ri   r   r   rQ   r   popr   r   r.   r7   rp   r   r-   r1   r5   r   r   r8   )rm   r`   rb   r   r   rw   _r   
same_countr   result_hashpreviousr   s                r?   
after_callz&ToolCallGuardrailController.after_call  sY    D!!%//	4@@	>-i@@IFA 2	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$ S S* S S S ($'
 
 
 
 )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   rE   rB   )rm   r`   s     r?   r   z*ToolCallGuardrailController._is_idempotenty  s'    2225DK888r>   r;   )r   r   )rH   r   )rH   r   )r`   r_   rb   rG   rH   rp   )
r`   r_   rb   rG   r   r   r   r   rH   rp   )r`   r_   rH   r,   )rW   rX   rY   rZ   r   r   r   r   r   r   r   r=   r>   r?   r   r      s        MM    A A A A # # # X#*O *O *O *Od #]c ]c ]c ]c ]c ]c~9 9 9 9 9 9r>   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   ru   rn   )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~   ry   r~   zTool loop hard stopzTool loop warningz

[z: z; count=z; r   rt   )rr   ru   rs   rv   )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>   rG   c                4    t          | t                    r| ni S r;   )rP   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 )Nrt   FTr   r   )r   r   r   r_   r   rg   )r   parsedrh   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)rP   r,   r0   floatr_   stripr   )r   r   lowereds      r?   rR   rR     s    }% %#u&& E{{% ++--%%'';;;4===5Nr>   r0   c                r    | |S 	 t          |           }n# t          t          f$ r |cY S w xY w|dk    r|n|S )Nr   )r0   r   
ValueError)r   r   r   s      r?   rS   rS     sY    }Uz"   q[[66g-s    ,,c                t    t          j        |                     d                                                    S )Nzutf-8)hashlibsha256encode	hexdigest)r   s    r?   rg   rg     s*    >%,,w//00::<<<r>   )rb   r   rH   r_   )r`   r_   r   r   rH   r   )r   rp   rH   r_   )r   r_   r   rp   rH   r_   )rb   rG   rH   r   )r   r   rH   r_   )r   r   r   r,   rH   r,   )r   r   r   r0   rH   r0   )r   r_   rH   r_   )rZ   
__future__r   r   r   dataclassesr   r   typingr   r   utilsr   	frozensetr<   rD   r+   r^   rp   rf   r   r   r   r   r   r   rR   rS   rg   r=   r>   r?   <module>r      s|    # " " " " "   ( ( ( ( ( ( ( (         ! ! ! ! ! ! "	    *  i    , $<
 <
 <
 <
 <
 <
 <
 <
~ $J J J J J J J J  $       >
 
 
 
   B_9 _9 _9 _9 _9 _9 _9 _9D   	# 	# 	# 	#5 5 5 5   $    . . . .= = = = = =r>   