§
    ÷žyjQ‘  ã                  óÖ  — U d Z ddlmZ ddlZddlZddlZddlZddlmZ  ej	        e
¦  «        Z ej        d¦  «        ZdId	„ZdJd„ZdKd„ZdLd„ZdZdMdNd„ZdOdPd„ZdId„ZdKd„ZdQd„ZdKd„ZdJd„Zg d ¢ZdRdSd%„ZdTd'„ZdUd)„Zd* ed+d,h¦  «         e¦   «         d-d.fd/ e¦   «          ed/h¦  «        d0d1fd2 e¦   «          ed3h¦  «        d4d5ffZd6e d7<   dVd9„Z!dWd=„Z"dXd?„Z#dYd@„Z$dZdF„Z%d[dH„Z&dS )\at  Message and tool-payload sanitization helpers.

Pure functions extracted from ``run_agent.py`` so the AIAgent module can
stay focused on the conversation loop.  These walk OpenAI-format message
lists and structured payloads, repairing or stripping problematic
characters that would otherwise crash ``json.dumps`` inside the OpenAI
SDK or be rejected by upstream APIs.

All helpers are stateless and side-effect-free except for in-place
mutation of their input (where documented).  Backward-compatible
re-exports from ``run_agent`` remain in place so existing imports
``from run_agent import _sanitize_surrogates`` keep working.
é    )ÚannotationsN)ÚAnyz[\ud800-\udfff]ÚtextÚstrÚreturnc                óp   — t                                | ¦  «        rt                                d| ¦  «        S | S )zèReplace lone surrogate code points with U+FFFD (replacement character).

    Surrogates are invalid in UTF-8 and will crash ``json.dumps()`` inside the
    OpenAI SDK.  This is a fast no-op when the text contains no surrogates.
    õ   ï¿½)Ú_SURROGATE_REÚsearchÚsub©r   s    ú@/home/ragecks/.hermes/hermes-agent/agent/message_sanitization.pyÚ_sanitize_surrogatesr       s5   € õ ×Ò˜DÑ!Ô!ð 1Ý× Ò  ¨4Ñ0Ô0Ð0Ø€Kó    Úpayloadr   Úboolc                ó0   ‡‡— dŠˆˆfd„Š ‰| ¦  «         ‰S )uu  Replace surrogate code points in nested dict/list payloads in-place.

    Mirror of ``_sanitize_structure_non_ascii`` but for surrogate recovery.
    Used to scrub nested structured fields (e.g. ``reasoning_details`` â€” an
    array of dicts with ``summary``/``text`` strings) that flat per-field
    checks don't reach.  Returns True if any surrogates were replaced.
    Fc                óž  •— t          | t          ¦  «        r“|                      ¦   «         D ]|\  }}t          |t          ¦  «        r;t                               |¦  «        r t                               d|¦  «        | |<   dŠŒUt          |t          t          f¦  «        r ‰|¦  «         Œ}d S t          | t          ¦  «        rŒt          | ¦  «        D ]~\  }}t          |t          ¦  «        r;t                               |¦  «        r t                               d|¦  «        | |<   dŠŒUt          |t          t          f¦  «        r ‰|¦  «         Œ}d S d S )Nr	   T)	Ú
isinstanceÚdictÚitemsr   r
   r   r   ÚlistÚ	enumerate)ÚnodeÚkeyÚvalueÚidxÚ_walkÚfounds       €€r   r   z-_sanitize_structure_surrogates.<locals>._walk5   sQ  ø€ å�d�DÑ!Ô!ð 	!Ø"Ÿjšj™lœlð !ð !‘
��UÝ˜e¥SÑ)Ô)ð !Ý$×+Ò+¨EÑ2Ô2ð %Ý$1×$5Ò$5°hÀÑ$FÔ$F˜˜S™	Ø $˜øÝ ­­d |Ñ4Ô4ð !Ø�E˜%‘L”L�Løð!ð !õ ˜�dÑ#Ô#ð 	!Ý'¨™oœoð !ð !‘
��UÝ˜e¥SÑ)Ô)ð !Ý$×+Ò+¨EÑ2Ô2ð %Ý$1×$5Ò$5°hÀÑ$FÔ$F˜˜S™	Ø $˜øÝ ­­d |Ñ4Ô4ð !Ø�E˜%‘L”L�Løð	!ð 	!ð!ð !r   © ©r   r   r   s    @@r   Ú_sanitize_structure_surrogatesr"   +   s=   øø€ ð €Eð!ð !ð !ð !ð !ð !ð& 
€Eˆ'�N„N€NØ€Lr   Úmessagesr   c                óp  — d}| D �]¯}t          |t          ¦  «        sŒ|                     d¦  «        }t          |t          ¦  «        r;t                               |¦  «        r!t                               d|¦  «        |d<   d}n“t          |t          ¦  «        r~|D ]{}t          |t          ¦  «        rd|                     d¦  «        }t          |t          ¦  «        r:t                               |¦  «        r t                               d|¦  «        |d<   d}Œ||                     d¦  «        }t          |t          ¦  «        r:t                               |¦  «        r t                               d|¦  «        |d<   d}|                     d¦  «        }t          |t          ¦  «        �rs|D �]o}t          |t          ¦  «        sŒ|                     d¦  «        }	t          |	t          ¦  «        r:t                               |	¦  «        r t                               d|	¦  «        |d<   d}|                     d	¦  «        }
t          |
t          ¦  «        rÈ|
                     d¦  «        }t          |t          ¦  «        r:t                               |¦  «        r t                               d|¦  «        |
d<   d}|
                     d
¦  «        }t          |t          ¦  «        r:t                               |¦  «        r t                               d|¦  «        |
d
<   d}�Œq|                     ¦   «         D ]‡\  }}|dv rŒ
t          |t          ¦  «        r;t                               |¦  «        r t                               d|¦  «        ||<   d}ŒZt          |t          t          f¦  «        rt          |¦  «        rd}Œˆ�Œ±|S )a’  Sanitize surrogate characters from all string content in a messages list.

    Walks message dicts in-place. Returns True if any surrogates were found
    and replaced, False otherwise. Covers content/text, name, tool call
    metadata/arguments, AND any additional string or nested structured fields
    (``reasoning``, ``reasoning_content``, ``reasoning_details``, etc.) so
    retries don't fail on a non-content field.  Byte-level reasoning models
    (xiaomi/mimo, kimi, glm) can emit lone surrogates in reasoning output
    that flow through to ``api_messages["reasoning_content"]`` on the next
    turn and crash json.dumps inside the OpenAI SDK.
    FÚcontentr	   Tr   ÚnameÚ
tool_callsÚidÚfunctionÚ	arguments>   r&   Úroler%   r'   )
r   r   Úgetr   r
   r   r   r   r   r"   )r#   r   Úmsgr%   Úpartr   r&   r'   ÚtcÚtc_idÚfnÚfn_nameÚfn_argsr   r   s                  r   Ú_sanitize_messages_surrogatesr4   L   sw  € ð €EØð 3!ñ 3!ˆÝ˜#�tÑ$Ô$ð 	ØØ—'’'˜)Ñ$Ô$ˆÝ�g�sÑ#Ô#ð 		%­×(<Ò(<¸WÑ(EÔ(Eð 		%Ý*×.Ò.¨x¸ÑAÔAˆC�	‰NØˆEˆEÝ˜¥Ñ&Ô&ð 	%Øð %ð %�Ý˜d¥DÑ)Ô)ð %ØŸ8š8 FÑ+Ô+�DÝ! $­Ñ,Ô,ð %µ×1EÒ1EÀdÑ1KÔ1Kð %Ý'4×'8Ò'8¸À4Ñ'HÔ'H˜˜V™Ø $˜øØ�wŠw�v‰ŒˆÝ�d�CÑ Ô ð 	¥]×%9Ò%9¸$Ñ%?Ô%?ð 	Ý'×+Ò+¨H°dÑ;Ô;ˆC�‰KØˆEØ—W’W˜\Ñ*Ô*ˆ
Ý�j¥$Ñ'Ô'ñ 	%Ø ð %ñ %�Ý! "¥dÑ+Ô+ð ØØŸš˜t™œ�Ý˜e¥SÑ)Ô)ð !­m×.BÒ.BÀ5Ñ.IÔ.Ið !Ý,×0Ò0°¸5ÑAÔA�B�t‘HØ �EØ—V’V˜JÑ'Ô'�Ý˜b¥$Ñ'Ô'ð %Ø Ÿfšf V™nœn�GÝ! '­3Ñ/Ô/ð %µM×4HÒ4HÈÑ4QÔ4Qð %Ý%2×%6Ò%6°xÀÑ%IÔ%I˜˜6™
Ø $˜Ø Ÿfšf [Ñ1Ô1�GÝ! '­3Ñ/Ô/ð %µM×4HÒ4HÈÑ4QÔ4Qð %Ý*7×*;Ò*;¸HÀgÑ*NÔ*N˜˜;™Ø $˜ùð Ÿ)š)™+œ+ð 		!ð 		!‰JˆC�ØÐ?Ð?Ð?ØÝ˜%¥Ñ%Ô%ð !Ý ×'Ò'¨Ñ.Ô.ð !Ý,×0Ò0°¸5ÑAÔA�C˜‘HØ �EøÝ˜E¥D­$ <Ñ0Ô0ð !Ý1°%Ñ8Ô8ð !Ø �Eøñ		!ð €Lr   Úrawc                ó2  — g }d}d}t          | ¦  «        }||k     rè| |         }|r¶|dk    rB|dz   |k     r9|                     |¦  «         |                     | |dz            ¦  «         |dz  }ŒX|dk    rd}|                     |¦  «         nmt          |¦  «        dk     r'|                     dt          |¦  «        d	›�¦  «         n3|                     |¦  «         n|dk    rd
}|                     |¦  «         |dz  }||k     °èd                     |¦  «        S )ug  Escape unescaped control chars inside JSON string values.

    Walks the raw JSON character-by-character, tracking whether we are
    inside a double-quoted string. Inside strings, replaces literal
    control characters (0x00-0x1F) that aren't already part of an escape
    sequence with their ``\uXXXX`` equivalents. Pass-through for everything
    else.

    Ported from #12093 â€” complements the other repair passes in
    ``_repair_tool_call_arguments`` when ``json.loads(strict=False)`` is
    not enough (e.g. llama.cpp backends that emit literal apostrophes or
    tabs alongside other malformations).
    Fr   ú\é   é   ú"é    z\uÚ04xTÚ )ÚlenÚappendÚordÚjoin)r5   ÚoutÚ	in_stringÚiÚnÚchs         r   Ú%_escape_invalid_chars_in_json_stringsrG   �   s+  € ð €CØ€IØ	€AÝˆC‰Œ€AØ
ˆaŠ%ˆ%Ø�ŒVˆØð 	Ø�TŠzˆz˜a !™e aši˜ià—
’
˜2‘”�Ø—
’
˜3˜q 1™uœ:Ñ&Ô&Ð&Ø�Q‘�ØØ�SŠyˆyØ!�	Ø—
’
˜2‘”��Ý�R‘”˜4’�Ø—
’
Ð.¥ R¡¤Ð.Ð.Ð.Ñ/Ô/Ð/Ð/à—
’
˜2‘”��à�SŠyˆyØ �	Ø�JŠJ�r‰NŒNˆNØ	ˆQ‰ˆð) ˆaŠ%ˆ%ð* �7Š7�3‰<Œ<Ðr   i † ú?Úraw_argsÚ	tool_namec                ó„  — t          | t          ¦  «        r|                      ¦   «         nd}|st                               d|¦  «         dS |dk    rt                               d|¦  «         dS 	 t          j        |d¬¦  «        }t          j        |d¬	¦  «        }||k    rt                               d
|¦  «         |S # t
          j        t          t          f$ r Y nw xY w|}t          j        dd|¦  «        }|                     d¦  «        |                     d¦  «        z
  }|                     d¦  «        |                     d¦  «        z
  }|dk    r|d|z  z  }|dk    r|d|z  z  }t          d¦  «        D ]È}	 t          j        |¦  «          n°# t
          j        $ rž |                     d¦  «        r7|                     d¦  «        |                     d¦  «        k    r|dd…         }nO|                     d¦  «        r7|                     d¦  «        |                     d¦  «        k    r|dd…         }nY  nY ŒÅw xY w	 t          j        |¦  «         t                               d||dd…         |dd…         ¦  «         |S # t
          j        $ r Y nw xY w	 t!          |¦  «        }	|	|k    rCt          j        |	¦  «         t                               d||dd…         |	dd…         ¦  «         |	S n"# t
          j        t          t          f$ r Y nw xY wt                               d||dt"          …         ¦  «         dS )a§  Attempt to repair malformed tool_call argument JSON.

    Models like GLM-5.1 via Ollama can produce truncated JSON, trailing
    commas, Python ``None``, etc.  The API proxy rejects these with HTTP 400
    "invalid tool call arguments".  This function applies common repairs;
    if all fail it returns ``"{}"`` so the request succeeds (better than
    crashing the session).  All repairs are logged at WARNING level.
    r=   z*Sanitized empty tool_call arguments for %sz{}ÚNonez0Sanitized Python-None tool_call arguments for %sF)Ústrict)Ú,ú:)Ú
separatorsz>Repaired unescaped control chars in tool_call arguments for %sz,\s*([}\]])z\1Ú{Ú}Ú[Ú]r   é2   Néÿÿÿÿu8   Repaired malformed tool_call arguments for %s: %s â†’ %séP   uA   Repaired control-char-laced tool_call arguments for %s: %s â†’ %suP   Unrepairable tool_call arguments for %s â€” replaced with empty object (was: %s))r   r   ÚstripÚloggerÚwarningÚjsonÚloadsÚdumpsÚJSONDecodeErrorÚ	TypeErrorÚ
ValueErrorÚrer   ÚcountÚrangeÚendswithrG   Ú_FULL_ARGS_LOG_BOUND)
rI   rJ   Úraw_strippedÚparsedÚreserialisedÚfixedÚ
open_curlyÚopen_bracketÚ_Úescapeds
             r   Ú_repair_tool_call_argumentsrn   Ã   s£  € õ (2°(½CÑ'@Ô'@ÐH�8—>’>Ñ#Ô#Ð#Àb€Lð ð Ý�ŠÐCÀYÑOÔOÐOØˆtð �vÒÐÝ�ŠÐIÈ9ÑUÔUÐUØˆtð
Ý”˜L°Ð7Ñ7Ô7ˆÝ”z &°ZÐ@Ñ@Ô@ˆØ˜<Ò'Ð'Ý�NŠNØPØñô ð ð ÐøÝÔ ¥)­ZÐ8ð ð ð Øˆðøøøð €EåŒF�> 5¨%Ñ0Ô0€Eà—’˜SÑ!Ô! E§K¢K°Ñ$4Ô$4Ñ4€JØ—;’;˜sÑ#Ô# e§k¢k°#Ñ&6Ô&6Ñ6€LØ�A‚~€~Ø��zÑ!Ñ!ˆØ�aÒÐØ��|Ñ#Ñ#ˆå�2‰YŒYð 
ð 
ˆð		ÝŒJ�uÑÔÐØˆEøÝÔ#ð 	ð 	ð 	Ø�~Š~˜cÑ"Ô"ð  u§{¢{°3Ñ'7Ô'7¸%¿+º+ÀcÑ:JÔ:JÒ'JÐ'JØ˜c˜r˜cœ
��Ø—’ Ñ$Ô$ð ¨¯ª°SÑ)9Ô)9¸E¿KºKÈÑ<LÔ<LÒ)LÐ)LØ˜c˜r˜cœ
��à��øøð	øøøðÝŒ
�5ÑÔÐÝ�ŠØFØ�| C R CÔ(¨%°°°¬*ñ	
ô 	
ð 	
ð ˆøÝÔð ð ð Øˆðøøøð
Ý7¸Ñ>Ô>ˆØ�eÒÐÝŒJ�wÑÔÐÝ�NŠNØSØ˜<¨¨¨Ô,¨g°c°r°c¬lñô ð ð ˆNð øõ Ô ¥)­ZÐ8ð ð ð Øˆðøøøõ ‡N‚Nð	/à�<Ð 5Õ!5Ð 5Ô6ñô ð ð
 ˆ4sL   Á/AB> Â>CÃCÅ<FÆB'H?È>H?ÉAJ ÊJÊJÊAK5 Ë5LÌLÚfinal_responsec                ó  — | sdS | d         }t          |t          ¦  «        r|                     d¦  «        dk    rdS t          |t          ¦  «        r|nd}|                      d|                     ¦   «         pddœ¦  «         d	S )
u"  Append a synthetic assistant turn when an interrupted tail is a tool result.

    A turn cut short by ``/stop`` can leave the transcript ending on a raw
    ``tool`` message (a tool finished, or its execution was cancelled, but the
    model never streamed a closing assistant turn). Persisting that tail means
    the next user message lands as ``â€¦ tool â†’ user`` â€” a role-alternation
    violation that strict providers (Gemini, Claude) react to by hallucinating
    a continuation of the user's message and ignoring prior context, which
    reads to the user as "lost context" (#48879).

    ``finalize_turn`` closes this on the happy interrupt path, but the
    retry/backoff/error interrupt aborts in ``conversation_loop`` ``return``
    early and never reach it â€” this shared helper closes the sequence on all of
    them. ``final_response`` is usually empty on an interrupt, so an explicit
    placeholder is used rather than an empty-content assistant turn.

    Mutates ``messages`` in place. Returns True if a closing turn was appended.
    FrV   r+   Útoolr=   Ú	assistantzOperation interrupted.)r+   r%   T)r   r   r,   r   r?   rX   )r#   ro   Úlastr   s       r   Úclose_interrupted_tool_sequencert   (  sš   € ð& ð ØˆuØ�BŒ<€DÝ�d�DÑ!Ô!ð  T§X¢X¨fÑ%5Ô%5¸Ò%?Ð%?ØˆuÝ'¨½Ñ<Ô<ÐDˆ>ˆ>À"€DØ‡O‚OØØ—:’:‘<”<Ð;Ð#;ðð ñ ô ð ð ˆ4r   c                óV   — |                       dd¬¦  «                             d¦  «        S )zßRemove non-ASCII characters, replacing with closest ASCII equivalent or removing.

    Used as a last resort when the system encoding is ASCII and can't handle
    any non-ASCII characters (e.g. LANG=C on Chromebooks).
    ÚasciiÚignore©Úerrors)ÚencodeÚdecoder   s    r   Ú_strip_non_asciir|   H  s(   € ð �;Š;�w xˆ;Ñ0Ô0×7Ò7¸Ñ@Ô@Ð@r   c                óR  — d}| D �] }t          |t          ¦  «        sŒ|                     d¦  «        }t          |t          ¦  «        rt	          |¦  «        }||k    r||d<   d}nut          |t
          ¦  «        r`|D ]]}t          |t          ¦  «        rF|                     d¦  «        }t          |t          ¦  «        rt	          |¦  «        }||k    r||d<   d}Œ^|                     d¦  «        }t          |t          ¦  «        rt	          |¦  «        }||k    r||d<   d}|                     d¦  «        }t          |t
          ¦  «        r‹|D ]ˆ}	t          |	t          ¦  «        rq|	                     di ¦  «        }
t          |
t          ¦  «        rF|
                     d¦  «        }t          |t          ¦  «        rt	          |¦  «        }||k    r||
d<   d}Œ‰|                     ¦   «         D ];\  }}|d	v rŒ
t          |t          ¦  «        rt	          |¦  «        }||k    r|||<   d}Œ<�Œ"|S )
a  Strip non-ASCII characters from all string content in a messages list.

    This is a last-resort recovery for systems with ASCII-only encoding
    (LANG=C, Chromebooks, minimal containers).  Returns True if any
    non-ASCII content was found and sanitized.
    Fr%   Tr   r&   r'   r)   r*   >   r&   r+   r%   r'   )r   r   r,   r   r|   r   r   )r#   r   r-   r%   Ú	sanitizedr.   r   r&   r'   r/   r1   r3   r   r   s                 r   Ú_sanitize_messages_non_asciir   Q  s]  € ð €EØð /!ñ /!ˆÝ˜#�tÑ$Ô$ð 	Øà—'’'˜)Ñ$Ô$ˆÝ�g�sÑ#Ô#ð 	)Ý(¨Ñ1Ô1ˆIØ˜GÒ#Ð#Ø!*��I‘Ø�øÝ˜¥Ñ&Ô&ð 	)Øð )ð )�Ý˜d¥DÑ)Ô)ð )ØŸ8š8 FÑ+Ô+�DÝ! $­Ñ,Ô,ð )Ý$4°TÑ$:Ô$:˜	Ø$¨Ò,Ð,Ø+4˜D ™LØ$(˜Eøà�wŠw�v‰ŒˆÝ�d�CÑ Ô ð 	Ý(¨Ñ.Ô.ˆIØ˜DÒ Ð Ø'��F‘Ø�à—W’W˜\Ñ*Ô*ˆ
Ý�j¥$Ñ'Ô'ð 
	-Ø ð 	-ð 	-�Ý˜b¥$Ñ'Ô'ð -ØŸš 
¨BÑ/Ô/�BÝ! "¥dÑ+Ô+ð -Ø"$§&¢&¨Ñ"5Ô"5˜Ý% g­sÑ3Ô3ð -Ý(8¸Ñ(AÔ(A˜IØ(¨GÒ3Ð3Ø2;  ;¡Ø(, øàŸ)š)™+œ+ð 	!ð 	!‰JˆC�ØÐ?Ð?Ð?ØÝ˜%¥Ñ%Ô%ð !Ý,¨UÑ3Ô3�	Ø Ò%Ð%Ø(�C˜‘HØ �Eøñ	!ð €Lr   Útoolsc                ó    — t          | ¦  «        S )z7Strip non-ASCII characters from tool payloads in-place.)Ú_sanitize_structure_non_ascii)r€   s    r   Ú_sanitize_tools_non_asciirƒ   Œ  s   € å(¨Ñ/Ô/Ð/r   c                ó2  — d}g }t          | ¦  «        D ]í\  }}t          |t          ¦  «        sŒ|                     d¦  «        }t          |t          ¦  «        sŒFg }|D ]F}t          |t          ¦  «        r|                     d¦  «        dv rd}Œ1|                     |¦  «         ŒGt          |¦  «        t          |¦  «        k     r<|r||d<   Œ¹|                     d¦  «        dk    rd|d<   ŒØ|                     |¦  «         Œît          |¦  «        D ]}| |= Œ|S )	uB  Remove image_url content parts from all messages in-place.

    Called when a server signals it does not support images (e.g.
    "Only 'text' content type is supported.").  Mutates messages so the
    next API call sends text only.

    Preserves message alternation invariants:
      * ``tool``-role messages whose content was entirely images are replaced
        with a plaintext placeholder, NOT deleted â€” deleting them would leave
        the paired ``tool_call_id`` on the prior assistant message unmatched,
        which providers reject with HTTP 400.
      * Non-tool messages whose content becomes empty are dropped.  In
        practice this only hits synthetic image-only user messages appended
        for attachment delivery; real user turns always include text.

    Returns True if any image parts were removed.
    Fr%   Útype>   ÚimageÚ	image_urlÚinput_imageTr+   rq   u:   [image content removed â€” server does not support images])r   r   r   r,   r   r?   r>   Úreversed)r#   r   Ú	to_deleterD   r-   r%   Ú	new_partsr.   s           r   Ú_strip_images_from_messagesrŒ   ‘  sH  € ð$ €EØ€IÝ˜HÑ%Ô%ð $ð $‰ˆˆ3Ý˜#�tÑ$Ô$ð 	ØØ—'’'˜)Ñ$Ô$ˆÝ˜'¥4Ñ(Ô(ð 	ØØˆ	Øð 	'ð 	'ˆDÝ˜$¥Ñ%Ô%ð '¨$¯(ª(°6Ñ*:Ô*:Ð>cÐ*cÐ*cØ��à× Ò  Ñ&Ô&Ð&Ð&Ýˆy‰>Œ>�C ™LœLÒ(Ð(Øð 	$Ø!*��I‘�Ø—’˜‘” FÒ*Ð*ð "^��I‘�ð × Ò  Ñ#Ô#Ð#øÝ�iÑ Ô ð ð ˆØ�QˆKˆKØ€Lr   c                ó0   ‡‡— dŠˆˆfd„Š ‰| ¦  «         ‰S )zCStrip non-ASCII characters from nested dict/list payloads in-place.Fc                ó&  •— t          | t          ¦  «        ru|                      ¦   «         D ]^\  }}t          |t          ¦  «        rt	          |¦  «        }||k    r|| |<   dŠŒ7t          |t          t
          f¦  «        r ‰|¦  «         Œ_d S t          | t
          ¦  «        rnt          | ¦  «        D ]`\  }}t          |t          ¦  «        rt	          |¦  «        }||k    r|| |<   dŠŒ7t          |t          t
          f¦  «        r ‰|¦  «         Œ_d S d S )NT)r   r   r   r   r|   r   r   )r   r   r   r~   r   r   r   s        €€r   r   z,_sanitize_structure_non_ascii.<locals>._walkÅ  s5  ø€ å�d�DÑ!Ô!ð 	!Ø"Ÿjšj™lœlð !ð !‘
��UÝ˜e¥SÑ)Ô)ð !Ý 0°Ñ 7Ô 7�IØ  EÒ)Ð)Ø$-˜˜S™	Ø $˜øÝ ­­d |Ñ4Ô4ð !Ø�E˜%‘L”L�Løð!ð !õ ˜�dÑ#Ô#ð 	!Ý'¨™oœoð !ð !‘
��UÝ˜e¥SÑ)Ô)ð !Ý 0°Ñ 7Ô 7�IØ  EÒ)Ð)Ø$-˜˜S™	Ø $˜øÝ ­­d |Ñ4Ô4ð !Ø�E˜%‘L”L�Løð	!ð 	!ð!ð !r   r    r!   s    @@r   r‚   r‚   Á  s;   øø€ à€Eð!ð !ð !ð !ð !ð !ð* 
€Eˆ'�N„N€NØ€Lr   )r
   rt   r   r"   r4   rG   rn   r|   r   rƒ   rŒ   r‚   Údeterministic_call_idÚcoalesce_tool_call_idÚuniquify_tool_call_idsÚreasoning_echo_familyÚmatches_reasoning_echo_familyÚneeds_reasoning_echoÚapply_reasoning_content_policyÚreapply_reasoning_echor2   r*   ÚindexÚintc                ó¦   — | › d|› d|› �}t          j        |                     dd¬¦  «        ¦  «                             ¦   «         dd…         }d|› �S )u  Generate a deterministic call_id from tool call content.

    Used as a fallback when the API doesn't provide a call_id.
    Deterministic IDs prevent cache invalidation â€” random UUIDs would
    make every API call's prefix unique, breaking OpenAI's prompt cache.
    rO   zutf-8Úreplacerx   Né   Úcall_)ÚhashlibÚsha256rz   Ú	hexdigest)r2   r*   r—   ÚseedÚdigests        r   r�   r�     sd   € ð Ð+Ð+˜	Ð+Ð+ EÐ+Ð+€DÝŒ^˜DŸKšK¨¸	˜KÑBÔBÑCÔC×MÒMÑOÔOÐPSÐQSÐPSÔT€FØ�6ÐÐÐr   r/   c                ó  — t          | t          ¦  «        r@|                      dd¦  «        p|                      dd¦  «        pd                     ¦   «         S t	          | dd¦  «        pt	          | dd¦  «        pd                     ¦   «         S )a'  Extract the effective call ID from a tool_call entry (dict or object).

    Single owner for the ``call_id or id`` coalescing rule: Codex Responses
    tool calls carry ``call_id`` (authoritative pairing key), Chat
    Completions ones carry ``id`` only. Returns ``""`` when neither is set.
    Úcall_idr=   r(   )r   r   r,   rX   Úgetattr)r/   s    r   r�   r�     sƒ   € õ �"•dÑÔð IØ—’�y "Ñ%Ô%Ð?¨¯ª°°bÑ)9Ô)9Ð?¸R×FÒFÑHÔHÐHÝ�B˜	 2Ñ&Ô&ÐE­'°"°d¸BÑ*?Ô*?ÐEÀ2×LÒLÑNÔNÐNr   r'   c                óð  ‡	— t          ¦   «         }| pg D �]`}t          |t          ¦  «        r-|                     d¦  «        p|                     d¦  «        pd}n$t	          |dd¦  «        pt	          |dd¦  «        pd}t          |t
          ¦  «        r|                     ¦   «         nd}|sŒ—|                     dd¦  «        d         }|sŒ¶||vr|                     |¦  «         ŒÐd}|› d	|› �Š	‰	|v r|dz  }|› d	|› �Š	‰	|v °|                     ‰	¦  «         ˆ	fd
„}	 t          |t          ¦  «        rJ|                     d¦  «        r ||d         ¦  «        |d<   n‰	|d<   |                     d¦  «        r‰	|d<   n7 |t	          |dd¦  «        ¦  «        |_	        t	          |dd¦  «        r‰	|_
        n,# t          $ r t                               d|¦  «         Y �ŒÇw xY wt          |t          ¦  «        r|                     d¦  «        nt	          |dd¦  «        }t          |t          ¦  «        r|                     d¦  «        nt	          |dd¦  «        pd}t                               d|‰	|¦  «         �Œb| S )uù  Ensure every tool call in a single assistant turn has a distinct id.

    Some models/providers reuse one call id across different calls in a
    single batch (observed with native Kimi Responses replays, Ollama-
    compatible endpoints, and degraded models at long context; same bug
    class as openclaw/openclaw#110518 / #110956). Duplicate ids are lossy
    downstream: the pre-API sanitizer keeps only the first call/result
    pair per id (#58327), so the later call's result silently vanishes
    from every replayed payload, and strict providers (Anthropic
    tool_use, DeepSeek) reject duplicate ids outright.

    The first occurrence keeps its id; later collisions get a
    deterministic ``<id>_d<n>`` suffix â€” never a random UUID, which would
    break prompt-cache prefix stability across replays. Mutates the
    entries in place (SDK models / SimpleNamespace / dicts) and returns
    the same list. Blank/missing ids are left for the deterministic
    fallback in ``build_assistant_message``.
    r£   r(   r=   NÚ|r8   r   r9   Ú_dc                ó|   •— t          | t          ¦  «        r%d| v r!‰› d|                      dd¦  «        d         › �S ‰S )Nr¦   r8   )r   r   Úsplit)r   Únew_ids    €r   Ú_renamedz(uniquify_tool_call_ids.<locals>._renamedR  sK   ø€ õ ˜%¥Ñ%Ô%ð <¨#°¨,¨,Ø Ð;Ð; 5§;¢;¨s°AÑ#6Ô#6°qÔ#9Ð;Ð;Ð;ØˆMr   z,Could not uniquify duplicate tool call id %sr)   r&   rH   zyModel reused tool call id %s within one turn; renamed the duplicate to %s (tool=%s) to keep call/result pairing lossless.)Úsetr   r   r,   r¤   r   rX   r©   Úaddr(   r£   Ú	ExceptionrY   rZ   )
r'   Úseenr/   r5   ÚcidrE   r«   Ú_fnÚ_fn_namerª   s
            @r   r‘   r‘   %  sÔ  ø€ õ& ‘”€DØÐ˜Bð 7
ñ 7
ˆõ �b�$ÑÔð 	PØ—&’&˜Ñ#Ô#Ð9 r§v¢v¨d¡|¤|Ð9°rˆCˆCå˜"˜i¨Ñ.Ô.ÐOµ'¸"¸dÀDÑ2IÔ2IÐOÈRˆCÝ'¨­SÑ1Ô1Ð9ˆc�iŠi‰kŒkˆk°rˆØð 	Øð �iŠi˜˜QÑÔ Ô"ˆØð 	ØØ�dˆ?ˆ?Ø�HŠH�S‰MŒMˆMØØˆØ��˜1��ˆØ˜ˆnˆnØ�‰FˆAØ�]�]˜q�]�]ˆFð ˜ˆnˆnð 	�Š�ÑÔÐð	ð 	ð 	ð 	ð 	ð	Ý˜"�dÑ#Ô#ð 
(Ø—6’6˜$‘<”<ð &Ø'˜x¨¨4¬Ñ1Ô1�B�t‘H�Hà%�B�t‘HØ—6’6˜)Ñ$Ô$ð +Ø$*�B�y‘Møà ˜¥¨¨T°4Ñ!8Ô!8Ñ9Ô9�”Ý˜2˜y¨$Ñ/Ô/ð (Ø!'�B”JøøÝð 	ð 	ð 	Ý�NŠNØ>Àñô ð ð ‰Hð		øøøõ
 %/¨rµ4Ñ$8Ô$8Ð[ˆb�fŠf�ZÑ Ô Ð ½gÀbÈ*ÐVZÑ>[Ô>[ˆÝ'1°#µtÑ'<Ô'<Ð\�C—G’G˜F‘O”O�OÅ'È#ÈvÐW[ÑB\ÔB\ÐdÐadˆÝ�Šðà˜f hñ	
ô 	
ð 	
ñ 	
ð
 Ðs   ÄBF3Æ3%GÇGÚkimizkimi-codingzkimi-coding-cnr    )zapi.kimi.comzmoonshot.aizmoonshot.cnÚdeepseek)r´   )zapi.deepseek.comÚmimoÚxiaomi)rµ   )zapi.xiaomimimo.comzxiaomimimo.comÚtupleÚ_REASONING_ECHO_RULESÚfamilyc                óT   — t           D ]}|d         | k    r|c S Œt          | ¦  «        ‚)Nr   )r¸   ÚKeyError)r¹   Úrules     r   Ú_family_ruler½   ™  s<   € Ý%ð ð ˆØ�Œ7�fÒÐØˆKˆKˆKð å
�6Ñ
Ô
Ðr   ÚproviderÚmodelÚbase_urlc                ó  ‡‡
‡— ddl mŠ
 t          | ¦  «        \  }}}}}|pd                     ¦   «         }	|pd                     ¦   «         Š||v s|	|v rdS t	          ˆfd„|D ¦   «         ¦  «        rdS t	          ˆˆ
fd„|D ¦   «         ¦  «        S )a  True when (provider, model, base_url) matches one echo-back family.

    Families can overlap (e.g. a deepseek-named model pointed at a kimi
    host); this membership test is independent per family so per-family
    predicates keep their original semantics.
    r   )Úbase_url_host_matchesr=   Tc              3  ó    •K  — | ]}|‰v V — Œ	d S ©Nr    )Ú.0r   Úmodel_lowers     €r   ú	<genexpr>z0matches_reasoning_echo_family.<locals>.<genexpr>°  s(   øè è € Ð
4Ð
4 #ˆ3�+ÐÐ
4Ð
4Ð
4Ð
4Ð
4Ð
4r   c              3  ó0   •K  — | ]} ‰‰|¦  «        V — Œd S rÄ   r    )rÅ   ÚhostrÀ   rÂ   s     €€r   rÇ   z0matches_reasoning_echo_family.<locals>.<genexpr>²  s1   øè è € ÐGÐG¸Ð$Ð$ X¨tÑ4Ô4ÐGÐGÐGÐGÐGÐGr   )ÚutilsrÂ   r½   ÚlowerÚany)r¹   r¾   r¿   rÀ   rl   Úraw_providersÚlowered_providersÚ
model_subsÚhostsÚprovider_lowerrÂ   rÆ   s      `      @@r   r“   r“      sÇ   øøø€ ð ,Ð+Ð+Ð+Ð+Ð+å=IÈ&Ñ=QÔ=QÑ:€A€}Ð'¨°UØ�n "×+Ò+Ñ-Ô-€NØ�;˜B×%Ò%Ñ'Ô'€KØ�=Ð Ð  NÐ6GÐ$GÐ$GØˆtÝ
Ð
4Ð
4Ð
4Ð
4¨Ð
4Ñ
4Ô
4Ñ4Ô4ð ØˆtÝÐGÐGÐGÐGÐGÀÐGÑGÔGÑGÔGÐGr   ú'str | None'c                ó^   — t           D ]$}t          |d         | ||¦  «        r
|d         c S Œ%dS )uE  Classify the provider direction for the reasoning_content echo policy.

    Returns ``"kimi"``, ``"deepseek"``, or ``"mimo"`` (first match in table
    order) when the target endpoint enforces reasoning_content echo-back on
    assistant turns, else ``None`` (strict/indifferent side â€” the field must
    be stripped).
    r   N)r¸   r“   )r¾   r¿   rÀ   r¼   s       r   r’   r’   µ  sD   € õ &ð ð ˆÝ(¨¨a¬°(¸EÀ8ÑLÔLð 	Ø˜”7ˆNˆNˆNð	àˆ4r   c                ó(   — t          | ||¦  «        duS )z<True when the endpoint requires reasoning_content echo-back.N)r’   )r¾   r¿   rÀ   s      r   r”   r”   Ã  s   € å  ¨5°(Ñ;Ô;À4ÐGÐGr   Ú
source_msgr   Úapi_msgÚneeds_thinking_padrL   c                ó(  — |                       d¦  «        dk    rdS |                       d¦  «        }t          |t          ¦  «        r,|s|                     dd¦  «         n|dk    rd|d<   n||d<   dS |                       d¦  «        }|r3|                       d¦  «        rt          |t          ¦  «        r	|rd|d<   dS t          |t          ¦  «        r"|r |r||d<   n|                     dd¦  «         dS |rd|d<   dS |                     dd¦  «         dS )	zøCopy provider-facing reasoning fields onto an API replay message.

    ``needs_thinking_pad`` is the require-side flag (see
    ``needs_reasoning_echo`` / the agent's cached
    ``_needs_thinking_reasoning_pad``). Mutates ``api_msg`` in place.
    r+   rr   NÚreasoning_contentr=   Ú Ú	reasoningr'   )r,   r   r   Úpop)rÕ   rÖ   r×   ÚexistingÚnormalized_reasonings        r   r•   r•   È  sr  € ð ‡~‚~�fÑÔ Ò,Ð,Øˆð( �~Š~Ð1Ñ2Ô2€HÝ�(�CÑ Ô ð Ø!ð 	4Ø�KŠKÐ+¨TÑ2Ô2Ð2Ð2Ø˜Š^ˆ^Ø+.ˆGÐ'Ñ(Ð(à+3ˆGÐ'Ñ(Øˆð &Ÿ>š>¨+Ñ6Ô6Ðàðà�NŠN˜<Ñ(Ô(ðõ Ð+­SÑ1Ô1ðð !ð	ð (+ˆÐ#Ñ$Øˆõ Ð&­Ñ,Ô,ð Ð1Eð Øð 	3Ø+?ˆGÐ'Ñ(Ð(à�KŠKÐ+¨TÑ2Ô2Ð2Øˆð ð Ø'*ˆÐ#Ñ$Øˆð ‡K‚KÐ# TÑ*Ô*Ð*Ð*Ð*r   Úapi_messagesc                ó  — d}| D ]}|                      d¦  «        dk    rŒ|rB|                      d¦  «        rŒ4t          |||¦  «         |                      d¦  «        r|dz  }Œ`d|v r|                     dd¦  «         |dz  }Œ€|S )u0  Re-pad (or strip) assistant turns' reasoning_content for the active provider.

    ``api_messages`` is built once, before the retry loop, while the *primary*
    provider is active.  A mid-conversation fallback can then switch providers,
    so the reasoning fields baked into ``api_messages`` are shaped for the
    *prior* provider and must be reconciled against the *current* one:

    * Switching TO a require-side provider (DeepSeek / Kimi / MiMo thinking
      mode): assistant turns built when the prior provider did NOT need the
      echo-back go out without ``reasoning_content`` and the new provider
      rejects them with HTTP 400 ("The reasoning_content in the thinking mode
      must be passed back").  Re-apply the pad.

    * Switching TO a strict provider that rejects the field (Mistral,
      Cerebras, Groq, SambaNova, â€¦): assistant turns built under a reasoning
      primary carry a ``reasoning_content`` pad (often a single space ``" "``),
      and the strict provider rejects it with HTTP 400/422 ("Extra inputs are
      not permitted").  Strip the field.  This is the exact cross-provider
      fallback bug from #45655 â€” a DeepSeek primary pads history with ``" "``,
      the request falls back to Mistral, and Mistral 422s on the stale pad.

    Calling this immediately before building the request kwargs reconciles the
    fields against the *current* provider.  It is idempotent and safe to call
    every iteration; it covers every fallback path.

    Returns the number of assistant turns whose reasoning_content was added or
    removed.
    r   r+   rr   rÙ   r8   N)r,   r•   rÜ   )rß   r×   ÚchangedrÖ   s       r   r–   r–   #  sµ   € ð: €GØð ð ˆØ�;Š;�vÑÔ +Ò-Ð-ØØð 	Ø�{Š{Ð.Ñ/Ô/ð ØÝ*¨7°GÐ=OÑPÔPÐPØ�{Š{Ð.Ñ/Ô/ð Ø˜1‘�øð
 # gÐ-Ð-Ø—’Ð/°Ñ6Ô6Ð6Ø˜1‘�øØ€Nr   )r   r   r   r   )r   r   r   r   )r#   r   r   r   )r5   r   r   r   )rH   )rI   r   rJ   r   r   r   rÄ   )r#   r   ro   r   r   r   )r€   r   r   r   )r   )r2   r   r*   r   r—   r˜   r   r   )r/   r   r   r   )r'   r   r   r   )r¹   r   r   r·   )
r¹   r   r¾   r   r¿   r   rÀ   r   r   r   )r¾   r   r¿   r   rÀ   r   r   rÒ   )r¾   r   r¿   r   rÀ   r   r   r   )rÕ   r   rÖ   r   r×   r   r   rL   )rß   r   r×   r   r   r˜   )'Ú__doc__Ú
__future__r   r�   r[   Úloggingra   Útypingr   Ú	getLoggerÚ__name__rY   Úcompiler
   r   r"   r4   rG   re   rn   rt   r|   r   rƒ   rŒ   r‚   Ú__all__r�   r�   r‘   Ú	frozensetr¸   Ú__annotations__r½   r“   r’   r”   r•   r–   r    r   r   ú<module>rì      sü  ððð ð ð #Ð "Ð "Ð "Ð "Ð "à €€€Ø €€€Ø €€€Ø 	€	€	€	Ø Ð Ð Ð Ð Ð à	ˆÔ	˜8Ñ	$Ô	$€ð �”
Ð-Ñ.Ô.€ðð ð ð ðð ð ð ðBAð Að Að AðH'ð 'ð 'ð 'ð` Ð ðbð bð bð bð bðJð ð ð ð ð@Að Að Að Að8ð 8ð 8ð 8ðv0ð 0ð 0ð 0ð
-ð -ð -ð -ð`ð ð ð ð:ð ð €ð^	ð 	ð 	ð 	ð 	ð	Oð 	Oð 	Oð 	OðLð Lð Lð LðV ˆYˆY˜Ð'7Ð8Ñ9Ô9¸9¸9¹;¼;ÈØ3ð5à��‘”˜i˜i¨¨Ñ5Ô5°}ØðàˆYˆY‰[Œ[˜)˜) X JÑ/Ô/°Ø-ð/ð	 Ð ð 	ð 	ð 	ñ 	ðð ð ð ðHð Hð Hð Hð*ð ð ð ðHð Hð Hð Hð
X+ð X+ð X+ð X+ðv.ð .ð .ð .ð .ð .r   