use crate::sessions::SessionEventKind;
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Default)]
pub(crate) struct CodeModeAudit {
actions: BTreeMap<u64, (String, Option<bool>)>,
changed_paths: BTreeSet<String>,
changed_paths_incomplete: bool,
warnings: Vec<String>,
warning_count: usize,
unpersisted_execution_effects: crate::sessions::effects::ExecutionEffects,
pub(crate) contexts: Vec<Value>,
}
impl CodeModeAudit {
pub(crate) fn has_result(&self, sequence: u64) -> bool {
self.actions
.get(&sequence)
.is_some_and(|(_, success)| success.is_some())
}
pub(crate) fn observe(&mut self, kind: SessionEventKind, payload: &Value) {
let sequence = payload["sequence"].as_u64().unwrap_or_default();
match kind {
SessionEventKind::CodeModeToolCall => {
self.actions.insert(
sequence,
(
payload["name"]
.as_str()
.unwrap_or("unknown")
.chars()
.take(256)
.collect(),
None,
),
);
}
SessionEventKind::CodeModeToolResult => {
if let Some(action) = self.actions.get_mut(&sequence) {
action.1 = payload["result"]["success"].as_bool();
}
if let Some(paths) = payload["changed_paths"]
.as_array()
.or_else(|| payload["execution_effects"]["paths"].as_array())
{
self.changed_paths_incomplete |=
payload["execution_effects"]["changed_path_count"]
.as_u64()
.is_some_and(|count| count > paths.len() as u64);
for path in paths.iter().filter_map(Value::as_str) {
let original = path;
let mut path = crate::output::sanitize_display_text(path);
self.changed_paths_incomplete |= path != original;
self.changed_paths_incomplete |= path.len() > 512;
let mut end = path.len().min(512);
while !path.is_char_boundary(end) {
end -= 1;
}
path.truncate(end);
if self.changed_paths.len() < 1000 {
self.changed_paths.insert(path);
} else if !self.changed_paths.contains(&path) {
self.changed_paths_incomplete = true;
}
}
}
}
SessionEventKind::CodeModeContext => self.contexts.push(payload.clone()),
SessionEventKind::CodeModeWarning => {
if let Some(warning) = payload["warning"].as_str() {
self.warning_count = self.warning_count.saturating_add(1);
if self.warnings.len() < 32 {
let safe = crate::output::sanitize_display_text(warning);
let mut end = safe.len().min(512);
while !safe.is_char_boundary(end) {
end -= 1;
}
self.warnings.push(safe[..end].to_owned());
}
}
}
_ => {}
}
}
pub(crate) fn observe_unpersisted_result(
&mut self,
payload: &Value,
definitely_not_written: bool,
) {
if definitely_not_written {
self.unpersisted_execution_effects
.observe_tool_result(Some(SessionEventKind::CodeModeToolResult), payload);
} else {
self.unpersisted_execution_effects.mark_incomplete();
}
}
pub(crate) fn summary(&self) -> Value {
let mut actions: Vec<_> = self.actions.iter().collect();
actions.sort_by_key(|(sequence, (_, success))| (success == &Some(true), **sequence));
actions.truncate(100);
actions.sort_by_key(|(sequence, _)| **sequence);
json!({
"inner_calls": self.actions.len(),
"successful": self.actions.values().filter(|(_, success)| *success == Some(true)).count(),
"recoverable_failures": self.actions.values().filter(|(_, success)| *success == Some(false)).count(),
"unknown_outcomes": self.actions.values().filter(|(_, success)| success.is_none()).count(),
"running_calls": 0,
"actions": actions.into_iter().map(|(sequence, (name, success))| json!({"sequence": sequence,"tool":name,"success":success})).collect::<Vec<_>>(),
"actions_omitted": self.actions.len().saturating_sub(100),
"changed_paths": self.changed_paths.iter().take(50).collect::<Vec<_>>(),
"changed_path_count": self.changed_paths.len(),
"changed_paths_incomplete": self.changed_paths_incomplete,
"unpersisted_execution_effects": self.unpersisted_execution_effects,
"warnings": self.warnings,
"warning_count": self.warning_count,
"warnings_omitted": self.warning_count.saturating_sub(self.warnings.len()),
"effects": "Completed actions remain applied. Missing results have unknown outcomes. No rollback or automatic retry. Shell and external effects may be untracked."
})
}
}
pub(crate) fn recorded_summaries(
events: &[crate::sessions::SessionEvent],
) -> BTreeMap<usize, Value> {
let mut parents = BTreeMap::<&str, Option<usize>>::new();
let mut audits = BTreeMap::<usize, CodeModeAudit>::new();
for (index, event) in events.iter().enumerate() {
match event.kind() {
Some(SessionEventKind::UserInput) => parents.clear(),
Some(SessionEventKind::ToolCall)
if event.payload["name"] == crate::code_mode::TOOL_NAME =>
{
if let Some(id) = event.payload["id"].as_str()
&& let Some(previous) = parents.insert(id, Some(index))
{
if let Some(previous) = previous {
audits.remove(&previous);
}
parents.insert(id, None);
}
}
Some(
kind @ (SessionEventKind::CodeModeToolCall
| SessionEventKind::CodeModeToolResult
| SessionEventKind::CodeModeWarning),
) => {
if let Some(Some(parent)) = event.payload["parent_call_id"]
.as_str()
.and_then(|id| parents.get(id))
{
audits
.entry(*parent)
.or_default()
.observe(kind, &event.payload);
}
}
_ => {}
}
}
audits
.into_iter()
.map(|(index, audit)| (index, audit.summary()))
.collect()
}