atman_runtime/
activity.rs1use std::path::Path;
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
4pub struct EditMetrics {
5 pub hunks: usize,
6 pub insertions: usize,
7 pub deletions: usize,
8}
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct ActivitySummary {
12 pub attempted_calls: usize,
13 pub completed_calls: usize,
14 pub failed_calls: usize,
15 pub applied_edits: usize,
16 pub files: usize,
17 pub hunks: usize,
18 pub insertions: usize,
19 pub deletions: usize,
20}
21
22#[derive(Debug, Clone, Default)]
23pub(crate) struct ActivityAccumulator {
24 summary: ActivitySummary,
25 attempted: std::collections::HashSet<(String, String)>,
26 completed: std::collections::HashSet<(String, String)>,
27 files: std::collections::BTreeSet<String>,
28}
29
30impl ActivityAccumulator {
31 pub(crate) fn observe(&mut self, event: &crate::event::Event) {
32 match event {
33 crate::event::Event::ToolNode {
34 run_id,
35 tool_use_id,
36 ..
37 } => {
38 if self
39 .attempted
40 .insert((run_id.to_string(), tool_use_id.clone()))
41 {
42 self.summary.attempted_calls += 1;
43 }
44 }
45 crate::event::Event::ToolResultMsg {
46 flow_run_id,
47 message,
48 ..
49 } => {
50 for part in &message.parts {
51 if let crate::message::MessagePart::ToolResult {
52 tool_use_id,
53 is_error,
54 ..
55 } = part
56 {
57 let key = (
58 flow_run_id
59 .as_ref()
60 .map(ToString::to_string)
61 .unwrap_or_default(),
62 tool_use_id.clone(),
63 );
64 if self.completed.insert(key) {
65 self.summary.completed_calls += 1;
66 self.summary.failed_calls += usize::from(*is_error);
67 }
68 }
69 }
70 }
71 crate::event::Event::FileEditApplied { path, metrics, .. } => {
72 self.summary.applied_edits += 1;
73 self.files.insert(path.clone());
74 self.summary.hunks += metrics.hunks;
75 self.summary.insertions += metrics.insertions;
76 self.summary.deletions += metrics.deletions;
77 }
78 _ => {}
79 }
80 self.summary.files = self.files.len();
81 }
82
83 pub(crate) fn summary(&self) -> ActivitySummary {
84 self.summary.clone()
85 }
86
87 pub(crate) fn file_paths(&self) -> Vec<String> {
88 self.files.iter().cloned().collect()
89 }
90}
91
92pub fn summarize_events(events: &[crate::event::EventEnvelope]) -> ActivitySummary {
93 let mut activity = ActivityAccumulator::default();
94 for envelope in events {
95 activity.observe(&envelope.event);
96 }
97 activity.summary()
98}
99
100pub fn edit_metrics(before: &str, after: &str) -> EditMetrics {
101 let diff = similar::TextDiff::from_lines(before, after);
102 let mut metrics = EditMetrics::default();
103 for change in diff.iter_all_changes() {
104 match change.tag() {
105 similar::ChangeTag::Insert => metrics.insertions += 1,
106 similar::ChangeTag::Delete => metrics.deletions += 1,
107 similar::ChangeTag::Equal => {}
108 }
109 }
110 metrics.hunks = diff
111 .grouped_ops(3)
112 .into_iter()
113 .filter(|group| {
114 group
115 .iter()
116 .any(|op| !matches!(op.tag(), similar::DiffTag::Equal))
117 })
118 .count();
119 metrics
120}
121
122pub fn emit_file_edit_applied(
123 ctx: &crate::tool::ToolCtx,
124 tool_name: &str,
125 path: &Path,
126 before: &str,
127 after: &str,
128) {
129 let metrics = edit_metrics(before, after);
130 if metrics.insertions == 0 && metrics.deletions == 0 {
131 return;
132 }
133 let path = path
134 .canonicalize()
135 .unwrap_or_else(|_| path.to_path_buf())
136 .to_string_lossy()
137 .into_owned();
138 if let Some(sink) = &ctx.events {
139 sink.emit(crate::event::Event::FileEditApplied {
140 turn_id: ctx.turn_id.clone(),
141 flow_run_id: ctx.flow_run_id.clone(),
142 tool_use_id: ctx.tool_use_id.clone(),
143 tool_name: tool_name.to_string(),
144 path: path.clone(),
145 metrics,
146 });
147 }
148 if let Some(tx) = &ctx.stream_tx {
149 let _ = tx.send(crate::stream::StreamFrame::FileEditApplied {
150 turn_id: ctx.turn_id.as_ref().map(ToString::to_string),
151 run_id: ctx.flow_run_id.as_ref().map(ToString::to_string),
152 tool_use_id: ctx.tool_use_id.clone(),
153 tool_name: tool_name.to_string(),
154 path,
155 metrics,
156 });
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn edit_metrics_count_visual_line_changes_and_hunks() {
166 let metrics = edit_metrics("one\ntwo\nthree\n", "one\nchanged\nthree\nadded\n");
167 assert_eq!(
168 metrics,
169 EditMetrics {
170 hunks: 1,
171 insertions: 2,
172 deletions: 1,
173 }
174 );
175 }
176}