
    .cjS                         d Z ddlmZmZ ddlmZmZmZmZ ddl	m
Z
 dZdZdZdZd	ed
efdZdedededed
edz  f
dZ G d de          ZdS )u  Abstract base class for pluggable context engines.

A context engine controls how conversation context is managed when
approaching the model's token limit. The built-in ContextCompressor
is the default implementation. Third-party engines (e.g. LCM) can
replace it via the plugin system or by being placed in the
``plugins/context_engine/<name>/`` directory.

Selection is config-driven: ``context.engine`` in config.yaml.
Default is ``"compressor"`` (the built-in). Only one engine is active.

The engine is responsible for:
  - Deciding when compaction should fire
  - Performing compaction (summarization, DAG construction, etc.)
  - Optionally exposing tools the agent can call (e.g. lcm_grep)
  - Tracking token usage from API responses

Lifecycle:
  1. Engine is instantiated and registered (plugin register() or default)
  2. on_session_start() called when a conversation begins
  3. update_from_response() called after each API response with usage data
  4. should_compress() checked after each turn
  5. compress() called when should_compress() returns True
  6. on_session_end() called at real session boundaries (CLI exit, /reset,
     gateway session expiry) — NOT per-turn
    )ABCabstractmethod)AnyDictListOptional)redact_sensitive_textip  i  i  z+
...[memory provider context truncated]...
memory_contextreturnc                     t          |                                 dd          }t          |          t          k    r|S |dt                   t
          z   |t           d         z   S )zBPrepare provider context for a context-engine/LLM egress boundary.T)forceredact_url_credentialsN)r	   striplenMEMORY_CONTEXT_MAX_CHARS_MEMORY_CONTEXT_HEAD_CHARS!_MEMORY_CONTEXT_TRUNCATION_MARKER_MEMORY_CONTEXT_TAIL_CHARS)r
   	sanitizeds     6/home/ice/.hermes/hermes-agent/agent/context_engine.pysanitize_memory_contextr   (   sx    %#  I
 9~~111---.
+	,
//00
1	2    enginephasedefault_messagecontextNc                    t          | dd          sdS t          | dd          }t          |          r |d||d|}n|}|dS t          |                                          }|pdS )aD  Resolve host-visible status for an automatic compaction event.

    Engines can suppress routine automatic status with
    ``emit_automatic_compaction_status = False`` or customize it by defining
    ``get_automatic_compaction_status_message(...)``. Empty strings and
    ``None`` mean "do not emit a lifecycle status".
     emit_automatic_compaction_statusTN'get_automatic_compaction_status_message)r   r    )getattrcallablestrr   )r   r   r   r   	formattermessages         r   #automatic_compaction_status_messager&   8   s     6=tDD t I4PPI	 ") 
+
 
 
 
 "t'll  ""G?dr   c                   V   e Zd ZU dZeedefd                        ZdZe	e
d<   dZe	e
d<   dZe	e
d<   dZe	e
d<   dZe	e
d	<   dZe	e
d
<   dZee
d<   dZe	e
d<   dZe	e
d<   dZee
d<   edeeef         ddfd            ZedBde	defd            ZdBde	ddfdZe	 	 	 	 dCdeeeef                  dee	         dee         ded edeeeef                  fd!            Z	 dBdeeeef                  de	dz  deeeeef                  e	f         fd"Zdddd#d$eeeef                  d%eeeef                  d&eeef         d'e	deeeef                  f
d(Z 	 dBdeeeef                  deeef         d)eddfd*Z!deeeef                  defd+Z"d,e	defd-Z#d.ed/ed0ededz  fd1Z$deeeef                  defd2Z%d3eddfd4Z&d3edeeeef                  ddfd5Z'dDd6Z(deeeef                  fd7Z)d8ed9eeef         defd:Z*deeef         fd;Z+	 	 	 	 dEd<ed	e	d=ed>ed?ed@eddfdAZ,dS )FContextEnginez.Base class all context engines must implement.r   c                     dS )z,Short identifier (e.g. 'compressor', 'lcm').Nr    selfs    r   namezContextEngine.name^         r   r   last_prompt_tokenslast_completion_tokenslast_total_tokensthreshold_tokenscontext_lengthcompression_countg      ?threshold_percent   protect_first_n   protect_last_nTr   usageNc                     dS )a  Update tracked token usage from an API response.

        Called after every LLM call with a normalized usage dict. The legacy
        keys ``prompt_tokens``, ``completion_tokens``, and ``total_tokens``
        are always present. Newer hosts also include canonical buckets:
        ``input_tokens``, ``output_tokens``, ``cache_read_tokens``,
        ``cache_write_tokens``, and ``reasoning_tokens``. Engines should
        treat those fields as optional for compatibility with older hosts.
        Nr    )r+   r9   s     r   update_from_responsez"ContextEngine.update_from_response   r-   r   prompt_tokensc                     dS )z0Return True if compaction should fire this turn.Nr    r+   r<   s     r   should_compresszContextEngine.should_compress   r-   r   ztuple[bool, str | None]c                 0    |                      |          dfS )a4  Return ``(should_compress, reason)``.

        The base implementation is backward-compatible: engines that only
        implement ``should_compress`` get ``(should_compress(prompt_tokens),
        None)``. Concrete engines with richer block reasons (e.g. a
        summary-LLM cooldown or an anti-thrashing guard) override this to
        surface a human-readable reason so callers can warn the user instead
        of silently skipping compression. Added for the silent-overflow
        warning fix (#62625) so plugin engines don't raise AttributeError.
        N)r?   r>   s     r   should_compress_infoz"ContextEngine.should_compress_info   s     ##M22D88r   F messagescurrent_tokensfocus_topicr   r
   c                     dS )u  Compact the message list and return the new message list.

        This is the main entry point. The engine receives the full message
        list and returns a (possibly shorter) list that fits within the
        context budget. The implementation is free to summarize, build a
        DAG, or do anything else — as long as the returned list is a valid
        OpenAI-format message sequence.

        Args:
            focus_topic: Optional topic string from manual ``/compress <focus>``.
                Engines that support guided compression should prioritise
                preserving information related to this topic.  Engines that
                don't support it may simply ignore this argument.
            force: Whether a user-requested compression should bypass an
                engine-owned cooldown. Engines without cooldowns may ignore it.
            memory_context: Text returned by memory providers immediately before
                compaction. Summarizing engines should include non-empty text in
                their handoff prompt. Older engines may omit this parameter; the
                host filters unsupported optional arguments by signature.
        Nr    )r+   rC   rD   rE   r   r
   s         r   compresszContextEngine.compress   r-   r   c                 
    |dfS )u  Deterministically trim old tool-result payloads without an LLM call.

        Runs on a low, cost-oriented trigger independent of ``should_compress``
        so large-window engines can reclaim re-sent tool output long before full
        compaction would fire. Returns ``(messages, n_pruned)``.

        Default is a safe no-op: the list is returned unchanged with ``0``
        pruned. Engines that don't implement a cheap prune — and any engine that
        predates this hook — inherit this default, so the agent loop's
        post-tool-call prune path never raises ``AttributeError`` on them. The
        built-in ContextCompressor overrides this with the real implementation.
        r   r    )r+   rC   rD   s      r   prune_tool_results_onlyz%ContextEngine.prune_tool_results_only   s    " {r   )conversation_messagesincoming_messagebudget_tokensrequest_messagesrJ   rK   rL   c                    dS )u  Optionally choose/replace the context for THIS request, pre-generation.

        Called every turn after the request message list is assembled and
        before it is dispatched to the provider — independent of
        ``should_compress()``. This lets an engine *select* which context
        enters the prompt (retrieval, topic routing, role/branch switching)
        rather than *shrink* context that is already there. The two verbs are
        orthogonal:

          - ``compress()``      : context is too long  -> make it shorter.
          - ``select_context()``: this turn belongs to a different context
                                  -> use that one instead.

        Without this hook, engines that need per-turn access to the message
        list have to force ``should_compress()`` to return ``True`` so that
        ``compress()`` is invoked every turn purely as a callback — which
        conflates selection with compression and degrades behaviour when the
        engine's backend is unavailable. ``select_context()`` removes the need
        for that workaround.

        The returned list is request-only: it replaces the messages sent to
        the provider for this single call and MUST NOT be treated as persisted
        transcript state. The conversation history in the session DB is left
        untouched, so nothing leaks across turns. Return ``None`` to leave the
        request unchanged.

        Unlike the ``pre_llm_call`` plugin hook (which appends to the user
        message and intentionally never rewrites the list, to preserve the
        cache prefix), ``select_context()`` may *replace* the message list.

        Ordering / cache contract: the host runs this hook **before** prompt
        cache-control and **before** every request sanitizer (orphaned-tool
        cleanup, thinking-only/role normalization, whitespace/JSON
        normalization). So (a) whatever the hook returns still passes through
        the same validation as any request — a malformed replacement cannot
        reach the provider — and (b) prompt-cache stability (an AGENTS.md
        invariant) is preserved: the default no-op leaves the request
        byte-identical, so cache behaviour is unchanged for the built-in
        compressor and any non-implementing engine. An engine that *does*
        replace the list changes its own cache prefix by definition; that is
        the engine's concern, and cache-control breakpoints are re-derived on
        the selected list. The hook is evaluated per provider request (so it
        re-runs on retries within a turn), consistent with "select the context
        for THIS request".

        Args:
            request_messages: The assembled request message list (system
                prompt + history + any ephemeral prefill), in OpenAI format.
            conversation_messages: The unmodified persisted conversation
                history, for reference only (do not mutate).
            incoming_message: The current turn's user message, if available.
            budget_tokens: The active model's context length, or 0 if unknown.

        Default returns ``None`` (no-op) — zero impact on the built-in
        compressor or any existing engine.
        Nr    )r+   rM   rJ   rK   rL   s        r   select_contextzContextEngine.select_context   s
    @ tr   kwargsc                     dS )uS	  Observe a finished user turn (post-turn ingestion / observation).

        Called from the standard turn-finalization path once the assistant/tool
        loop completes, with the finalized in-memory transcript snapshot. This
        is the complement to ``select_context()``: selection happens *before*
        the request, while observation happens *after* the turn. It lets an
        engine ingest, index, summarize, or update routing / topic / session
        state from what actually happened — so the next ``select_context()``
        can act on it.

        Coverage: this fires from the normal finalization seam. Some abnormal
        early-return paths in the loop (e.g. a content-policy block or a
        provider terminal failure) persist and return without routing through
        finalization, and therefore do not currently emit this hook. Treat it
        as a best-effort post-turn observation for completed turns, not a
        guaranteed callback for every possible early exit; unifying all
        terminal paths behind one finalization seam is a separate follow-up.

        Together the two hooks remove the need to abuse ``should_compress()`` /
        ``compress()`` as a generic per-turn callback just to observe history,
        and they cover the case where a turn finishes and there may be no next
        request from which to infer the previous turn.

        ``messages`` is a shallow copy and should be treated as read-only:
        return values are ignored and this hook must not rely on transcript
        mutation for persistence. ``kwargs`` may include ``turn_id``,
        ``task_id``, ``api_call_count``, ``interrupted``, ``failed``, and
        ``turn_exit_reason``.

        ``usage`` carries the completed turn's canonical token usage (the same
        dict shape passed to ``update_from_response`` — ``prompt_tokens`` /
        ``completion_tokens`` / ``total_tokens`` plus the canonical
        ``input_tokens`` / ``output_tokens`` / ``cache_read_tokens`` /
        ``cache_write_tokens`` / ``reasoning_tokens`` buckets) so an engine can
        weigh how large/expensive the selected context actually was when
        deciding the next ``select_context()``. It is ``None`` on finalized
        turns that never reached a provider response (e.g. interrupt); engines
        must treat it as optional.

        Default is a no-op.
        Nr    )r+   rC   r9   rP   s       r   on_turn_completezContextEngine.on_turn_complete  s
    ^ tr   c                     dS )zQuick rough check before the API call (no real token count yet).

        Default returns False (skip pre-flight). Override if your engine
        can do a cheap estimate.
        Fr    r+   rC   s     r   should_compress_preflightz'ContextEngine.should_compress_preflightL  s	     ur   rough_tokensc                     dS )a  Return True when preflight should trust recent real usage instead.

        Built-in compression uses this to avoid re-compacting from known-noisy
        rough estimates after a compressed request has already fit. Third-party
        engines can ignore it safely.
        Fr    )r+   rV   s     r   $should_defer_preflight_to_real_usagez2ContextEngine.should_defer_preflight_to_real_usageT  s	     ur   r   r   r   c                    | j         sdS |S )a  Return user-visible status for automatic host-triggered compaction.

        Return ``None`` to suppress successful automatic lifecycle status for
        this compaction event. ``phase`` identifies the host call site (for
        example ``"preflight"`` or ``"compress"``). ``context`` contains
        best-effort fields such as ``approx_tokens`` and ``threshold_tokens``.

        This hook does not control warning/error messages or explicit manual
        commands such as ``/compress``.
        N)r   )r+   r   r   r   s       r   r   z5ContextEngine.get_automatic_compaction_status_message]  s    " 4 	4r   c                     dS )u  Quick check: is there anything in ``messages`` that can be compacted?

        Used by the gateway ``/compress`` command as a preflight guard —
        returning False lets the gateway report "nothing to compress yet"
        without making an LLM call.

        Default returns True (always attempt).  Engines with a cheap way
        to introspect their own head/tail boundaries should override this
        to return False when the transcript is still entirely protected.
        Tr    rT   s     r   has_content_to_compressz%ContextEngine.has_content_to_compresst  s	     tr   
session_idc                     dS )zCalled when a new conversation session begins.

        Use this to load persisted state (DAG, store) for the session.
        kwargs may include hermes_home, platform, model, etc.
        Nr    )r+   r\   rP   s      r   on_session_startzContextEngine.on_session_start  r-   r   c                     dS )u   Called at real session boundaries (CLI exit, /reset, gateway expiry).

        Use this to flush state, close DB connections, etc.
        NOT called per-turn — only when the session truly ends.
        Nr    )r+   r\   rC   s      r   on_session_endzContextEngine.on_session_end  r-   r   c                 >    d| _         d| _        d| _        d| _        dS )zyCalled on /new or /reset. Reset per-session state.

        Default resets compression_count and token tracking.
        r   N)r.   r/   r0   r3   r*   s    r   on_session_resetzContextEngine.on_session_reset  s*    
 #$&'#!"!"r   c                     g S )zReturn tool schemas this engine provides to the agent.

        Default returns empty list (no tools). LCM would return schemas
        for lcm_grep, lcm_describe, lcm_expand here.
        r    r*   s    r   get_tool_schemaszContextEngine.get_tool_schemas  s	     	r   r,   argsc                 >    ddl }|                    dd| i          S )zHandle a tool call from the agent.

        Only called for tool names returned by get_tool_schemas().
        Must return a JSON string.

        kwargs may include:
          messages: the current in-memory message list (for live ingestion)
        r   NerrorzUnknown context engine tool: )jsondumps)r+   r,   re   rP   rh   s        r   handle_tool_callzContextEngine.handle_tool_call  s-     	zz7$JD$J$JKLLLr   c                     | j         dk    r| j         nd}|| j        | j        | j        rt          d|| j        z  dz            nd| j        dS )zsReturn status dict for display/logging.

        Default returns the standard fields run_agent.py expects.
        r   d   )r.   r1   r2   usage_percentr3   )r.   r1   r2   minr3   )r+   last_prompts     r   
get_statuszContextEngine.get_status  so     261H11L1Ld--RS"- $ 5"1 &.Ct'::S@AAA,-!%!7	
 	
 		
r   modelbase_urlapi_keyproviderapi_modec                     || _         ddlm} t          | d          s| j        | _         ||t          | di           | j                  | _        | j        | _        t          || j        z            | _	        dS )a  Called when the user switches models or on fallback activation.

        Default updates context_length and recalculates threshold_tokens
        from threshold_percent. Override if your engine needs more
        (e.g. recalculate DAG budgets, switch summary models).
        r   )resolve_model_threshold_config_threshold_percentmodel_thresholdsN)
r2   agent.context_compressorrw   hasattrr4   rx   r!   _base_threshold_percentintr1   )r+   rq   r2   rr   rs   rt   ru   rw   s           r   update_modelzContextEngine.update_model  s     -
 	EDDDDDt899 	D .2-CD*'>'>74!3R88*(
 (
$ "&!= #NT5K$K L Lr   )N)NNFrB   )r   N)rB   rB   rB   rB   )-__name__
__module____qualname____doc__propertyr   r#   r,   r.   r}   __annotations__r/   r0   r1   r2   r3   r4   floatr6   r8   r   boolr   r   r;   r?   rA   r   r   rG   tuplerI   rO   rR   rU   rX   r   r[   r^   r`   rb   rd   rj   rp   r~   r    r   r   r(   r(   Y   s        88 ;c ; ; ; ^ X;  "#C###scNCs  $u###OSNC .2$d111 	$sCx. 	T 	 	 	 ^	 ? ?S ?D ? ? ? ^?9 9# 9AZ 9 9 9 9  )-%)  tCH~& ! c]	
   
d38n	   ^D &* tCH~& d
 
tDcN#S(	)	   2 7;+/@ @ @tCH~.@  $DcN3	@
 sCx.@ @ 
d38n	@ @ @ @J !%/ /tCH~&/ CH~/ 	/
 
/ / / /f$tCH~2F 4           	
  
t   .T#s(^0D     3 T     T#s(^8L QU    # # # #$tCH~"6    
MS 
MS#X 
MS 
M 
M 
M 
M
DcN 
 
 
 
6 M MM M 	M
 M M M 
M M M M M Mr   r(   )r   abcr   r   typingr   r   r   r   agent.redactr	   r   r   r   r   r#   r   r&   r(   r    r   r   <module>r      s,   6 $ # # # # # # # , , , , , , , , , , , , . . . . . . ! " " $S !C C       	
  	4Z   BPM PM PM PM PMC PM PM PM PM PMr   