navi_core/goal/
runtime.rs1use crate::goal::types::SessionGoal;
2use std::sync::RwLock;
3
4use super::accounting::GoalAccountingState;
5use super::steering;
6
7pub struct GoalRuntimeHandle {
14 session_id: RwLock<Option<String>>,
16 goal: RwLock<Option<SessionGoal>>,
18 accounting: RwLock<Option<GoalAccountingState>>,
20 auto_continue: RwLock<bool>,
22 state_lock: std::sync::Mutex<()>,
25}
26
27impl GoalRuntimeHandle {
28 pub fn new(initial_goal: Option<SessionGoal>) -> Self {
30 let accounting = initial_goal
32 .as_ref()
33 .map(|g| GoalAccountingState::new(g.clone()));
34 let session_id = initial_goal
35 .as_ref()
36 .and_then(|goal| (!goal.session_id.is_empty()).then(|| goal.session_id.clone()));
37 Self {
38 session_id: RwLock::new(session_id),
39 goal: RwLock::new(initial_goal),
40 accounting: RwLock::new(accounting),
41 auto_continue: RwLock::new(true),
42 state_lock: std::sync::Mutex::new(()),
43 }
44 }
45
46 pub fn get_goal(&self) -> Option<SessionGoal> {
50 self.goal.read().unwrap_or_else(|e| e.into_inner()).clone()
51 }
52
53 pub fn set_session_id(&self, session_id: impl Into<String>) {
55 let session_id = session_id.into();
56 *self.session_id.write().unwrap_or_else(|e| e.into_inner()) = Some(session_id.clone());
57 if let Some(ref mut goal) = *self.goal.write().unwrap_or_else(|e| e.into_inner()) {
58 goal.session_id = session_id;
59 goal.updated_at = crate::session::current_unix_timestamp();
60 }
61 }
62
63 pub fn set_objective(&self, objective: String, token_budget: Option<i64>) -> SessionGoal {
65 self.set_objective_with_short_description(objective, None, token_budget)
66 }
67
68 pub fn set_objective_with_short_description(
70 &self,
71 objective: String,
72 short_description: Option<String>,
73 token_budget: Option<i64>,
74 ) -> SessionGoal {
75 let mut goal_guard = self.goal.write().unwrap_or_else(|e| e.into_inner());
76 if let Some(ref mut goal) = *goal_guard {
77 goal.objective = objective;
78 goal.short_description = short_description;
79 goal.token_budget = token_budget;
80 goal.status = crate::goal::types::GoalStatus::Active;
81 goal.consecutive_blocked_turns = 0;
82 goal.block_reason = None;
83 goal.updated_at = crate::session::current_unix_timestamp();
84 let new_goal = goal.clone();
85 drop(goal_guard);
86 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) =
87 Some(GoalAccountingState::new(new_goal.clone()));
88 new_goal
89 } else {
90 let session_id = self
91 .session_id
92 .read()
93 .unwrap_or_else(|e| e.into_inner())
94 .clone()
95 .unwrap_or_default();
96 let mut new_goal = SessionGoal::new(session_id, objective, token_budget);
97 new_goal.short_description = short_description;
98 let cloned = new_goal.clone();
99 *goal_guard = Some(new_goal);
100 drop(goal_guard);
101 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) =
102 Some(GoalAccountingState::new(cloned.clone()));
103 cloned
104 }
105 }
106
107 pub fn update_goal(&self, mut goal: SessionGoal) {
109 if let Some(session_id) = self
110 .session_id
111 .read()
112 .unwrap_or_else(|e| e.into_inner())
113 .clone()
114 {
115 goal.session_id = session_id;
116 }
117 *self.goal.write().unwrap_or_else(|e| e.into_inner()) = Some(goal.clone());
118 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
119 acct.replace_goal(goal);
120 }
121 }
122
123 pub fn clear_goal(&self) {
125 *self.goal.write().unwrap_or_else(|e| e.into_inner()) = None;
126 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) = None;
127 *self.session_id.write().unwrap_or_else(|e| e.into_inner()) = None;
128 }
129
130 pub fn update_checklist(&self, tasks: Vec<super::types::GoalTask>) -> Option<SessionGoal> {
132 let mut goal = self.goal.write().unwrap_or_else(|e| e.into_inner());
133 if let Some(ref mut g) = *goal {
134 g.set_checklist(tasks);
135 let cloned = g.clone();
136 drop(goal);
137 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
138 acct.replace_goal(cloned.clone());
139 }
140 Some(cloned)
141 } else {
142 None
143 }
144 }
145
146 pub fn update_task_status(
148 &self,
149 task_id: usize,
150 status: super::types::TaskStatus,
151 ) -> Option<SessionGoal> {
152 let mut goal = self.goal.write().unwrap_or_else(|e| e.into_inner());
153 if let Some(ref mut g) = *goal {
154 if !g.update_task_status(task_id, status) {
155 return None;
156 }
157 let cloned = g.clone();
158 drop(goal);
159 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
160 acct.replace_goal(cloned.clone());
161 }
162 Some(cloned)
163 } else {
164 None
165 }
166 }
167
168 pub fn start_turn(&self) {
172 let goal = self.get_goal();
173 if let Some(goal) = goal {
174 if goal.status.should_auto_continue() {
175 let acct = GoalAccountingState::new(goal);
176 acct.start_turn();
177 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) = Some(acct);
178 }
179 }
180 }
181
182 pub fn record_tokens(&self, delta: i64) -> bool {
184 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
185 let exceeded = acct.record_token_usage(delta);
186 if exceeded {
187 if let Some(goal) = acct.snapshot() {
189 self.update_goal(goal);
190 }
191 }
192 exceeded
193 } else {
194 false
195 }
196 }
197
198 pub fn finish_turn(&self) {
200 if let Some(acct) = self
201 .accounting
202 .read()
203 .unwrap_or_else(|e| e.into_inner())
204 .as_ref()
205 {
206 if let Some(goal) = acct.finish_turn() {
207 self.update_goal(goal);
208 }
209 }
210 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) = None;
211 }
212
213 pub fn record_blocked_turn(&self, reason: &str) -> bool {
217 let mut goal_guard = self.goal.write().unwrap_or_else(|e| e.into_inner());
218 if let Some(ref mut goal) = *goal_guard {
219 let became_blocked = goal.record_blocked_turn(reason);
220 let updated = goal.clone();
221 drop(goal_guard);
222 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
223 acct.replace_goal(updated);
224 }
225 became_blocked
226 } else {
227 false
228 }
229 }
230
231 pub fn mark_usage_limited(&self) {
233 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
234 if let Some(mut goal) = acct.snapshot() {
235 goal.transition_to(crate::goal::types::GoalStatus::UsageLimited);
236 self.update_goal(goal);
237 }
238 } else if let Some(mut goal) = self.get_goal() {
239 goal.transition_to(crate::goal::types::GoalStatus::UsageLimited);
240 self.update_goal(goal);
241 }
242 }
243
244 pub fn continue_if_idle(&self) -> Option<String> {
251 let _lock = self.state_lock.lock().unwrap_or_else(|e| e.into_inner());
252 let goal = self.get_goal()?;
253 if !goal.status.should_auto_continue() {
254 return None;
255 }
256 let auto = *self.auto_continue.read().unwrap_or_else(|e| e.into_inner());
257 if !auto {
258 return None;
259 }
260 Some(steering::build_continuation_prompt(&goal))
261 }
262
263 pub fn budget_limit_prompt(&self) -> Option<String> {
265 let goal = self.get_goal()?;
266 if goal.is_budget_exceeded() {
267 Some(steering::build_budget_limit_prompt(&goal))
268 } else {
269 None
270 }
271 }
272
273 pub fn objective_updated_prompt(&self) -> Option<String> {
275 let goal = self.get_goal()?;
276 if goal.status == crate::goal::types::GoalStatus::Active {
277 Some(steering::build_objective_updated_prompt(&goal))
278 } else {
279 None
280 }
281 }
282
283 pub fn set_auto_continue(&self, enabled: bool) {
285 *self
286 .auto_continue
287 .write()
288 .unwrap_or_else(|e| e.into_inner()) = enabled;
289 }
290
291 pub fn is_terminal(&self) -> bool {
293 self.get_goal()
294 .map(|g| g.status.is_terminal())
295 .unwrap_or(false)
296 }
297}