
    iM                    d   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m	Z	 ddl
mZmZmZmZ  ej        e          ZdZdZdZd	Zd
ZdZe G d d                      Zd.dZi Zded<   d/dZd0dZd1dZd2dZd3dZ  ej!        d ej"                  Z#d4d#Z$ed$d5d*Z% G d+ d,          Z&g d-Z'dS )6un  Persistent session goals — the Ralph loop for Hermes.

A goal is a free-form user objective that stays active across turns. After
each turn completes, a small judge call asks an auxiliary model "is this
goal satisfied by the assistant's last response?". If not, Hermes feeds a
continuation prompt back into the same session and keeps working until the
goal is done, turn budget is exhausted, the user pauses/clears it, or the
user sends a new message (which takes priority and pauses the goal loop).

State is persisted in SessionDB's ``state_meta`` table keyed by
``goal:<session_id>`` so ``/resume`` picks it up.

Design notes / invariants:

- The continuation prompt is just a normal user message appended to the
  session via ``run_conversation``. No system-prompt mutation, no toolset
  swap — prompt caching stays intact.
- Judge failures are fail-OPEN: ``continue``. A broken judge must not wedge
  progress; the turn budget is the backstop.
- When a real user message arrives mid-loop it preempts the continuation
  prompt and also pauses the goal loop for that turn (we still re-judge
  after, so if the user's message happens to complete the goal the judge
  will say ``done``).
- This module has zero hard dependency on ``cli.HermesCLI`` or the gateway
  runner — both wire the same ``GoalManager`` in.

Nothing in this module touches the agent's system prompt or toolset.
    )annotationsN)	dataclassasdict)AnyDictOptionalTuple   g      >@i  a  [Continuing toward your standing goal]
Goal: {goal}

Continue working toward this goal. Take the next concrete step. If you believe the goal is complete, state so explicitly and stop. If you are blocked and need input from the user, say so clearly and stop.u  You are a strict judge evaluating whether an autonomous agent has achieved a user's stated goal. You receive the goal text and the agent's most recent response. Your only job is to decide whether the goal is fully satisfied based on that response.

A goal is DONE only when:
- The response explicitly confirms the goal was completed, OR
- The response clearly shows the final deliverable was produced, OR
- The response explains the goal is unachievable / blocked / needs user input (treat this as DONE with reason describing the block).

Otherwise the goal is NOT done — CONTINUE.

Reply ONLY with a single JSON object on one line:
{"done": <true|false>, "reason": "<one-sentence rationale>"}zNGoal:
{goal}

Agent's most recent response:
{response}

Is the goal satisfied?c                      e Zd ZU dZded<   dZded<   dZded<   eZded	<   d
Z	ded<   d
Z
ded<   dZded<   dZded<   dZded<   ddZedd            ZdS )	GoalStatez+Serializable goal state stored per session.strgoalactivestatusr   int
turns_used	max_turns        float
created_atlast_turn_atNOptional[str]last_verdictlast_reasonpaused_reasonreturnc                H    t          j        t          |           d          S )NF)ensure_ascii)jsondumpsr   selfs    5/home/ubuntu/.hermes/hermes-agent/hermes_cli/goals.pyto_jsonzGoalState.to_jsong   s    z&,,U;;;;    raw'GoalState'c                @   t          j        |          } | |                    dd          |                    dd          t          |                    dd          pd          t          |                    dt                    pt                    t          |                    dd	          pd	          t          |                    d
d	          pd	          |                    d          |                    d          |                    d          	  	        S )Nr    r   r   r   r   r   r   r   r   r   r   r   )	r   r   r   r   r   r   r   r   r   )r   loadsgetr   DEFAULT_MAX_TURNSr   )clsr&   datas      r#   	from_jsonzGoalState.from_jsonj   s    z#s&"%%88Hh//488L!449::$((;0ABBWFWXXTXXlC88?C@@txx<<CDD.11//((?33

 

 

 
	
r%   r   r   )r&   r   r   r'   )__name__
__module____qualname____doc____annotations__r   r   r,   r   r   r   r   r   r   r$   classmethodr/    r%   r#   r   r   Y   s         55IIIFJ&I&&&&JL"&L&&&&!%K%%%%#'M''''< < < < 
 
 
 [
 
 
r%   r   
session_idr   r   c                    d|  S )Nzgoal:r7   )r8   s    r#   	_meta_keyr:      s    :r%   Dict[str, Any]	_DB_CACHEOptional[Any]c                    	 ddl m}  ddlm} t	           |                       }n3# t
          $ r&}t                              d|           Y d}~dS d}~ww xY wt          	                    |          }||S 	  |            }n3# t
          $ r&}t                              d|           Y d}~dS d}~ww xY w|t          |<   |S )a  Return a SessionDB instance for the current HERMES_HOME.

    SessionDB has no built-in singleton, but opening a new connection per
    /goal call would thrash the file. We cache one instance per
    ``hermes_home`` path so profile switches still pick up the right DB.
    Defensive against import/instantiation failures so tests and
    non-standard launchers can still use the GoalManager.
    r   )get_hermes_home)	SessionDBz,GoalManager: SessionDB bootstrap failed (%s)Nz$GoalManager: SessionDB() raised (%s))
hermes_constantsr?   hermes_stater@   r   	Exceptionloggerdebugr<   r+   )r?   r@   homeexccacheddbs         r#   _get_session_dbrJ      s   444444******??$$%%   CSIIIttttt ]]4  FY[[   ;SAAAttttt IdOIs,   #& 
AAA8
B 
B3B..B3Optional[GoalState]c                   | sdS t                      }|dS 	 |                    t          |                     }n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w|sdS 	 t                              |          S # t          $ r'}t                              d| |           Y d}~dS d}~ww xY w)z4Load the goal for a session, or None if none exists.Nz GoalManager: get_meta failed: %sz3GoalManager: could not parse stored goal for %s: %s)	rJ   get_metar:   rC   rD   rE   r   r/   warning)r8   rI   r&   rG   s       r#   	load_goalrO      s     t			B	ztkk)J//00   7===ttttt  t""3'''   LjZ]^^^ttttts-   "; 
A+A&&A+3B 
B>B99B>stateNonec                   | sdS t                      }|dS 	 |                    t          |           |                                           dS # t          $ r&}t
                              d|           Y d}~dS d}~ww xY w)z5Persist a goal to SessionDB. No-op if DB unavailable.Nz GoalManager: set_meta failed: %s)rJ   set_metar:   r$   rC   rD   rE   )r8   rP   rI   rG   s       r#   	save_goalrT      s     			B	z>
Ij))5==??;;;;; > > >7=========>s   5A 
A?A::A?c                Z    t          |           }|dS d|_        t          | |           dS )zDMark a goal cleared in the DB (preserved for audit, status=cleared).Ncleared)rO   r   rT   )r8   rP   s     r#   
clear_goalrW      s6    j!!E}ELj%     r%   textlimitr   c                N    | sdS t          |           |k    r| S | d |         dz   S )Nr)   u   … [truncated])len)rX   rY   s     r#   	_truncater\      s9     r
4yyE<+++r%   z\{.*?\}r&   Tuple[bool, str]c                h   | sdS |                                  }|                    d          r=|                     d          }|                    d          }|dk    r||dz   d         }d}	 t          j        |          }ng# t
          $ rZ t                              |          }|r;	 t          j        |                    d                    }n# t
          $ r d}Y nw xY wY nw xY wt          |t                    sd	d
t          | d          fS |                    d          }t          |t                    r)|                                                                 dv }nt          |          }t          |                    d          pd                                           }|sd}||fS )zdParse the judge's reply. Fail-open to ``(False, "<reason>")``.

    Returns ``(done, reason)``.
    )Fzjudge returned empty responsez````
   Nr   Fzjudge reply was not JSON:    done)trueyes1rd   reasonr)   zno reason provided)strip
startswithfindr   r*   rC   _JSON_OBJECT_REsearchgroup
isinstancedictr\   r+   r   lowerbool)r&   rX   nlr.   matchdone_valrd   rh   s           r#   _parse_judge_responserv      s   
  65599;;D u !zz#YYt__88Q=D &*D	z$   &&t,, 	z%++a..11    dD!! KJ9S#3F3FJJJJxxH(C   ~~%%''+GGH~~(##)r**0022F &%<s6   .B &C'*'CC'C!C' C!!C'&C')timeoutr   last_responserw   r   Tuple[str, str]c                  |                                  sdS |                                 sdS 	 ddlm} n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w	  |d          \  }}n3# t          $ r&}t                              d	|           Y d}~dS d}~ww xY w||sd
S t                              t          | d          t          |t                              }	 |j
        j                            |dt          dd|dgdd|          }nK# t          $ r>}t                              d|           ddt          |          j         fcY d}~S d}~ww xY w	 |j        d         j        j        pd}	n# t          $ r d}	Y nw xY wt)          |	          \  }
}|
rdnd}t                              d|t          |d                     ||fS )uk  Ask the auxiliary model whether the goal is satisfied.

    Returns ``(verdict, reason)`` where verdict is ``"done"``, ``"continue"``,
    or ``"skipped"`` (when the judge couldn't be reached).

    This is deliberately fail-open: any error returns ``("continue", "...")``
    so a broken judge doesn't wedge progress — the turn budget is the
    backstop.
    )skippedz
empty goal)continuez$empty response (nothing to evaluate)r   )get_text_auxiliary_clientz.goal judge: auxiliary client import failed: %sN)r|   zauxiliary client unavailable
goal_judgez0goal judge: get_text_auxiliary_client failed: %s)r|   zno auxiliary client configuredi  )r   responsesystem)rolecontentuserrc   )modelmessagestemperature
max_tokensrw   u@   goal judge: API call failed (%s) — falling through to continuer|   zjudge error: r)   rd   z goal judge: verdict=%s reason=%sx   )ri   agent.auxiliary_clientr}   rC   rD   rE   JUDGE_USER_PROMPT_TEMPLATEformatr\   _JUDGE_RESPONSE_SNIPPET_CHARSchatcompletionscreateJUDGE_SYSTEM_PROMPTinfotyper1   choicesmessager   rv   )r   rx   rw   r}   rG   clientr   promptrespr&   rd   rh   verdicts                r#   
judge_goalr     s    ::<< '&&   BAA:DDDDDDD : : :EsKKK999999::11,?? : : :GMMM999999: ~U~;;'..tT""=*GHH /  F
@{&--!.ABBF33  . 	
 	
  @ @ @VX[\\\?499+=?????????@l1o%-3    )--LD&,ff*G
KK2GYvs=S=STTTF?s\   5 
A%A  A%)A8 8
B(B##B(/1D! !
E)+3E$E)$E)-F FFc                      e Zd ZdZedd'dZed(d
            Zd)dZd)dZ	d*dZ
ddd+dZd,d-dZddd.dZd/dZd0dZdd d1d$Zd2d&ZdS )3GoalManageruf  Per-session goal state + continuation decisions.

    The CLI and gateway each hold one ``GoalManager`` per live session.

    Methods:

    - ``set(goal)`` — start a new standing goal.
    - ``clear()`` — remove the active goal.
    - ``pause()`` / ``resume()`` — explicit user controls.
    - ``status()`` — printable one-liner.
    - ``evaluate_after_turn(last_response)`` — call the judge, update state,
      and return a decision dict the caller uses to drive the next turn.
    - ``next_continuation_prompt()`` — the canonical user-role message to
      feed back into ``run_conversation``.
    )default_max_turnsr8   r   r   r   c               r    || _         t          |pt                    | _        t	          |          | _        d S N)r8   r   r,   r   rO   _state)r"   r8   r   s      r#   __init__zGoalManager.__init__e  s3    $!$%6%K:K!L!L+4Z+@+@r%   r   rK   c                    | j         S r   )r   r!   s    r#   rP   zGoalManager.statel  s
    {r%   rr   c                4    | j         d uo| j         j        dk    S )Nr   r   r   r!   s    r#   	is_activezGoalManager.is_activep  s    {$&I4;+=+IIr%   c                0    | j         d uo| j         j        dv S )N)r   pausedr   r!   s    r#   has_goalzGoalManager.has_goals  s    {$&U4;+=AU+UUr%   c                2   | j         }|	|j        dv rdS |j         d|j         d}|j        dk    rd| d|j         S |j        dk    r"|j        r
d	|j         nd
}d| | d|j         S |j        dk    rd| d|j         S d|j         d| d|j         S )N)rV   z*No active goal. Set one with /goal <text>./z turnsr   u   ⊙ Goal (active, ): r   u    — r)   u   ⏸ Goal (paused, rd   u   ✓ Goal done (zGoal (z, )r   r   r   r   r   r   )r"   sturnsextras       r#   status_linezGoalManager.status_linev  s    K9L00??<55!+5558x:::!&:::8x12H-AO---bEAAuAAAAA8v7U77qv777666E66af666r%   N)r   r   r   Optional[int]r   c                  |pd                                 }|st          d          t          |dd|rt          |          n| j        t          j                    d          }|| _        t          | j        |           |S )Nr)   zgoal text is emptyr   r   r   )r   r   r   r   r   r   )	ri   
ValueErrorr   r   r   timer   rT   r8   )r"   r   r   rP   s       r#   setzGoalManager.set  s    
!!## 	31222(1Mc)nnnt7My{{
 
 
 $/5)))r%   user-pausedrh   c                    | j         sd S d| j         _        || j         _        t          | j        | j                    | j         S )Nr   )r   r   r   rT   r8   r"   rh   s     r#   pausezGoalManager.pause  sA    { 	4%$*!$/4;///{r%   T)reset_budgetr   c                   | j         sd S d| j         _        d | j         _        |rd| j         _        t	          | j        | j                    | j         S )Nr   r   )r   r   r   r   rT   r8   )r"   r   s     r#   resumezGoalManager.resume  sS    { 	4%$(! 	'%&DK"$/4;///{r%   rQ   c                r    | j         d S d| j         _        t          | j        | j                    d | _         d S )NrV   )r   r   rT   r8   r!   s    r#   clearzGoalManager.clear  s8    ;F&$/4;///r%   c                    | j         sd S d| j         _        d| j         _        || j         _        t	          | j        | j                    d S )Nrd   )r   r   r   r   rT   r8   r   s     r#   	mark_donezGoalManager.mark_done  sI    { 	F##) "($/4;/////r%   )user_initiatedrx   r   r;   c                  | j         }||j        dk    r|r|j        ndddddddS |xj        dz  c_        t          j                    |_        t          |j        |          \  }}||_        ||_        |d	k    r(d	|_        t          | j
        |           d	ddd	|d
| dS |j        |j        k    rNd|_        d|j         d|j         d|_        t          | j
        |           dddd|d|j         d|j         ddS t          | j
        |           dd|                                 d|d|j         d|j         d| dS )uv  Run the judge and update state. Return a decision dict.

        ``user_initiated`` distinguishes a real user prompt (True) from a
        continuation prompt we fed ourselves (False). Both increment
        ``turns_used`` because both consume model budget.

        Decision keys:
          - ``status``: current goal status after update
          - ``should_continue``: bool — caller should fire another turn
          - ``continuation_prompt``: str or None
          - ``verdict``: "done" | "continue" | "skipped" | "inactive"
          - ``reason``: str
          - ``message``: user-visible one-liner to print/send
        Nr   Finactivezno active goalr)   )r   should_continuecontinuation_promptr   rh   r   rb   rd   u   ✓ Goal achieved: r   zturn budget exhausted (r   )r|   u   ⏸ Goal paused — zD turns used. Use /goal resume to keep going, or /goal clear to stop.Tu   ↻ Continuing toward goal (r   )r   r   r   r   r   r   r   r   r   rT   r8   r   r   next_continuation_prompt)r"   rx   r   rP   r   rh   s         r#   evaluate_after_turnzGoalManager.evaluate_after_turn  s   ( =ELH44*/9%,,T#('+%*   	A!Y[[$UZ??$"f!ELdou--- #('+! 999   u..#EL"aE<L"a"au"a"a"aEdou---"#('+% N5+; N Neo N N N
 
 
 	$/5)))##'#@#@#B#B!^u/?^^%/^^V\^^	
 	
 		
r%   r   c                ~    | j         r| j         j        dk    rd S t                              | j         j                  S )Nr   )r   )r   r   CONTINUATION_PROMPT_TEMPLATEr   r   r!   s    r#   r   z$GoalManager.next_continuation_prompt  s<    { 	dk0H<<4+228H2IIIr%   )r8   r   r   r   )r   rK   )r   rr   r0   )r   r   r   r   r   r   )r   )rh   r   r   rK   )r   rr   r   rK   )r   rQ   )rh   r   r   rQ   )rx   r   r   rr   r   r;   )r   r   )r1   r2   r3   r4   r,   r   propertyrP   r   r   r   r   r   r   r   r   r   r   r7   r%   r#   r   r   T  sy          EV A A A A A A    XJ J J JV V V V7 7 7 7  <@            .2         0 0 0 0  $	M
 M
 M
 M
 M
 M
^J J J J J Jr%   r   )r   r   r   r,   rO   rT   rW   r   )r8   r   r   r   )r   r=   )r8   r   r   rK   )r8   r   rP   r   r   rQ   )r8   r   r   rQ   )rX   r   rY   r   r   r   )r&   r   r   r]   )r   r   rx   r   rw   r   r   ry   )(r4   
__future__r   r   loggingrer   dataclassesr   r   typingr   r   r   r	   	getLoggerr1   rD   r,   DEFAULT_JUDGE_TIMEOUTr   r   r   r   r   r:   r<   r5   rJ   rO   rT   rW   r\   compileDOTALLrl   rv   r   r   __all__r7   r%   r#   <module>r      s2    : # " " " " "   				  ) ) ) ) ) ) ) ) - - - - - - - - - - - -		8	$	$    $ P I "  
 
 
 
 
 
 
 
J        	       <   *
> 
> 
> 
>! ! ! !, , , , "*Z33* * * *b +	@ @ @ @ @ @PwJ wJ wJ wJ wJ wJ wJ wJt	 	 	r%   