+
    iHàj«'  ã                  óV  € R t ^ RIHt ^ RIt^ RIHt ^ RIHt ^ RIH	t	H
t
HtHt ]P                  ! ]4      t^t^t]! R4      tRtRtR	tR
tRt]! RR7       ! R R4      4       tR R ltR R ltR R ltR R ltR R ltR R ltR R ltRR/R R llt R  R! lt!R" R# lt"R$ R% lt#R# )&u	  Deterministic-empty detection and cost-aware retry budgets.

On an empty completion the loop retries up to 3 times, then walks the fallback chain â€”
each attempt re-bills the full input. Signaled refusals (``content_filter``, Anthropic
``refusal``, Bedrock guardrails) are already terminal; this handles *unsignaled* empties
(success, zero output tokens, generic finish reason â€” typical of portal-proxied refusals).

Two independent guards, both failing OPEN to legacy behaviour:

1. Deterministic-empty: two consecutive empties, both with usage present and
   ``output_tokens == 0``, from the same (model, provider, finish_reason) â†’ skip the
   remaining retries and go straight to the fallback chain. Missing usage or
   ``output_tokens > 0`` (think-block stripping, whitespace, flaky decoding) never classifies.
2. Cost-aware budget: when one empty attempt's estimated input cost exceeds the threshold
   (default $0.25), the retry budget drops from 3 to 1. Unknown pricing / missing usage /
   included routes leave it untouched.

1. **Deterministic-empty detection** â€” two consecutive empty attempts from
   the same (model, provider, finish_reason) are treated as deterministic
   when usage proves zero output, or when usage is absent and the assembled
   responses contain neither content nor reasoning. Remaining retries are
   skipped and the loop proceeds straight to the fallback chain (a different
   model may behave differently). Mixed evidence or any generated tokens keep
   the full retry budget.

2. **Cost-aware retry budget** â€” when the estimated input cost of a
   single empty attempt exceeds the configured threshold (default
   $0.25), the empty-retry budget for this streak drops from 3 to 1.
   Unknown pricing, missing usage, or included/subscription routes
   leave the budget untouched.

Configured via the additive ``agent.empty_response_guard`` section in
``config.yaml`` (resolved once at agent init by ``agent_init``)::

    agent:
      empty_response_guard:
        enabled: true            # false = legacy fixed 3-retry behaviour
        cost_threshold_usd: 0.25 # per-attempt cost that halves the budget

Per project policy, no ``HERMES_*`` environment variables are involved â€”
``.env`` is reserved for credentials; behavioural settings live in
``config.yaml``.
)ÚannotationsN)Ú	dataclass)ÚDecimal)ÚAnyÚListÚOptionalÚTuplez0.25TÚ_empty_attempt_historyÚ_empty_streak_cost_usdÚ_empty_guard_enabledÚ_empty_guard_cost_threshold_usd)Úfrozenc                  ón   € ] tR t^Dt$ RtR]R&   R]R&   R]R&   R]R&   R]R&   R]R	&   ]R
 R l4       tRtR# )ÚEmptyAttemptz8One observed empty completion within the current streak.ÚstrÚmodelÚproviderÚfinish_reasonÚboolÚusage_presentÚzero_outputÚobserved_generationc               ó   € V ^8„  d   QhRR/# )é   ÚreturnÚtuple© )Úformats   "Ú?/home/ubuntu/.hermes/hermes-agent/agent/empty_response_guard.pyÚ__annotate__ÚEmptyAttempt.__annotate__P   s   € ÷ ?ñ ?˜5ñ ?ó    c                	óH   € V P                   V P                  V P                  3# ©N)r   r   r   )Úselfs   &r   Ú	signatureÚEmptyAttempt.signatureO   s   € à—
‘
˜DŸM™M¨4×+=Ñ+=Ð>Ð>r!   r   N)	Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__Ú__annotations__Úpropertyr%   Ú__static_attributes__r   r!   r   r   r   D   s9   ‡ áBàƒJØƒMØÓØÓØÓØÓàô?ó ô?r!   r   c               ó    € V ^8„  d   QhRRRR/# )r   Úsectionr   r   ztuple[bool, Decimal]r   )r   s   "r   r   r   T   s   € ÷  ñ   Cð  Ð,@ñ  r!   c                ó  € \        V \        4      '       g   \        \        3# V P	                  R\        4      p\        V\
        4      '       d#   VP                  4       P                  4       R9  pM\        V\        4      '       g   \        p\        pV P	                  R4      pVe8   \        V\        4      '       g"    \        \        V4      4      pV^ 8”  d   TpW3# W3#   \         d    \        P                  RT4        Y3# i ; i)uf   Resolve ``agent.empty_response_guard`` into (enabled, threshold); malformed input â†’ schema defaults.ÚenabledÚcost_threshold_usdz9empty-guard: invalid cost_threshold_usd %r, using default)Ú0ÚfalseÚnoÚoff)Ú
isinstanceÚdictÚDEFAULT_GUARD_ENABLEDÚDEFAULT_COST_THRESHOLD_USDÚgetr   ÚstripÚlowerr   r   Ú	ExceptionÚloggerÚdebug)r0   r2   Ú	thresholdÚthreshold_rawÚ	candidates   &    r   Úresolve_guard_settingsrE   T   së   € ä�gœt×$Ò$Ü%Ô'AÐBÐBà�k‰k˜)Ô%:Ó;€GÜ�'œ3×ÒØ—-‘-“/×'Ñ'Ó)Ð1LÑL‰Ü˜¤×&Ò&Ü'ˆä*€IØ—K‘KÐ 4Ó5€MØÒ ¬°MÄ4×)HÒ)Hð	eÜ¤ MÓ 2Ó3ˆIØ˜1Œ}Ø%�	ð ÐÐˆGÐÐøô ô 	eÜ�L‰LÐTÐVcÕdØÐÐð	eús   Â?C" Ã"!DÄDc               ó    € V ^8„  d   QhRRRR/# ©r   Úagentr   r   r   r   )r   s   "r   r   r   k   s   € ÷ Gñ G˜ð G ñ Gr!   c                óh   € \        V \        \        4      p\        V\        4      '       d   V# \        # )zMConfig-resolved enabled flag; agents built without config default to enabled.)ÚgetattrÚ_ENABLED_ATTRr:   r8   r   ©rH   Úvalues   & r   Úguard_enabledrN   k   s)   € ä�Eœ=Ô*?Ó@€EÜ˜u¤d×+Ò+ˆ5ÐFÔ1FÐFr!   c               ó    € V ^8„  d   QhRRRR/# )r   rH   r   r   r   r   )r   s   "r   r   r   q   s   € ÷ ]ñ ]˜sð ] wñ ]r!   c                ón   € \        V \        R 4      p\        V\        4      '       d
   V^ 8”  d   V# \        # r#   )rJ   Ú_THRESHOLD_ATTRr8   r   r;   rL   s   & r   Ú_cost_threshold_usdrR   q   s.   € Ü�Eœ?¨DÓ1€EÜ˜u¤g×.Ò.°5¸1´9ˆ5Ð\ÔB\Ð\r!   c               ó    € V ^8„  d   QhRRRR/# )r   rH   r   r   zlist[EmptyAttempt]r   )r   s   "r   r   r   v   s   € ÷ ñ �Sð Ð/ñ r!   c                óV   € \        V \        R 4      pVf   . p\        V \        V4       V# r#   )rJ   Ú_ATTEMPTS_ATTRÚsetattr)rH   Úattemptss   & r   Ú	_attemptsrX   v   s,   € Ü�uœn¨dÓ3€HØÒØˆÜ�”~ xÔ0Ø€Or!   c               ó(   € V ^8„  d   QhRRRRRRRR/# )r   rH   r   ÚresponseÚwhatr   r   r   )r   s   "r   r   r   ~   s(   € ÷ ñ ˜Sð ¨Cð °sð ¸sñ r!   c           	     óÔ   € \        VRR4      pV'       g   R#  ^ RIHp V! V\        V RR4      \        V RR4      R7      #   \         d    \        P                  RTRR	7        R# i ; i)
zKCanonical usage for ``response`` or None (no usage / normalization failed).ÚusageN)Únormalize_usager   Úapi_mode)r   r_   zempty-guard: %s failedT©Úexc_info)rJ   Úagent.usage_pricingr^   r?   r@   rA   )rH   rZ   r[   Ú	raw_usager^   s   &&&  r   Ú_normalized_usagerd   ~   sk   € ä˜ '¨4Ó0€IßÙðÝ7Ù˜y´7¸5À*ÈdÓ3SÜ(/°°zÀ4Ó(HôJð 	Jøäô Ü�‰Ð-¨t¸dˆÔCÚðús   ™&A  Á #A'Á&A'c               ó$   € V ^8„  d   QhRRRRRR/# )r   rH   r   rZ   r   úOptional[Decimal]r   )r   s   "r   r   r   Œ   s"   € ÷ /ñ / #ð /°ð /Ð9Jñ /r!   c                ó&  € \        WR4      pVf   R#  ^ RIHp T! \        V RR4      ;'       g    RV\        V RR4      \        V RR4      \        V RR4      R	7      p\        TRR4      #   \         d    \
        P                  R
RR7        R# i ; i)z<Best-effort USD estimate for one attempt. None when unknown.zcost estimationN)Úestimate_usage_costr   Ú r   Úbase_urlÚapi_key)r   rj   rk   z#empty-guard: cost estimation failedTr`   Ú
amount_usd)rd   rb   rh   rJ   r?   r@   rA   )rH   rZ   Ú	canonicalrh   Úresults   &&   r   Ú_estimate_attempt_costro   Œ   s›   € ä! %Ð3DÓE€IØÒÙðÝ;Ù$Ü�E˜7 BÓ'×-Ð-¨2¨yÄ7È5ÐR\Ð^bÓCcÜ˜U J°Ó5¼wÀuÈiÐY]Ó?^ô
ˆô �6˜<¨Ó.Ð.øô ô Ü�‰Ð:ÀTˆÔJÚðús   ”A* °-A* Á*"BÂBc               ó$   € V ^8„  d   QhRRRRRR/# )r   rH   r   rZ   r   r   r   )r   s   "r   r   r   �   s!   € ÷ -ñ -˜ð - sð -¨uñ -r!   c                ó¸   € \        WR4      pVf   R# \        VRR4      pVe   \        VR^ 4      ^ 8:  d   R# \        VR^ 4      ;'       g    ^ pRW4,           ^ 8H  3# )zAReturn (usage_present, zero_output) for a response, failing open.zusage normalizationNÚoutput_tokensÚprompt_tokensÚreasoning_tokensT)FF)rd   rJ   )rH   rZ   rm   ÚoutputÚ	reasonings   &&   r   Ú_zero_outputrw   �   sr   € ä! %Ð3HÓI€IØÒØÐÜ�Y °Ó6€Fð ‚~œ ¨O¸QÓ?À1ÔDØÐô ˜	Ð#5°qÓ9×>Ð>¸Q€IØ�6Õ%¨!Ñ+Ð,Ð,r!   r   c          
     ó,   € V ^8„  d   QhRRRRRRRRRR	/# )
r   rH   r   r   r   rZ   r   r   r   ÚNoner   )r   s   "r   r   r   ­   s<   € ÷ 8ñ 8Øð8ð ð8ð ð	8ð
 ð8ð 
ñ8r!   c               óZ  € \        V 4      p\        V R^ 4      ^ 8X  d+   VP                  4        \        V \        \        ^ 4      4       \        W4      w  rVTP                  \        \        \        V RR4      ;'       g    R4      \        \        V RR4      ;'       g    R4      \        T;'       g    R4      VV\        V4      R7      4       \        W4      pVeP   V^ 8”  dG   \        V \        \        ^ 4      4      ;'       g    \        ^ 4      p\        V \        W‡,           4       R# R# R# )z¦Record one empty completion in the current streak.

Call BEFORE ``_empty_content_retries`` is incremented: a counter of 0 marks a new
streak and clears prior history.Ú_empty_content_retriesr   ri   r   )r   r   r   r   r   r   N)rX   rJ   ÚclearrV   Ú_STREAK_COST_ATTRr   rw   Úappendr   r   r   ro   )	rH   r   rZ   r   rW   r   r   ÚcostÚpriors	   &$$$     r   Úrecord_empty_attemptr�   ­   sû   € ô ˜Ó€HÜˆuÐ.°Ó2°aÔ7Ø�‰ÔÜ�Ô(¬'°!«*Ô5ä!-¨eÓ!>Ñ€MØ‡O�OÜÜ”g˜e W¨bÓ1×7Ð7°RÓ8Üœ ¨
°BÓ7×=Ð=¸2Ó>Ü˜m×1Ð1¨rÓ2Ø'Ø#Ü $Ð%8Ó 9ô	
ô	ô " %Ó2€DØÒ˜D 1œHÜ˜Ô0´'¸!³*Ó=×KÐKÄÈÃˆÜ�Ô(¨%­,Ö7ñ %Ñr!   c               ó    € V ^8„  d   QhRRRR/# rG   r   )r   s   "r   r   r   Ï   s   € ÷ Lñ L˜sð L tñ Lr!   c                ó  a€ \        V 4      '       g   R# \        V \        R4      ;'       g    . p\        V4      ^8  d   R# V^ ,          o\        ;QJ d    V3R lV 4       F  '       d   K   RM	  RM! V3R lV 4       4      p\        ;QJ d    R V 4       F  '       d   K   RM	  RM! R V 4       4      p\        ;QJ d    R V 4       F  '       d   K   RM	  RM! R V 4       4      pT;'       d    T;'       g    T# )aR  True when the current streak looks deterministic.

Requires >= 2 consecutive attempts with an identical (model, provider,
finish_reason) signature. Usage-backed attempts must all prove zero output.
Usage-absent attempts must all have no observed content or reasoning. Mixed
evidence fails open so ambiguous transients keep their retries.
FNc              3  óT   <"  € T F  qP                   SP                   8H  x € K  	  R # 5ir#   )r%   )Ú.0ÚaÚfirsts   & €r   Ú	<genexpr>Ú&deterministic_empty.<locals>.<genexpr>Ý   s   øé € ÐJÁ¸AŸ™¨¯©Ö7Ãùs   ƒ%(Tc              3  ó`   "  € T F$  qP                   ;'       d    VP                  x € K&  	  R # 5ir#   )r   r   ©r…   r†   s   & r   rˆ   r‰   Þ   s!   é € ÐQÉÀ1Ÿ_™_×>Ð>°·±Ô>Ëùs   ‚.™.c              3  ót   "  € T F.  qP                   '       * ;'       d    VP                  '       * x € K0  	  R # 5ir#   )r   r   r‹   s   & r   rˆ   r‰   ß   s,   é € ð  ÙCK¸a�O‰OÔ×9Ð9 A×$9Ñ$9Ô 9Ô9Ã8ùs   ‚8ž8)rN   rJ   rU   ÚlenÚall)rH   rW   Úsame_signatureÚusage_proves_emptyÚresponse_proves_emptyr‡   s   &    @r   Údeterministic_emptyr’   Ï   sÉ   ø€ ô ˜×ÒÙÜ�uœn¨dÓ3×9Ð9°r€HÜ
ˆ8ƒ}�qÔÙØ�Q�K€Eß“SÔJÁÓJ—S—S’SÔJÁÓJÓJ€Nß›ÑQÉÓQŸŸšÑQÉÓQÓQÐß›Cñ  ÙCKó ŸCŸCšCñ  ÙCKó ó Ðð ×KÐKÐ1×JÐJÐ5JÐKr!   c               ó$   € V ^8„  d   QhRRRRRR/# )r   rH   r   rZ   r   Úintr   )r   s   "r   r   r   å   s!   € ÷ &ñ &˜cð &¨Sð &°Sñ &r!   c                ó†   € \        V 4      '       g   \        # \        W4      pVe   V\        V 4      8¼  d   \        # \        # )z‚Empty-retry budget for the current streak (3, or 1 when a single attempt is
estimated to cost more than the configured threshold).)rN   ÚDEFAULT_EMPTY_RETRY_BUDGETro   rR   ÚREDUCED_EMPTY_RETRY_BUDGET)rH   rZ   r   s   && r   Úempty_retry_budgetr˜   å   s>   € ô ˜×ÒÜ)Ð)Ü! %Ó2€DØÒ˜DÔ$7¸Ó$>Ô>Ü)Ð)Ü%Ð%r!   c               ó    € V ^8„  d   QhRRRR/# )r   rH   r   r   rf   r   )r   s   "r   r   r   ð   s   € ÷ ;ñ ;˜3ð ;Ð#4ñ ;r!   c                óB   € \        V \        R4      pVe
   V^ 8”  d   V# R# )zAAccumulated estimated cost of the current empty streak, if known.N)rJ   r}   )rH   r   s   & r   Ústreak_cost_usdr›   ð   s'   € ä�5Ô+¨TÓ2€DØÒ#¨¨q¬ˆ4Ð:°dÐ:r!   )$r+   Ú
__future__r   ÚloggingÚdataclassesr   Údecimalr   Útypingr   r   r   r   Ú	getLoggerr'   r@   r–   r—   r;   r:   rU   r}   rK   rQ   r   rE   rN   rR   rX   rd   ro   rw   r�   r’   r˜   r›   r   r!   r   Ú<module>r¢      sÅ   ðñ*õX #ã Ý !Ý ß -Ó -à	×	Ò	˜8Ó	$€àÐ ØÐ Ù$ V›_Ð ØÐ ð
 *€Ø,Ð Ø&€Ø3€ñ �$Ô÷?ð ?ó ð?õ õ.Gõ]õ
õõ/õ"-ð 8ð
 !%÷8õDLõ,&÷;r!   