
    Fj]                    F   d Z ddlmZ ddlZddlZddlZddlmZmZm	Z	m
Z
 ddlmZ ddlmZ  ej        e          Z ej        dej                  Z ej        dej                  Z ej        d	ej                  ZddZ G d d          ZddZ G d d          ZdS )uT  MemoryManager — orchestrates memory providers for the agent.

Single integration point in run_agent.py. Replaces scattered per-backend
code with one manager that delegates to registered providers.

Only ONE external plugin provider is allowed at a time — attempting to
register a second external provider is rejected with a warning.  This
prevents tool schema bloat and conflicting memory backends.

Usage in run_agent.py:
    self._memory_manager = MemoryManager()
    # Only ONE of these:
    self._memory_manager.add_provider(plugin_provider)

    # System prompt
    prompt_parts.append(self._memory_manager.build_system_prompt())

    # Pre-turn
    context = self._memory_manager.prefetch_all(user_message)

    # Post-turn
    self._memory_manager.sync_all(user_msg, assistant_response)
    self._memory_manager.queue_prefetch_all(user_msg)
    )annotationsN)AnyDictListOptional)MemoryProvider)
tool_errorz</?\s*memory-context\s*>z5<\s*memory-context\s*>[\s\S]*?</\s*memory-context\s*>z\[System note:\s*The following is recalled memory context,\s*NOT new user input\.\s*Treat as (?:informational background data|authoritative reference data[^\]]*)\.\]\s*textstrreturnc                    t                               d|           } t                              d|           } t                              d|           } | S )zQStrip fence tags, injected context blocks, and system notes from provider output. )_INTERNAL_CONTEXT_REsub_INTERNAL_NOTE_RE_FENCE_TAG_RE)r
   s    3/usr/local/lib/hermes-agent/agent/memory_manager.pysanitize_contextr   6   sF    ##B--D  T**DR&&DK    c                      e Zd ZdZdZdZddZddZdd
ZddZ	e
dd            ZddZddZd dZd dZd!dZd"dZdS )#StreamingContextScrubbera  Stateful scrubber for streaming text that may contain split memory-context spans.

    The one-shot ``sanitize_context`` regex cannot survive chunk boundaries:
    a ``<memory-context>`` opened in one delta and closed in a later delta
    leaks its payload to the UI because the non-greedy block regex needs
    both tags in one string.  This scrubber runs a small state machine
    across deltas, holding back partial-tag tails and discarding
    everything inside a span (including the system-note line).

    Usage::

        scrubber = StreamingContextScrubber()
        for delta in stream:
            visible = scrubber.feed(delta)
            if visible:
                emit(visible)
        trailing = scrubber.flush()  # at end of stream
        if trailing:
            emit(trailing)

    The scrubber is re-entrant per agent instance.  Callers building new
    top-level responses (new turn) should create a fresh scrubber or call
    ``reset()``.
    z<memory-context>z</memory-context>r   Nonec                0    d| _         d| _        d| _        d S NFr   T_in_span_buf_at_block_boundaryselfs    r   __init__z!StreamingContextScrubber.__init__[   s    #	(,r   c                0    d| _         d| _        d| _        d S r   r   r   s    r   resetzStreamingContextScrubber.reset`   s    	"&r   r
   r   c                   |sdS | j         |z   }d| _         g }|r| j        r|                                                    | j                  }|dk    rD|                     || j                  }|r|| d         nd| _         d                    |          S ||t          | j                  z   d         }d| _        n|                     |          }|dk    r| 	                    |          p|                     || j
                  }|r0|                     ||d|                     || d         | _         n|                     ||           d                    |          S |dk    r|                     ||d|                    ||t          | j
                  z   d         }d| _        |d                    |          S )a  Return the visible portion of ``text`` after scrubbing.

        Any trailing fragment that could be the start of an open/close tag
        is held back in the internal buffer and surfaced on the next
        ``feed()`` call or discarded/emitted by ``flush()``.
        r   NFr   T)r   r   lowerfind
_CLOSE_TAG_max_partial_suffixjoinlen_find_boundary_open_tag_max_pending_open_suffix	_OPEN_TAG_append_visible)r    r
   bufoutidxhelds         r   feedzStreamingContextScrubber.feede   s     	2i$	 	%} %iikk&&t77"9933CIID/3 ;TEFFDI773<<'#DO 4 44556 %22377"99 55c:: I33CHH   7,,S#fuf+>>>$'K		,,S#666773<<'77((c$3$i888#DN 3 33445 $;  	%> wws||r   c                P    | j         rd| _        d| _         dS | j        }d| _        |S )aQ  Emit any held-back buffer at end-of-stream.

        If we're still inside an unterminated span the remaining content is
        discarded (safer: leaking partial memory context is worse than a
        truncated answer).  Otherwise the held-back partial-tag tail is
        emitted verbatim (it turned out not to be a real tag).
        r   F)r   r   )r    tails     r   flushzStreamingContextScrubber.flush   s4     = 	DI!DM2y	r   r0   tagintc                   |                                 }|                                  }t          t          |          t          |          dz
            }t          |dd          D ]$}|                    || d                   r|c S %dS )zReturn the length of the longest buf-suffix that is a tag-prefix.

        Case-insensitive.  Returns 0 if no suffix could start the tag.
           r   r%   N)r&   minr+   range
startswith)r0   r8   	tag_lower	buf_lower	max_checkis         r   r)   z,StreamingContextScrubber._max_partial_suffix   s     IIKK	IIKK	II(:;;	y!R(( 	 	A##IqbccN33 qr   c                    |                                 }d}	 |                    | j        |          }|dk    rdS |                     ||          r|                     ||          r|S |dz   }W)z<Find an opening fence only when it starts a block-like span.r   Tr%   r;   )r&   r'   r.   _is_block_boundary_has_block_opener_suffix)r    r0   r@   search_startr2   s        r   r,   z0StreamingContextScrubber._find_boundary_open_tag   s    IIKK		#..>>Cbyyr&&sC00 T5R5RSVX[5\5\ 
7L	#r   c                    |                                                     | j                  sdS t          |          t          | j                  z
  }|                     ||          sdS t          | j                  S )zBHold a complete boundary tag until the following char confirms it.r   )r&   endswithr.   r+   rD   )r    r0   r2   s      r   r-   z1StreamingContextScrubber._max_pending_open_suffix   sn    yy{{##DN33 	1#hhT^,,,&&sC00 	14>"""r   r2   boolc                n    |t          | j                  z   }|t          |          k    rdS ||         dv S )NFz
)r+   r.   )r    r0   r2   	after_idxs       r   rE   z1StreamingContextScrubber._has_block_opener_suffix   s;    #dn---	C  59~''r   c                    |dk    r| j         S |d |         }|                    d          }|dk    r| j         o|                                dk    S ||dz   d                                          dk    S )Nr   
r%   r   r;   )r   rfindstrip)r    r0   r2   	precedinglast_newlines        r   rD   z+StreamingContextScrubber._is_block_boundary   s    !88**I	 t,,2*Fy/@/@B/FF)**+1133r99r   r1   	list[str]c                b    |sd S |                     |           |                     |           d S N)append_update_block_boundary)r    r1   r
   s      r   r/   z(StreamingContextScrubber._append_visible   s;     	F

4##D)))))r   c                    |                     d          }|dk    r*||dz   d                                          dk    | _        d S | j        o|                                dk    | _        d S )NrM   r%   r;   r   )rN   rO   r   )r    r
   rQ   s      r   rV   z/StreamingContextScrubber._update_block_boundary   sq    zz$''2&*<!+;+<+<&=&C&C&E&E&KD###&*&=&T$**,,RTBTD###r   Nr   r   r
   r   r   r   r   r   )r0   r   r8   r   r   r9   )r0   r   r   r9   )r0   r   r2   r9   r   rI   )r1   rR   r
   r   r   r   )r
   r   r   r   )__name__
__module____qualname____doc__r.   r(   r!   r#   r4   r7   staticmethodr)   r,   r-   rE   rD   r/   rV    r   r   r   r   >   s        2 #I$J- - - -
' ' ' '
, , , ,\        \
# 
# 
# 
## # # #( ( ( (: : : :* * * *U U U U U Ur   r   raw_contextc                    | r|                                  sdS t          |           }|| k    rt                              d           d| dS )z:Wrap prefetched memory in a fenced block with system note.r   z6memory provider returned pre-wrapped context; strippedu   <memory-context>
[System note: The following is recalled memory context, NOT new user input. Treat as authoritative reference data — this is the agent's persistent memory and should inform all responses.]

z
</memory-context>)rO   r   loggerwarning)ra   cleans     r   build_memory_context_blockrf      sk     k//11 r[))EOPPP	 		 	 	r   c                     e Zd ZdZdAdZdBdZedCd	            ZdDdZdEdZ	dddFdZ
dddGdZedHd            ZddddIdZdJdZdKd!ZdLd#ZdMd&ZdNd*ZdOd+Zdd,d-dPd1ZdQd2ZedRd3            Z	 dSdTd9Zdd:dUd>ZdAd?ZdVd@ZdS )WMemoryManagerzOrchestrates the built-in provider plus at most one external provider.

    The builtin provider is always first. Only one non-builtin (external)
    provider is allowed.  Failures in one provider never block the other.
    r   r   c                0    g | _         i | _        d| _        d S )NF)
_providers_tool_to_provider_has_externalr   s    r   r!   zMemoryManager.__init__   s    02<>#(r   providerr   c                p   |j         dk    }|sP| j        rBt          d | j        D             d          }t                              d|j         |           dS d| _        | j                            |           |                                D ]i}|                    dd          }|r|| j	        vr|| j	        |<   .|| j	        v r2t                              d	|| j	        |         j         |j                    jt          
                    d
|j         t          |                                                     dS )u   Register a memory provider.

        Built-in provider (name ``"builtin"``) is always accepted.
        Only **one** external (non-builtin) provider is allowed — a second
        attempt is rejected with a warning.
        builtinc              3  :   K   | ]}|j         d k    |j         V  dS )ro   N)name.0ps     r   	<genexpr>z-MemoryManager.add_provider.<locals>.<genexpr>  s0      LL)8K8KQV8K8K8K8KLLr   unknownu   Rejected memory provider '%s' — external provider '%s' is already registered. Only one external memory provider is allowed at a time. Configure which one via memory.provider in config.yaml.NTrq   r   zJMemory tool name conflict: '%s' already registered by %s, ignoring from %sz*Memory provider '%s' registered (%d tools))rq   rl   nextrj   rc   rd   rU   get_tool_schemasgetrk   infor+   )r    rm   
is_builtinexistingschema	tool_names         r   add_providerzMemoryManager.add_provider  sh    ]i/
 	&! LLT_LLLi  & M8   !%Dx((( //11 	 	F

62..I 	Yd.DDD4<&y11d444'*95:M   	8M))++,,	
 	
 	
 	
 	
r   List[MemoryProvider]c                *    t          | j                  S )z"All registered providers in order.)listrj   r   s    r   	providerszMemoryManager.providers0  s     DO$$$r   rq   r   Optional[MemoryProvider]c                8    | j         D ]}|j        |k    r|c S dS )z2Get a provider by name, or None if not registered.N)rj   rq   )r    rq   rt   s      r   get_providerzMemoryManager.get_provider5  s1     	 	Av~~ tr   c                4   g }| j         D ]z}	 |                                }|r)|                                r|                    |           C# t          $ r+}t
                              d|j        |           Y d}~sd}~ww xY wd                    |          S )zCollect system prompt blocks from all providers.

        Returns combined text, or empty string if no providers contribute.
        Each non-empty block is labeled with the provider name.
        z5Memory provider '%s' system_prompt_block() failed: %sN

)	rj   system_prompt_blockrO   rU   	Exceptionrc   rd   rq   r*   )r    blocksrm   blockes        r   build_system_promptz!MemoryManager.build_system_prompt>  s      		 		H 4466 )U[[]] )MM%(((   KM1       
 {{6"""s   ?A
B!A==Br   
session_idqueryr   c               :   g }| j         D ]}}	 |                    ||          }|r)|                                r|                    |           F# t          $ r+}t
                              d|j        |           Y d}~vd}~ww xY wd                    |          S )zCollect prefetch context from all providers.

        Returns merged context text labeled by provider. Empty providers
        are skipped. Failures in one provider don't block others.
        r   z4Memory provider '%s' prefetch failed (non-fatal): %sNr   )	rj   prefetchrO   rU   r   rc   debugrq   r*   )r    r   r   partsrm   resultr   s          r   prefetch_allzMemoryManager.prefetch_allS  s      		 		H!**5Z*HH )fllnn )LL(((   JM1       
 {{5!!!s   AA
B!B  Bc                   | j         D ]R}	 |                    ||           # t          $ r+}t                              d|j        |           Y d}~Kd}~ww xY wdS )z=Queue background prefetch on all providers for the next turn.r   z:Memory provider '%s' queue_prefetch failed (non-fatal): %sN)rj   queue_prefetchr   rc   r   rq   )r    r   r   rm   r   s        r   queue_prefetch_allz MemoryManager.queue_prefetch_allf  s     	 	H''*'EEEE   PM1       	 	s   #
A!AArI   c                    	 t          j        | j                  }n# t          t          f$ r Y dS w xY wt          |j                                                  }t          d |D                       rdS d|j        v S )z4Return whether sync_turn accepts a messages keyword.Tc              3  J   K   | ]}|j         t          j        j        k    V  d S rT   kindinspect	ParameterVAR_KEYWORDrr   s     r   ru   z@MemoryManager._provider_sync_accepts_messages.<locals>.<genexpr>{  /      GG1qv*66GGGGGGr   messages)	r   	signature	sync_turn	TypeError
ValueErrorr   
parametersvaluesany)rm   r   paramss      r   _provider_sync_accepts_messagesz-MemoryManager._provider_sync_accepts_messagess  s    	)(*<==II:& 	 	 	44	i*113344GGGGGGG 	4Y111    11Nr   r   user_contentassistant_contentr   Optional[List[Dict[str, Any]]]c                  | j         D ]}	 |/|                     |          r|                    ||||           n|                    |||           M# t          $ r+}t                              d|j        |           Y d}~}d}~ww xY wdS )z'Sync a completed turn to all providers.Nr   r   z)Memory provider '%s' sync_turn failed: %s)rj   r   r   r   rc   rd   rq   )r    r   r   r   r   rm   r   s          r   sync_allzMemoryManager.sync_all  s      	 	H'D,P,PQY,Z,Z'&&$)#-!)	 '     &&$)#- '   
    ?M1       	 	s   A	A
B
!BB
List[Dict[str, Any]]c                f   g }t                      }| j        D ]}	 |                                D ]H}|                    dd          }|r.||vr*|                    |           |                    |           Ia# t          $ r+}t                              d|j	        |           Y d}~d}~ww xY w|S )z(Collect tool schemas from all providers.rq   r   z2Memory provider '%s' get_tool_schemas() failed: %sN)
setrj   rx   ry   rU   addr   rc   rd   rq   )r    schemasseenrm   r}   rq   r   s          r   get_all_tool_schemasz"MemoryManager.get_all_tool_schemas  s    uu 	 	H
&7799 ' 'F!::fb11D 'D 0 0v...	'
    HM1       
 s   AA99
B.!B))B.r   c                N    t          | j                                                  S )z2Return set of all tool names across all providers.)r   rk   keysr   s    r   get_all_tool_namesz MemoryManager.get_all_tool_names  s    4)..00111r   r~   c                    || j         v S )z(Check if any provider handles this tool.)rk   )r    r~   s     r   has_toolzMemoryManager.has_tool  s    D222r   argsDict[str, Any]c                   | j                             |          }|t          d| d          S 	  |j        ||fi |S # t          $ rA}t
                              d|j        ||           t          d| d|           cY d}~S d}~ww xY w)zRoute a tool call to the correct provider.

        Returns JSON string result. Raises ValueError if no provider
        handles the tool.
        Nz!No memory provider handles tool ''z4Memory provider '%s' handle_tool_call(%s) failed: %szMemory tool 'z
' failed: )rk   ry   r	   handle_tool_callr   rc   errorrq   )r    r~   r   kwargsrm   r   s         r   r   zMemoryManager.handle_tool_call  s     )--i88N)NNNOOO	H,8,YGGGGG 	H 	H 	HLLFy!   FiFF1FFGGGGGGGG	Hs   A   
B
6B BBturn_numberr9   messagec                    | j         D ]J}	  |j        ||fi | # t          $ r+}t                              d|j        |           Y d}~Cd}~ww xY wdS )zxNotify all providers of a new turn.

        kwargs may include: remaining_tokens, model, platform, tool_count.
        z-Memory provider '%s' on_turn_start failed: %sN)rj   on_turn_startr   rc   r   rq   )r    r   r   r   rm   r   s         r   r   zMemoryManager.on_turn_start  s    
  	 	H&&{GFFvFFFF   CM1       	 	s   
A!AAc                    | j         D ]P}	 |                    |           # t          $ r+}t                              d|j        |           Y d}~Id}~ww xY wdS )z$Notify all providers of session end.z.Memory provider '%s' on_session_end failed: %sN)rj   on_session_endr   rc   r   rq   )r    r   rm   r   s       r   r   zMemoryManager.on_session_end  s     	 	H''1111   DM1       	 	s   !
A!AAFparent_session_idr#   new_session_idr   r#   c                   |sdS | j         D ]L}	  |j        |f||d| # t          $ r+}t                              d|j        |           Y d}~Ed}~ww xY wdS )u  Notify all providers that the agent's session_id has rotated.

        Fires on ``/resume``, ``/branch``, ``/reset``, ``/new``, and
        context compression — any path that reassigns
        ``AIAgent.session_id`` without tearing the provider down.

        Providers keep running; they only need to refresh cached
        per-session state so subsequent writes land in the correct
        session's record. See ``MemoryProvider.on_session_switch`` for
        the full contract.
        Nr   z1Memory provider '%s' on_session_switch failed: %s)rj   on_session_switchr   rc   r   rq   )r    r   r   r#   r   rm   r   s          r   r   zMemoryManager.on_session_switch  s    &  	F 	 	H**"&7  	       GM1       	 	s   !
A!AAc                6   g }| j         D ]{}	 |                    |          }|r)|                                r|                    |           D# t          $ r+}t
                              d|j        |           Y d}~td}~ww xY wd                    |          S )zNotify all providers before context compression.

        Returns combined text from providers to include in the compression
        summary prompt. Empty string if no provider contributes.
        z/Memory provider '%s' on_pre_compress failed: %sNr   )	rj   on_pre_compressrO   rU   r   rc   r   rq   r*   )r    r   r   rm   r   r   s         r   r   zMemoryManager.on_pre_compress  s      		 		H!11(;; )fllnn )LL(((   EM1       
 {{5!!!s   A A
B!A>>Bc                F   	 t          j        | j                  }n# t          t          f$ r Y dS w xY wt          |j                                                  }t          d |D                       rdS d|j        v rdS d |D             }t          |          dk    rdS dS )z>Return how to pass metadata to a provider's memory-write hook.keywordc              3  J   K   | ]}|j         t          j        j        k    V  d S rT   r   rr   s     r   ru   zEMemoryManager._provider_memory_write_metadata_mode.<locals>.<genexpr>'  r   r   metadatac                    g | ]=}|j         t          j        j        t          j        j        t          j        j        hv ;|>S r`   )r   r   r   POSITIONAL_ONLYPOSITIONAL_OR_KEYWORDKEYWORD_ONLYrr   s     r   
<listcomp>zFMemoryManager._provider_memory_write_metadata_mode.<locals>.<listcomp>,  sW     
 
 
v!1!7!.     r      
positionallegacy)
r   r   on_memory_writer   r   r   r   r   r   r+   )rm   r   r   accepteds       r   $_provider_memory_write_metadata_modez2MemoryManager._provider_memory_write_metadata_mode  s    	)(*BCCII:& 	 	 	99	 i*113344GGGGGGG 	9---9
 

 
 
 x==A<xr   actiontargetcontentr   Optional[Dict[str, Any]]c           	        | j         D ]}|j        dk    r	 |                     |          }|dk    r)|                    |||t	          |pi                      nE|dk    r(|                    |||t	          |pi                      n|                    |||           # t
          $ r+}t                              d|j        |           Y d}~d}~ww xY wdS )zNotify external providers when the built-in memory tool writes.

        Skips the builtin provider itself (it's the source of the write).
        ro   r   )r   r   z/Memory provider '%s' on_memory_write failed: %sN)rj   rq   r   r   dictr   rc   r   )r    r   r   r   r   rm   metadata_moder   s           r   r   zMemoryManager.on_memory_write8  s1     	 	H}	)) $ I I( S S I--,,$x~2:N:N -     #l22,,VVWd8>WYFZFZ[[[[,,VVWEEE   EM1       	 	s   B	B!!
C+!CC)child_session_idtaskr   r   c                   | j         D ]L}	  |j        ||fd|i| # t          $ r+}t                              d|j        |           Y d}~Ed}~ww xY wdS )z/Notify all providers that a subagent completed.r   z-Memory provider '%s' on_delegation failed: %sN)rj   on_delegationr   rc   r   rq   )r    r   r   r   r   rm   r   s          r   r   zMemoryManager.on_delegationV  s      		 		H&&& 3CGM       CM1       		 		s   
A!AAc                    t          | j                  D ]O}	 |                                 # t          $ r+}t                              d|j        |           Y d}~Hd}~ww xY wdS )z;Shut down all providers (reverse order for clean teardown).z(Memory provider '%s' shutdown failed: %sN)reversedrj   shutdownr   rc   rd   rq   )r    rm   r   s      r   shutdown_allzMemoryManager.shutdown_alld  s     11 	 	H!!####   >M1       	 	s   -
A"!AA"c                    d|vr ddl m} t           |                      |d<   | j        D ]J}	  |j        dd|i| # t
          $ r+}t                              d|j        |           Y d}~Cd}~ww xY wdS )zInitialize all providers.

        Automatically injects ``hermes_home`` into *kwargs* so that every
        provider can resolve profile-scoped storage paths without importing
        ``get_hermes_home()`` themselves.
        hermes_homer   )get_hermes_homer   z*Memory provider '%s' initialize failed: %sNr`   )	hermes_constantsr   r   rj   
initializer   rc   rd   rq   )r    r   r   r   rm   r   s         r   initialize_allzMemoryManager.initialize_allo  s     &&888888$'(9(9$:$:F=! 	 	H##DDzDVDDDD   @M1       	 	s   ?
A4	!A//A4rX   )rm   r   r   r   )r   r   )rq   r   r   r   rZ   )r   r   r   r   r   r   )r   r   r   r   r   r   )rm   r   r   rI   )
r   r   r   r   r   r   r   r   r   r   )r   r   )r   r   )r~   r   r   rI   )r~   r   r   r   r   r   )r   r9   r   r   r   r   )r   r   r   r   )r   r   r   r   r#   rI   r   r   )r   r   r   r   )rm   r   r   r   rT   )
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   propertyr   r   r   r   r   r_   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r`   r   r   rh   rh      s^        ) ) ) ),
 ,
 ,
 ,
\ % % % X%   # # # #* =? " " " " " "& CE 	 	 	 	 	 	 	2 	2 	2 \	2  37     @   $2 2 2 23 3 3 3H H H H,   	 	 	 	 "$! ! ! ! ! !F" " " "&    \< .2    > /1     	 	 	 	     r   rh   rY   )ra   r   r   r   )r^   
__future__r   loggingrer   typingr   r   r   r   agent.memory_providerr   tools.registryr	   	getLoggerr[   rc   compile
IGNORECASEr   r   r   r   r   rf   rh   r`   r   r   <module>r     s   2 # " " " " "  				  , , , , , , , , , , , , 0 0 0 0 0 0 % % % % % %		8	$	$ 
6FF!rz<M   BJ pM     bU bU bU bU bU bU bU bUJ   "L L L L L L L L L Lr   