Class: Clacky::GoalManager
- Inherits:
-
Object
- Object
- Clacky::GoalManager
- Defined in:
- lib/clacky/agent/goal_manager.rb
Overview
Drives the /goal Ralph-style loop for a single session.
Responsibilities:
- own the GoalState lifecycle (set / pause / resume / clear)
- after each agent turn, ask a lightweight judge whether the goal is
done, then decide whether to feed the loop another turn
- enforce the turn budget and auto-pause on repeated judge failures
The judge is a single stateless LLM call routed through the same Client the agent already holds (optionally on a cheaper model name). It never touches conversation history — it only sees the goal text and the agent's last response.
Constant Summary collapse
- MAX_CONSECUTIVE_JUDGE_FAILURES =
After this many consecutive judge-output parse failures the loop auto-pauses instead of burning the whole turn budget on a judge model that cannot follow the JSON reply contract.
3- JUDGE_MAX_TOKENS =
512- JUDGE_SYSTEM_PROMPT =
<<~PROMPT.freeze You are a strict judge deciding whether an autonomous agent has achieved a user's stated goal. You receive the goal text and the agent's most recent response. Decide one verdict: - "done": the goal is fully satisfied, OR the agent clearly explains the goal is unachievable / blocked / needs user input (treat a genuine block as done). - "continue": not done, and there is a concrete next step the agent can take now. This is the default when in doubt. Reply with ONLY a single-line JSON object, no prose, no code fences: {"verdict": "done", "reason": "<one short sentence>"} {"verdict": "continue", "reason": "<one short sentence>"} PROMPT
- CONTINUATION_PROMPT_TEMPLATE =
<<~PROMPT.freeze [Continuing toward your standing goal] Goal: %<goal>s 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. PROMPT
Instance Attribute Summary collapse
-
#state ⇒ Object
readonly
Returns the value of attribute state.
Instance Method Summary collapse
- #active? ⇒ Boolean
- #clear ⇒ Object
- #continuation_prompt ⇒ Object
-
#evaluate_after_turn(last_response) ⇒ Hash
Judge the finished turn and decide the loop's next move.
- #has_goal? ⇒ Boolean
-
#initialize(judge_client:, judge_model:, state: nil) ⇒ GoalManager
constructor
A new instance of GoalManager.
- #pause(reason: "user-paused") ⇒ Object
- #resume(reset_budget: true) ⇒ Object
-
#set(goal, max_turns: nil) ⇒ Object
--- lifecycle ----------------------------------------------------------.
-
#status_line ⇒ Object
Human-readable one-liner for /goal status and status bars.
-
#to_h ⇒ Object
--- persistence bridge -------------------------------------------------.
Constructor Details
#initialize(judge_client:, judge_model:, state: nil) ⇒ GoalManager
Returns a new instance of GoalManager.
52 53 54 55 56 57 |
# File 'lib/clacky/agent/goal_manager.rb', line 52 def initialize(judge_client:, judge_model:, state: nil) @judge_client = judge_client @judge_model = judge_model @state = state @consecutive_judge_failures = 0 end |
Instance Attribute Details
#state ⇒ Object (readonly)
Returns the value of attribute state.
50 51 52 |
# File 'lib/clacky/agent/goal_manager.rb', line 50 def state @state end |
Instance Method Details
#active? ⇒ Boolean
59 60 61 |
# File 'lib/clacky/agent/goal_manager.rb', line 59 def active? @state&.active? || false end |
#clear ⇒ Object
97 98 99 |
# File 'lib/clacky/agent/goal_manager.rb', line 97 def clear @state = nil end |
#continuation_prompt ⇒ Object
224 225 226 |
# File 'lib/clacky/agent/goal_manager.rb', line 224 def continuation_prompt format(CONTINUATION_PROMPT_TEMPLATE, goal: @state.goal) end |
#evaluate_after_turn(last_response) ⇒ Hash
Judge the finished turn and decide the loop's next move.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/clacky/agent/goal_manager.rb', line 134 def evaluate_after_turn(last_response) return inactive_decision unless @state&.active? @state.turns_used += 1 @state.last_turn_at = Time.now.to_f last_response = last_response.to_s if last_response.strip.empty? # Nothing to judge — treat as a transient blip, keep going if budget allows. return continue_or_pause("empty response (nothing to evaluate)") end verdict, reason = judge(@state.goal, last_response) @state.last_verdict = verdict @state.last_reason = reason if verdict.nil? @consecutive_judge_failures += 1 if @consecutive_judge_failures >= MAX_CONSECUTIVE_JUDGE_FAILURES return pause_decision( "judge returned unusable output #{@consecutive_judge_failures} turns in a row" ) end return continue_or_pause("judge output unusable — continuing") end @consecutive_judge_failures = 0 if verdict == "done" @state.status = "done" return { should_continue: false, continuation_prompt: nil, status: "done", verdict: "done", reason: reason, message: "✓ Goal achieved: #{reason}" } end continue_or_pause(reason) end |
#has_goal? ⇒ Boolean
63 64 65 |
# File 'lib/clacky/agent/goal_manager.rb', line 63 def has_goal? @state && (@state.active? || @state.paused?) end |
#pause(reason: "user-paused") ⇒ Object
81 82 83 84 85 86 |
# File 'lib/clacky/agent/goal_manager.rb', line 81 def pause(reason: "user-paused") return nil unless @state @state.status = "paused" @state.paused_reason = reason @state end |
#resume(reset_budget: true) ⇒ Object
88 89 90 91 92 93 94 95 |
# File 'lib/clacky/agent/goal_manager.rb', line 88 def resume(reset_budget: true) return nil unless @state @state.status = "active" @state.paused_reason = nil @state.turns_used = 0 if reset_budget @consecutive_judge_failures = 0 @state end |
#set(goal, max_turns: nil) ⇒ Object
--- lifecycle ----------------------------------------------------------
69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/clacky/agent/goal_manager.rb', line 69 def set(goal, max_turns: nil) goal = goal.to_s.strip raise ArgumentError, "goal text is empty" if goal.empty? @state = GoalState.new( goal: goal, max_turns: max_turns || GoalState::DEFAULT_MAX_TURNS ) @consecutive_judge_failures = 0 @state end |
#status_line ⇒ Object
Human-readable one-liner for /goal status and status bars.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/clacky/agent/goal_manager.rb', line 102 def status_line return "No active goal. Set one with /goal <text>." unless @state s = @state = "#{s.turns_used}/#{s.max_turns} turns" case s.status when "active" "⊙ Goal (active, #{}): #{s.goal}" when "paused" extra = s.paused_reason ? " — #{s.paused_reason}" : "" "⏸ Goal (paused, #{}#{extra}): #{s.goal}" when "done" "✓ Goal done (#{}): #{s.goal}" else "Goal (#{s.status}, #{}): #{s.goal}" end end |
#to_h ⇒ Object
--- persistence bridge -------------------------------------------------
179 180 181 |
# File 'lib/clacky/agent/goal_manager.rb', line 179 def to_h @state&.to_h end |