Skip to main content

agentsight_capture/
model.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 eunomia-bpf org.
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::{BTreeMap, BTreeSet};
7
8pub type ViewResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
9
10pub const AGENT_NATIVE_SOURCE: &str = "agent_native_session";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TokenSummary {
14    pub group: String,
15    pub input_tokens: i64,
16    pub output_tokens: i64,
17    pub cache_creation_tokens: i64,
18    pub cache_read_tokens: i64,
19    pub total_tokens: i64,
20    pub calls: i64,
21    pub sessions: i64,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TokenUsageRow {
26    pub id: String,
27    pub llm_call_id: String,
28    pub timestamp_ms: u64,
29    pub pid: Option<u32>,
30    pub comm: Option<String>,
31    pub provider: Option<String>,
32    pub model: Option<String>,
33    pub input_tokens: i64,
34    pub output_tokens: i64,
35    pub cache_creation_tokens: i64,
36    pub cache_read_tokens: i64,
37    pub total_tokens: i64,
38    pub source: String,
39    pub view_source: String,
40    pub confidence: Option<f32>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct LlmCallRow {
45    pub id: String,
46    pub session_id: Option<String>,
47    pub conversation_id: Option<String>,
48    pub start_timestamp_ms: u64,
49    pub end_timestamp_ms: Option<u64>,
50    pub pid: Option<u32>,
51    pub comm: Option<String>,
52    pub provider: Option<String>,
53    pub model: Option<String>,
54    pub call_kind: Option<String>,
55    pub status: String,
56    pub error_type: Option<String>,
57    pub finish_reason: Option<String>,
58    pub host: Option<String>,
59    pub path: Option<String>,
60    pub status_code: Option<u16>,
61    pub input_tokens: i64,
62    pub output_tokens: i64,
63    pub total_tokens: i64,
64    pub request: Value,
65    pub response: Value,
66}
67
68#[derive(Debug, Clone, Copy)]
69pub struct SnapshotOptions {
70    pub audit_limit: usize,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Snapshot {
75    pub schema_version: u16,
76    pub generated_at: String,
77    pub summary: SnapshotSummary,
78    pub token_summary: Vec<TokenSummary>,
79    pub network_targets: Vec<NetworkTargetRow>,
80    pub process_nodes: Vec<ProcessNodeRow>,
81    pub audit_events: Vec<AuditEventRow>,
82    pub resource_samples: Vec<ResourceSampleRow>,
83    pub sessions: Vec<SessionRow>,
84    pub tool_calls: Vec<ToolCallRow>,
85}
86
87impl Snapshot {
88    pub fn empty(source: impl Into<String>) -> Self {
89        Self {
90            schema_version: 1,
91            generated_at: String::new(),
92            summary: SnapshotSummary::empty(source),
93            token_summary: Vec::new(),
94            network_targets: Vec::new(),
95            process_nodes: Vec::new(),
96            audit_events: Vec::new(),
97            resource_samples: Vec::new(),
98            sessions: Vec::new(),
99            tool_calls: Vec::new(),
100        }
101    }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct SnapshotSummary {
106    pub source: String,
107    pub view_events: i64,
108    pub llm_calls: i64,
109    pub token_usage_rows: i64,
110    pub audit_events: i64,
111    pub sessions: i64,
112    pub input_tokens: i64,
113    pub output_tokens: i64,
114    pub total_tokens: i64,
115    pub start_timestamp_ms: Option<u64>,
116    pub end_timestamp_ms: Option<u64>,
117    pub audit_limit: usize,
118}
119
120impl SnapshotSummary {
121    pub fn empty(source: impl Into<String>) -> Self {
122        Self {
123            source: source.into(),
124            view_events: 0,
125            llm_calls: 0,
126            token_usage_rows: 0,
127            audit_events: 0,
128            sessions: 0,
129            input_tokens: 0,
130            output_tokens: 0,
131            total_tokens: 0,
132            start_timestamp_ms: None,
133            end_timestamp_ms: None,
134            audit_limit: 0,
135        }
136    }
137
138    pub fn duration_s(&self) -> f64 {
139        match (self.start_timestamp_ms, self.end_timestamp_ms) {
140            (Some(start), Some(end)) if end > start => (end - start) as f64 / 1000.0,
141            _ => 0.0,
142        }
143    }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct NetworkTargetRow {
148    pub pid: Option<u32>,
149    pub comm: Option<String>,
150    pub host: String,
151    pub path: Option<String>,
152    pub count: i64,
153    pub error_count: i64,
154    pub first_timestamp_ms: Option<u64>,
155    pub last_timestamp_ms: Option<u64>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct ResourceSampleRow {
160    pub timestamp_ms: u64,
161    pub pid: Option<u32>,
162    pub comm: Option<String>,
163    pub cpu_percent: Option<f64>,
164    pub rss_mb: Option<i64>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct AuditEventRow {
169    pub id: String,
170    pub timestamp_ms: u64,
171    pub audit_type: String,
172    pub pid: Option<u32>,
173    pub comm: Option<String>,
174    pub subject: Option<String>,
175    pub action: Option<String>,
176    pub target: Option<String>,
177    pub status: Option<String>,
178    pub summary: Option<String>,
179    pub details: Value,
180}
181
182#[derive(Debug, Clone, Default)]
183pub struct AuditCounters {
184    pub process_execs: usize,
185    pub process_exits: usize,
186    pub process_exit_success: usize,
187    pub process_exit_failure: usize,
188    pub file_events: usize,
189    pub network_events: usize,
190    pub unique_files: BTreeSet<String>,
191}
192
193impl AuditCounters {
194    pub fn by_pid<'a>(rows: impl IntoIterator<Item = &'a AuditEventRow>) -> BTreeMap<u32, Self> {
195        let mut by_pid = BTreeMap::new();
196        for row in rows {
197            if let Some(pid) = row.pid {
198                by_pid.entry(pid).or_insert_with(Self::default).observe(row);
199            }
200        }
201        by_pid
202    }
203
204    fn observe(&mut self, row: &AuditEventRow) {
205        match row.audit_type.as_str() {
206            "process" if row.action.as_deref() == Some("exec") => self.process_execs += 1,
207            "process" if row.action.as_deref() == Some("exit") => {
208                self.process_exits += 1;
209                match row.status.as_deref() {
210                    Some("success") => self.process_exit_success += 1,
211                    Some("failure") => self.process_exit_failure += 1,
212                    _ => {}
213                }
214            }
215            "file" => {
216                self.file_events += 1;
217                if let Some(target) = &row.target {
218                    self.unique_files.insert(target.clone());
219                }
220            }
221            "network" => self.network_events += 1,
222            _ => {}
223        }
224    }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ProcessNodeRow {
229    pub id: String,
230    pub pid: u32,
231    pub ppid: Option<u32>,
232    pub root_pid: Option<u32>,
233    pub start_timestamp_ms: Option<u64>,
234    pub end_timestamp_ms: Option<u64>,
235    pub comm: Option<String>,
236    pub command: Option<String>,
237    pub argv: Vec<String>,
238    pub cwd: Option<String>,
239    pub exit_code: Option<i32>,
240    pub status: Option<String>,
241    pub view_source: String,
242    pub confidence: Option<f32>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct ToolCallRow {
247    pub id: String,
248    pub session_id: Option<String>,
249    pub conversation_id: Option<String>,
250    pub timestamp_ms: u64,
251    pub tool_name: Option<String>,
252    pub tool_call_id: Option<String>,
253    pub start_timestamp_ms: Option<u64>,
254    pub end_timestamp_ms: Option<u64>,
255    pub duration_ms: Option<u64>,
256    pub status: Option<String>,
257    pub input: Value,
258    pub output: Value,
259    pub related_pid: Option<u32>,
260    pub related_event_id: Option<String>,
261    pub view_source: String,
262    pub confidence: Option<f32>,
263}
264
265#[derive(Debug, Clone, Default, Serialize, Deserialize)]
266pub struct SessionRow {
267    pub id: String,
268    pub agent_type: String,
269    pub start_timestamp_ms: u64,
270    pub end_timestamp_ms: Option<u64>,
271    pub status: String,
272    pub model: Option<String>,
273    pub input_tokens: i64,
274    pub output_tokens: i64,
275    pub total_tokens: i64,
276    pub view_source: String,
277    pub confidence: Option<f64>,
278    pub attributes: Value,
279}
280
281pub trait ViewSink: Send {
282    fn llm_call(&mut self, _row: &LlmCallRow) -> ViewResult<()> {
283        Ok(())
284    }
285
286    fn token_usage(&mut self, _row: &TokenUsageRow) -> ViewResult<()> {
287        Ok(())
288    }
289
290    fn audit_event(&mut self, _row: &AuditEventRow) -> ViewResult<()> {
291        Ok(())
292    }
293
294    fn process_node(&mut self, _row: &ProcessNodeRow) -> ViewResult<()> {
295        Ok(())
296    }
297
298    fn tool_call(&mut self, _row: &ToolCallRow) -> ViewResult<()> {
299        Ok(())
300    }
301
302    fn network_target(&mut self, _row: &NetworkTargetRow) -> ViewResult<()> {
303        Ok(())
304    }
305
306    fn resource_sample(&mut self, _row: &ResourceSampleRow) -> ViewResult<()> {
307        Ok(())
308    }
309}