
    (Gje1                    j    d Z ddlmZ ddlmZmZmZmZmZ dZ	dZ
d d
Zd!dZd"dZd#dZd$dZd%dZdS )&u  Boundary-aware partial compression — "summarize up to here".

Inspired by Claude Code's Rewind menu "Summarize up to here" action
(v2.1.139–v2.1.142, Week 20, May 2026):
https://code.claude.com/docs/en/whats-new/2026-w20

Hermes already has ``/compress`` (full-history compaction) and an
automatic token-budget tail-protection heuristic inside
``ContextCompressor``. What was missing is *user-chosen* boundary
control: "fold everything before this point into a summary, but keep
my most recent N exchanges exactly as they are." That is the value of
the Claude Code feature — the user decides the compression boundary
instead of leaving it to the token-budget heuristic.

This module owns the pure, side-effect-free split logic so both the
CLI (``cli.py::_manual_compress``) and the gateway
(``gateway/run.py::_handle_compress_command``) share one
implementation. The slash-command surfaces handle compression of the
*head* via the existing ``_compress_context`` pipeline (preserving all
the session-rotation / lock / memory-notify machinery) and then
re-append the verbatim *tail* returned here.

Design notes / invariants honored:

* **Role alternation.** The compressed head ends with summary/handoff
  content (assistant- or user-role, possibly a trailing todo snapshot).
  The verbatim tail must begin with a ``user`` message so the rejoined
  history keeps the user↔assistant alternation that providers validate.
  :func:`split_history_for_partial_compress` snaps the tail boundary
  backwards to the nearest ``user`` turn so the rejoin is always legal.

* **No silent context mutation.** This is a manual, user-invoked
  action. It rotates the session exactly like ``/compress`` does (via
  the caller), so the prompt-cache reset is explicit and expected, not
  silent.

* **Conservative defaults.** ``keep_last`` counts *exchanges* (a user
  turn plus its following assistant/tool turns), defaulting to 2. The
  split never compresses if doing so would leave nothing in the head.
    )annotations)AnyDictListOptionalTuple   d   raw_argsstrreturnTuple[bool, int, Optional[str]]c                   | pd                                 }|s
dt          dfS |                                }|                    d          r.|t	          d          d         }|t	          d          d         }|                                }|r@|d         dk    r4t          }t	          |          dk    rt          |d	                   }d
|dfS |r5|d         dv r+t	          |          dk    rd
t          |d	                   dfS |rM|d                             d          r2d
t          |d                             dd	          d	                   dfS dt          |pdfS )u  Parse the argument string after ``/compress``.

    Recognizes the boundary-aware forms:

    * ``here``            → partial compress, keep ``DEFAULT_KEEP_LAST``
    * ``here 4``          → partial compress, keep 4 exchanges
    * ``--keep 4``        → partial compress, keep 4 exchanges
    * ``up to here``      → alias for ``here`` (matches Claude Code's
                            menu label "Summarize up to here")

    Anything else is treated as a focus topic for the existing full
    ``/compress <focus>`` behavior.

    Returns ``(partial, keep_last, focus_topic)``:

    * ``partial`` — True when a boundary-aware form was requested.
    * ``keep_last`` — exchanges to preserve verbatim (only meaningful
      when ``partial`` is True).
    * ``focus_topic`` — focus string for full compression, or None.
      Always None when ``partial`` is True (the two modes are exclusive;
      a focused partial compress is not a documented Claude Code
      behavior and would muddy the UX).
     FNz
up to herezup to r   herer	      T)z--keepz-kz--keep==)stripDEFAULT_KEEP_LASTlower
startswithlensplit_coerce_keep)r   textloweredtokenskeeps        A/home/rurouni/.hermes/hermes-agent/hermes_cli/partial_compress.pyparse_partial_compress_argsr    7   sz   4 N!!##D .'--jjllG ,'' %#h--//*CMMOO$]]__F   &)v%% v;;!q	**DT4  3&)///CKK14D4D\&),,d22 D&)&&y11 D\&)//#q"9"9!"<==tCC #T\T11    Tuple[str, bool, bool]c                    d}d}g }| pd                                 D ];}|                                }|dv rd}|dk    rd}&|                    |           <d                    |          ||fS )u  Strip ``--preview``/``--dry-run``/``--aggressive`` flags from the
    argument string after ``/compress`` (or its ``/compact`` alias).

    Flags may appear anywhere and coexist with the positional forms
    (``here [N]``, ``--keep N``, or a focus topic); the returned
    remainder is what :func:`parse_partial_compress_args` should see.

    Returns ``(remaining_args, preview, aggressive_requested)``:

    * ``preview`` — True when ``--preview`` or ``--dry-run`` was given.
      The caller must report what WOULD be compressed (message counts,
      token estimate, boundary) and make **no changes**.
    * ``aggressive_requested`` — True when ``--aggressive`` was given.
      The current surfaces do not implement an LLM-free hard-truncate
      path (it would need its own transcript-persistence branch outside
      the guarded ``_compress_context`` rotation machinery), so callers
      surface a "not supported" note instead of silently treating the
      flag as a focus topic.
    Fr   )z	--previewz	--dry-runz--dryrunTz--aggressive )r   r   appendjoin)r   preview
aggressivekepttoklows         r   extract_compress_flagsr,   o   s    ( GJDB%%''  iikk888GGN""JJKK88D>>7J..r!   historyList[Dict[str, Any]]partialbool	keep_lastintfocus_topicOptional[str]approx_tokensDict[str, Any]c                   t          |           }t          |           }g }|}|r(t          | |          \  }}|sd}t          |           g }}ddt          |           d| d|ddg}	|r*|	                    d| d	t          |           d
           n|r|	                    d           |r|	                    d| d           |	                    d           t          |          t          |          |||	dS )u  Build the ``/compress --preview`` report — pure, no side effects.

    Shared by the CLI (``cli.py::_manual_compress``) and the gateway
    (``gateway/slash_commands.py::_handle_compress_command``) so both
    surfaces report the same numbers the real run would use.

    Returns a dict with ``head_count``/``tail_count``/``lines`` where
    ``lines`` is a ready-to-print list of report strings.
    Fu   Preview — no changes made.zWould compress z of z message(s) (~,z tokens currently in context).zBoundary: keeping the last z exchange(s) (z message(s)) verbatim.uR   Boundary: 'here' split would keep everything — falling back to full compression.zFocus topic: ""z1Run the command again without --preview to apply.)
head_count
tail_counttotalr/   lines)r   list"split_history_for_partial_compressr%   )
r-   r/   r1   r3   r5   r<   headtaileffective_partialr=   s
             r   summarize_compress_previewrC      s{     LLE==D!#D +7KK
d 	+ %g$D 	'	=#d)) 	= 	= 	= 	=<	= 	= 	=E
  	
2) 2 2D		2 2 2	
 	
 	
 	
 
 
0	
 	
 	
  64k444555	LLDEEE $ii$ii$  r!   valuec                    	 t          |           }n# t          t          f$ r
 t          cY S w xY w|dk     rdS |t          k    rt          S |S )z9Parse a keep-count token, clamping to [1, MAX_KEEP_LAST].r   )r2   	TypeError
ValueErrorr   MAX_KEEP_LAST)rD   ns     r   r   r      sd    !JJz" ! ! !    !1uuq=Hs    --1Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]c                   |dk     rd}t          |           }|dk    rg g fS g }t          |dz
  dd          D ]K}| |                             d          dk    r*|                    |           t          |          |k    r nL|st	          |           g fS |d         }| d|         }| |d         }|st	          |           g fS ||fS )uO  Split ``history`` into ``(head, tail)`` for partial compression.

    ``head`` is the earlier portion that will be summarized; ``tail`` is
    the most recent ``keep_last`` exchanges, preserved verbatim.

    An *exchange* is counted by ``user``-role messages: keeping N
    exchanges means keeping everything from the Nth-most-recent ``user``
    message onward. This guarantees the tail starts on a ``user`` turn,
    so when the caller rejoins ``compressed_head + tail`` the
    user↔assistant alternation stays valid (the compressed head's
    trailing content is followed by a fresh user turn).

    Returns ``(head, tail)``. If the split would leave the head empty
    (not enough history to compress meaningfully), returns
    ``(history, [])`` — signaling the caller to fall back to full
    compression or report "nothing to do".
    r   r   roleuserN)r   rangegetr%   r>   )r-   r1   rI   user_startsidxboundaryr@   rA   s           r   r?   r?      s   * 1}}	GAAvv2v  KQUB##  3<F##v--s###;9,, ! G}}b  2H9H9D899D
  !G}}b  :r!   compressed_headrA   c                R   |st          |           S | st          |          S t          |           }t          |          }|d         }|d         }|                    d          }|                    d          }||k    r|dv r|                    d          }|                    d          }	t          |t                    r>t          |	t                    r)t	          |          }
| d|	 |
d<   |
|d<   |dd         }n"|d	k    rd
nd	}|                    |dd           ||z   S )u0  Concatenate a compressed head with the verbatim tail, defending
    the seam against an illegal user→user / assistant→assistant adjacency.

    In normal operation the compressed head ends with the head's own
    protected verbatim tail (the ``ContextCompressor`` always preserves a
    recent window), which terminates on an ``assistant``/``tool`` turn —
    so ``assistant → user`` at the seam is already valid. But the head
    compressor's exact output shape is not contractually guaranteed (a
    plugin context engine could return something that ends on a ``user``
    turn, or a degenerate single-summary message). Rather than trust the
    seam, this helper inspects the boundary and, if the last head message
    and the first tail message share a ``user``/``assistant`` role, folds
    the tail's first message content onto the head's last message so the
    rejoined list never violates provider role-alternation rules.

    ``tool`` messages are left alone — consecutive ``tool`` entries are
    the one legal repetition (parallel tool results).
    rL   r   rM   )rN   	assistantcontentz

r   NrN   rV   r   )rM   rW   )r>   rP   
isinstancer   dictr%   )rT   rA   r@   restlastfirst	last_role
first_rolelast_contentfirst_contentmergedbridge_roles               r   rejoin_compressed_head_and_tailrc     sK   ,  %O$$$ Dzz  D::D8DGE  I6""JJ90E#E#E xx	**		),,lC(( 
	>Zs-K-K 
	>$ZZF#/ D D] D DF9DH8DD
 *4v)=)=++6KKK<<===$;r!   N)r   r   r   r   )r   r   r   r"   )r-   r.   r/   r0   r1   r2   r3   r4   r5   r2   r   r6   )rD   r   r   r2   )r-   r.   r1   r2   r   rJ   )rT   r.   rA   r.   r   r.   )__doc__
__future__r   typingr   r   r   r   r   r   rH   r    r,   rC   r   r?   rc    r!   r   <module>rh      s   ' 'R # " " " " " 3 3 3 3 3 3 3 3 3 3 3 3 3 3   52 52 52 52p/ / / /D4 4 4 4n
 
 
 
5 5 5 5p7 7 7 7 7 7r!   