1use super::*;
2
3pub fn messages_json(id: &str) -> Vec<serde_json::Value> {
4 SESSIONS.with(|s| {
5 let map = s.borrow();
6 let Some(state) = map.get(id) else {
7 return Vec::new();
8 };
9 let Some(dict) = state.transcript.as_dict() else {
10 return Vec::new();
11 };
12 match dict.get("messages") {
13 Some(VmValue::List(list)) => list
14 .iter()
15 .map(crate::llm::helpers::vm_value_to_json)
16 .collect(),
17 _ => Vec::new(),
18 }
19 })
20}
21
22#[derive(Clone, Debug, Default)]
23pub struct SessionPromptState {
24 pub messages: Vec<serde_json::Value>,
25 pub summary: Option<String>,
26}
27
28fn summary_message_json(summary: &str) -> serde_json::Value {
29 serde_json::json!({
30 "role": "user",
31 "content": summary,
32 })
33}
34
35fn messages_begin_with_summary(messages: &[serde_json::Value], summary: &str) -> bool {
36 messages.first().is_some_and(|message| {
37 message.get("role").and_then(|value| value.as_str()) == Some("user")
38 && message.get("content").and_then(|value| value.as_str()) == Some(summary)
39 })
40}
41
42pub fn prompt_state_json(id: &str) -> SessionPromptState {
50 SESSIONS.with(|s| {
51 let map = s.borrow();
52 let Some(state) = map.get(id) else {
53 return SessionPromptState::default();
54 };
55 let Some(dict) = state.transcript.as_dict() else {
56 return SessionPromptState::default();
57 };
58 let mut messages = match dict.get("messages") {
59 Some(VmValue::List(list)) => list
60 .iter()
61 .map(crate::llm::helpers::vm_value_to_json)
62 .collect::<Vec<_>>(),
63 _ => Vec::new(),
64 };
65 let summary = dict.get("summary").and_then(|value| match value {
66 VmValue::String(text) if !text.trim().is_empty() => Some(text.to_string()),
67 _ => None,
68 });
69 if let Some(summary_text) = summary.as_deref() {
70 if !messages_begin_with_summary(&messages, summary_text) {
71 messages.insert(0, summary_message_json(summary_text));
72 }
73 }
74 SessionPromptState { messages, summary }
75 })
76}
77
78pub fn store_transcript(id: &str, transcript: VmValue) -> Result<(), String> {
81 SESSIONS.with(|s| {
82 let mut map = s.borrow_mut();
83 let Some(state) = map.get_mut(id) else {
84 return Err(format!(
85 "agent_session_store_transcript: unknown session id '{id}'"
86 ));
87 };
88 let transcript = transcript_with_session_metadata(transcript, state);
89 let text_tool_call_seq = next_text_tool_call_seq_from_transcript(&transcript);
90 apply_transcript_with_budget(state, transcript, "store_transcript")?;
91 state.text_tool_call_seq = state.text_tool_call_seq.max(text_tool_call_seq);
92 Ok(())
93 })
94}
95
96fn checkpoint_summary(checkpoint: &SessionTurnCheckpoint) -> SessionCheckpointSummary {
97 SessionCheckpointSummary {
98 checkpoint_id: checkpoint.checkpoint_id.clone(),
99 before_message_count: checkpoint.before_message_count,
100 after_message_count: checkpoint.after_message_count,
101 fs_snapshot_ids: checkpoint.fs_snapshot_ids.clone(),
102 }
103}
104
105fn checkpoint_error_status(error: SessionCheckpointError) -> &'static str {
106 match error {
107 SessionCheckpointError::UnknownSession => "unknown_session",
108 SessionCheckpointError::NoCheckpoint => "no_checkpoint",
109 SessionCheckpointError::NoRedo => "no_redo",
110 }
111}
112
113pub fn checkpoint_status_name(error: SessionCheckpointError) -> &'static str {
114 checkpoint_error_status(error)
115}
116
117pub fn invalidate_redo(id: &str) -> bool {
120 SESSIONS.with(|s| {
121 let mut map = s.borrow_mut();
122 let Some(state) = map.get_mut(id) else {
123 return false;
124 };
125 let had_redo = !state.redo_stack.is_empty();
126 state.redo_stack.clear();
127 state.touch();
128 had_redo
129 })
130}
131
132pub fn record_completed_turn_checkpoint(
139 id: &str,
140 before_transcript: VmValue,
141 fs_snapshot_ids: Vec<String>,
142) -> Result<Option<SessionCheckpointSummary>, SessionCheckpointError> {
143 SESSIONS.with(|s| {
144 let mut map = s.borrow_mut();
145 let Some(state) = map.get_mut(id) else {
146 return Err(SessionCheckpointError::UnknownSession);
147 };
148 let after_transcript = transcript_with_session_metadata(state.transcript.clone(), state);
149 let before_message_count = transcript_message_count(&before_transcript);
150 let after_message_count = transcript_message_count(&after_transcript);
151 if crate::values_equal(&before_transcript, &after_transcript) && fs_snapshot_ids.is_empty()
152 {
153 return Ok(None);
154 }
155 let checkpoint = SessionTurnCheckpoint {
156 checkpoint_id: format!("turn_{}", uuid::Uuid::now_v7().simple()),
157 completed_at: crate::orchestration::now_unix_seconds_text(),
158 before_message_count,
159 after_message_count,
160 before_transcript,
161 after_transcript,
162 fs_snapshot_ids,
163 };
164 state.redo_stack.clear();
165 state.completed_turn_checkpoints.push(checkpoint.clone());
166 state.touch();
167 Ok(Some(checkpoint_summary(&checkpoint)))
168 })
169}
170
171pub fn rollback_plan(id: &str) -> Result<SessionCheckpointSummary, SessionCheckpointError> {
172 SESSIONS.with(|s| {
173 let map = s.borrow();
174 let Some(state) = map.get(id) else {
175 return Err(SessionCheckpointError::UnknownSession);
176 };
177 state
178 .completed_turn_checkpoints
179 .last()
180 .map(checkpoint_summary)
181 .ok_or(SessionCheckpointError::NoCheckpoint)
182 })
183}
184
185pub fn redo_plan(id: &str) -> Result<SessionCheckpointSummary, SessionCheckpointError> {
186 SESSIONS.with(|s| {
187 let map = s.borrow();
188 let Some(state) = map.get(id) else {
189 return Err(SessionCheckpointError::UnknownSession);
190 };
191 state
192 .redo_stack
193 .last()
194 .map(|entry| {
195 let mut summary = checkpoint_summary(&entry.checkpoint);
196 summary.fs_snapshot_ids = entry.redo_fs_snapshot_ids.clone();
197 summary
198 })
199 .ok_or(SessionCheckpointError::NoRedo)
200 })
201}
202
203pub fn rollback_last_completed_turn(
204 id: &str,
205 redo_fs_snapshot_ids: Vec<String>,
206) -> Result<SessionCheckpointOutcome, SessionCheckpointError> {
207 SESSIONS.with(|s| {
208 let mut map = s.borrow_mut();
209 let Some(state) = map.get_mut(id) else {
210 return Err(SessionCheckpointError::UnknownSession);
211 };
212 let Some(checkpoint) = state.completed_turn_checkpoints.pop() else {
213 return Err(SessionCheckpointError::NoCheckpoint);
214 };
215 state.transcript = checkpoint.before_transcript.clone();
216 state.redo_stack.push(SessionRedoEntry {
217 checkpoint: checkpoint.clone(),
218 redo_fs_snapshot_ids: redo_fs_snapshot_ids.clone(),
219 });
220 state.touch();
221 Ok(SessionCheckpointOutcome {
222 status: "rolled_back",
223 checkpoint: checkpoint_summary(&checkpoint),
224 redo_fs_snapshot_ids,
225 })
226 })
227}
228
229pub fn redo_last_rollback(id: &str) -> Result<SessionCheckpointOutcome, SessionCheckpointError> {
230 SESSIONS.with(|s| {
231 let mut map = s.borrow_mut();
232 let Some(state) = map.get_mut(id) else {
233 return Err(SessionCheckpointError::UnknownSession);
234 };
235 let Some(entry) = state.redo_stack.pop() else {
236 return Err(SessionCheckpointError::NoRedo);
237 };
238 let checkpoint = entry.checkpoint;
239 state.transcript = checkpoint.after_transcript.clone();
240 state.completed_turn_checkpoints.push(checkpoint.clone());
241 state.touch();
242 Ok(SessionCheckpointOutcome {
243 status: "redone",
244 checkpoint: checkpoint_summary(&checkpoint),
245 redo_fs_snapshot_ids: entry.redo_fs_snapshot_ids,
246 })
247 })
248}
249
250pub fn prune_invalid_reminder_events(id: &str) -> usize {
254 SESSIONS.with(|s| {
255 let mut map = s.borrow_mut();
256 let Some(state) = map.get_mut(id) else {
257 return 0;
258 };
259 let Some(dict) = state.transcript.as_dict().cloned() else {
260 return 0;
261 };
262 let Some(VmValue::List(events)) = dict.get("events") else {
263 return 0;
264 };
265 let mut pruned = 0_usize;
266 let mut kept = Vec::with_capacity(events.len());
267 for event in events.iter().cloned() {
268 let is_reminder = event
269 .as_dict()
270 .and_then(|event| event.get("kind"))
271 .map(VmValue::display)
272 .as_deref()
273 == Some(crate::llm::helpers::SYSTEM_REMINDER_EVENT_KIND);
274 if !is_reminder {
275 kept.push(event);
276 continue;
277 }
278 let valid = crate::llm::helpers::reminder_from_event(&event)
279 .is_some_and(|reminder| !reminder.body.trim().is_empty());
280 if valid {
281 kept.push(event);
282 } else {
283 pruned += 1;
284 }
285 }
286 if pruned > 0 {
287 let mut next = dict;
288 next.insert(
289 crate::value::intern_key("events"),
290 VmValue::List(std::sync::Arc::new(kept)),
291 );
292 let _ = apply_transcript_with_budget(
293 state,
294 VmValue::dict(next),
295 "prune_invalid_reminder_events",
296 );
297 state.touch();
298 }
299 pruned
300 })
301}
302
303pub fn apply_reminder_post_turn(id: &str, turn: i64) -> Result<serde_json::Value, String> {
308 let report = SESSIONS.with(|s| {
309 let mut map = s.borrow_mut();
310 let Some(state) = map.get_mut(id) else {
311 return Err(format!(
312 "agent_session_apply_reminder_post_turn: unknown session id '{id}'"
313 ));
314 };
315 let report = crate::llm::helpers::apply_reminder_post_turn(&state.transcript, turn);
316 if report.decremented_count > 0 || !report.expired.is_empty() {
317 if let Some(next) = report.transcript.clone() {
318 apply_transcript_with_budget(state, next, "apply_reminder_post_turn")?;
319 }
320 state.touch();
321 }
322 Ok(report)
323 })?;
324
325 for reminder in &report.expired {
326 let mut payload = crate::llm::helpers::reminder_lifecycle_payload(Some(id), reminder);
327 if let Some(obj) = payload.as_object_mut() {
328 obj.insert(
329 "transcript_id".to_string(),
330 serde_json::Value::String(id.to_string()),
331 );
332 obj.insert(
333 "reason".to_string(),
334 serde_json::Value::String("ttl".to_string()),
335 );
336 obj.insert(
337 "ttl_turns_before".to_string(),
338 serde_json::json!(&reminder.ttl_turns),
339 );
340 obj.insert("expired_at_turn".to_string(), serde_json::json!(turn));
341 }
342 crate::llm::helpers::emit_reminder_lifecycle_event(
343 crate::llm::helpers::REMINDER_EXPIRED_EVENT_KIND,
344 payload,
345 );
346 }
347
348 Ok(serde_json::json!({
349 "expired_count": report.expired.len(),
350 "decremented_count": report.decremented_count,
351 "remaining_count": report.remaining_count,
352 }))
353}
354
355pub fn inject_reminder(
360 id: &str,
361 reminder: crate::llm::helpers::SystemReminder,
362) -> Result<ReminderInjectionReport, String> {
363 let reminder_id = reminder.id.clone();
364 let dedupe_key = reminder.dedupe_key.clone();
365 let mut deduped_reminder_ids = Vec::new();
366 SESSIONS.with(|s| {
367 let mut map = s.borrow_mut();
368 let Some(state) = map.get_mut(id) else {
369 return Err(format!(
370 "agent_session_inject_reminder: unknown session id '{id}'"
371 ));
372 };
373 let dict = state
374 .transcript
375 .as_dict()
376 .cloned()
377 .unwrap_or_else(crate::value::DictMap::new);
378 let mut events: Vec<VmValue> = match dict.get("events") {
379 Some(VmValue::List(list)) => list.iter().cloned().collect(),
380 _ => dict
381 .get("messages")
382 .and_then(|value| match value {
383 VmValue::List(list) => Some(list.iter().cloned().collect::<Vec<_>>()),
384 _ => None,
385 })
386 .map(|messages| crate::llm::helpers::transcript_events_from_messages(&messages))
387 .unwrap_or_default(),
388 };
389 if let Some(expected_key) = dedupe_key.as_deref() {
390 events.retain(|event| {
391 let Some(existing) = crate::llm::helpers::reminder_from_event(event) else {
392 return true;
393 };
394 if existing.dedupe_key.as_deref() == Some(expected_key) {
395 deduped_reminder_ids.push(existing.id);
396 false
397 } else {
398 true
399 }
400 });
401 }
402 events.push(crate::llm::helpers::transcript_reminder_event(&reminder));
403 let mut next = dict;
404 next.insert(
405 crate::value::intern_key("events"),
406 VmValue::List(std::sync::Arc::new(events)),
407 );
408 apply_transcript_with_budget(state, VmValue::dict(next), "inject_reminder")?;
409 state.touch();
410 Ok(())
411 })?;
412
413 if !deduped_reminder_ids.is_empty() {
414 let dropped_count = deduped_reminder_ids.len();
415 crate::llm::helpers::emit_reminder_lifecycle_event(
416 crate::llm::helpers::REMINDER_DEDUPED_EVENT_KIND,
417 serde_json::json!({
418 "session_id": id,
419 "transcript_id": id,
420 "reminder_id": &reminder_id,
421 "replacing_id": &reminder_id,
422 "replaced_id": deduped_reminder_ids.first(),
423 "replaced_ids": &deduped_reminder_ids,
424 "dedupe_key": &dedupe_key,
425 "dropped_reminder_ids": &deduped_reminder_ids,
426 "dropped_count": dropped_count,
427 }),
428 );
429 }
430
431 crate::llm::helpers::emit_reminder_lifecycle_event(
432 crate::llm::helpers::REMINDER_INJECTED_EVENT_KIND,
433 crate::llm::helpers::reminder_lifecycle_payload(Some(id), &reminder),
434 );
435
436 Ok(ReminderInjectionReport {
437 reminder_id,
438 deduped_count: deduped_reminder_ids.len(),
439 })
440}