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(
74 &self,
75 objective: String,
76 short_description: Option<String>,
77 token_budget: Option<i64>,
78 ) -> SessionGoal {
79 let mut goal_guard = self.goal.write().unwrap_or_else(|e| e.into_inner());
80 if let Some(ref mut goal) = *goal_guard {
81 goal.objective = objective;
82 goal.short_description = short_description;
83 goal.token_budget = token_budget;
84 goal.status = crate::goal::types::GoalStatus::Active;
85 goal.consecutive_blocked_turns = 0;
86 goal.block_reason = None;
87 goal.updated_at = crate::session::current_unix_timestamp();
88 let new_goal = goal.clone();
89 drop(goal_guard);
90 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) =
91 Some(GoalAccountingState::new(new_goal.clone()));
92 new_goal
93 } else {
94 let session_id = self
95 .session_id
96 .read()
97 .unwrap_or_else(|e| e.into_inner())
98 .clone()
99 .unwrap_or_default();
100 let mut new_goal = SessionGoal::new(session_id, objective, token_budget);
101 new_goal.short_description = short_description;
102 let cloned = new_goal.clone();
103 *goal_guard = Some(new_goal);
104 drop(goal_guard);
105 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) =
106 Some(GoalAccountingState::new(cloned.clone()));
107 cloned
108 }
109 }
110
111 pub fn create_new_goal(
116 &self,
117 objective: String,
118 short_description: Option<String>,
119 token_budget: Option<i64>,
120 ) -> SessionGoal {
121 let session_id = self
122 .session_id
123 .read()
124 .unwrap_or_else(|e| e.into_inner())
125 .clone()
126 .unwrap_or_default();
127 let mut new_goal = SessionGoal::new(session_id, objective, token_budget);
128 new_goal.short_description = short_description;
129 let cloned = new_goal.clone();
130 *self.goal.write().unwrap_or_else(|e| e.into_inner()) = Some(new_goal);
131 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) =
132 Some(GoalAccountingState::new(cloned.clone()));
133 cloned
134 }
135
136 pub fn update_goal(&self, mut goal: SessionGoal) {
138 if let Some(session_id) = self
139 .session_id
140 .read()
141 .unwrap_or_else(|e| e.into_inner())
142 .clone()
143 {
144 goal.session_id = session_id;
145 }
146 *self.goal.write().unwrap_or_else(|e| e.into_inner()) = Some(goal.clone());
147 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
148 acct.replace_goal(goal);
149 }
150 }
151
152 pub fn clear_goal(&self) {
154 *self.goal.write().unwrap_or_else(|e| e.into_inner()) = None;
155 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) = None;
156 *self.session_id.write().unwrap_or_else(|e| e.into_inner()) = None;
157 }
158
159 pub fn update_checklist(&self, tasks: Vec<super::types::GoalTask>) -> Option<SessionGoal> {
161 let mut goal = self.goal.write().unwrap_or_else(|e| e.into_inner());
162 if let Some(ref mut g) = *goal {
163 g.set_checklist(tasks);
164 let cloned = g.clone();
165 drop(goal);
166 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
167 acct.replace_goal(cloned.clone());
168 }
169 Some(cloned)
170 } else {
171 None
172 }
173 }
174
175 pub fn update_task_status(
177 &self,
178 task_id: usize,
179 status: super::types::TaskStatus,
180 ) -> Option<SessionGoal> {
181 let mut goal = self.goal.write().unwrap_or_else(|e| e.into_inner());
182 if let Some(ref mut g) = *goal {
183 if !g.update_task_status(task_id, status) {
184 return None;
185 }
186 let cloned = g.clone();
187 drop(goal);
188 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
189 acct.replace_goal(cloned.clone());
190 }
191 Some(cloned)
192 } else {
193 None
194 }
195 }
196
197 pub fn start_turn(&self) {
201 let goal = self.get_goal();
202 if let Some(goal) = goal {
203 if goal.status.should_auto_continue() {
204 let acct = GoalAccountingState::new(goal);
205 acct.start_turn();
206 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) = Some(acct);
207 }
208 }
209 }
210
211 pub fn record_tokens(&self, delta: i64) -> bool {
213 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
214 let exceeded = acct.record_token_usage(delta);
215 if exceeded {
216 if let Some(goal) = acct.snapshot() {
218 self.update_goal(goal);
219 }
220 }
221 exceeded
222 } else {
223 false
224 }
225 }
226
227 pub fn finish_turn(&self) {
229 if let Some(acct) = self
230 .accounting
231 .read()
232 .unwrap_or_else(|e| e.into_inner())
233 .as_ref()
234 {
235 if let Some(goal) = acct.finish_turn() {
236 self.update_goal(goal);
237 }
238 }
239 *self.accounting.write().unwrap_or_else(|e| e.into_inner()) = None;
240 }
241
242 pub fn record_blocked_turn(&self, reason: &str) -> bool {
246 let mut goal_guard = self.goal.write().unwrap_or_else(|e| e.into_inner());
247 if let Some(ref mut goal) = *goal_guard {
248 let became_blocked = goal.record_blocked_turn(reason);
249 let updated = goal.clone();
250 drop(goal_guard);
251 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
252 acct.replace_goal(updated);
253 }
254 became_blocked
255 } else {
256 false
257 }
258 }
259
260 pub fn mark_usage_limited(&self) {
262 if let Some(ref acct) = *self.accounting.read().unwrap_or_else(|e| e.into_inner()) {
263 if let Some(mut goal) = acct.snapshot() {
264 goal.transition_to(crate::goal::types::GoalStatus::UsageLimited);
265 self.update_goal(goal);
266 }
267 } else if let Some(mut goal) = self.get_goal() {
268 goal.transition_to(crate::goal::types::GoalStatus::UsageLimited);
269 self.update_goal(goal);
270 }
271 }
272
273 pub fn continue_if_idle(&self) -> Option<String> {
280 let _lock = self.state_lock.lock().unwrap_or_else(|e| e.into_inner());
281 let goal = self.get_goal()?;
282 if !goal.status.should_auto_continue() {
283 return None;
284 }
285 let auto = *self.auto_continue.read().unwrap_or_else(|e| e.into_inner());
286 if !auto {
287 return None;
288 }
289 Some(steering::build_continuation_prompt(&goal))
290 }
291
292 pub fn budget_limit_prompt(&self) -> Option<String> {
294 let goal = self.get_goal()?;
295 if goal.is_budget_exceeded() {
296 Some(steering::build_budget_limit_prompt(&goal))
297 } else {
298 None
299 }
300 }
301
302 pub fn objective_updated_prompt(&self) -> Option<String> {
304 let goal = self.get_goal()?;
305 if goal.status == crate::goal::types::GoalStatus::Active {
306 Some(steering::build_objective_updated_prompt(&goal))
307 } else {
308 None
309 }
310 }
311
312 pub fn set_auto_continue(&self, enabled: bool) {
314 *self
315 .auto_continue
316 .write()
317 .unwrap_or_else(|e| e.into_inner()) = enabled;
318 }
319
320 pub fn is_terminal(&self) -> bool {
322 self.get_goal()
323 .map(|g| g.status.is_terminal())
324 .unwrap_or(false)
325 }
326}