agent_sdk_core/application/
trace.rs1use crate::{
6 domain::{
7 AttemptId, ContextProjectionId, EffectId, MessageId, RunId, SessionId, ToolCallId, TurnId,
8 },
9 journal::{EventIndexProjection, JournalRecord, JournalRecordPayload},
10};
11
12#[derive(Clone, Debug, Default, Eq, PartialEq)]
13pub struct TurnTrace {
16 pub session_id: Option<SessionId>,
18 pub turn_id: Option<TurnId>,
20 pub run_ids: Vec<RunId>,
22 pub attempt_ids: Vec<AttemptId>,
24 pub message_ids: Vec<MessageId>,
26 pub context_projection_ids: Vec<ContextProjectionId>,
28 pub effect_ids: Vec<EffectId>,
30 pub tool_call_ids: Vec<ToolCallId>,
32 pub event_indexes: Vec<EventIndexProjection>,
34 pub records: Vec<JournalRecord>,
36}
37
38impl TurnTrace {
39 pub fn from_records<'a>(
42 turn_id: &TurnId,
43 records: impl IntoIterator<Item = &'a JournalRecord>,
44 ) -> Self {
45 Self::from_matching_records(records, |record| record.turn_id.as_ref() == Some(turn_id))
46 }
47
48 fn from_matching_records<'a>(
49 records: impl IntoIterator<Item = &'a JournalRecord>,
50 matches: impl Fn(&JournalRecord) -> bool,
51 ) -> Self {
52 let mut trace = TurnTrace::default();
53 for record in records.into_iter().filter(|record| matches(record)) {
54 trace.push_record(record);
55 }
56 trace
57 }
58
59 fn push_record(&mut self, record: &JournalRecord) {
60 if self.session_id.is_none() {
61 self.session_id = record.session_id.clone();
62 }
63 if self.turn_id.is_none() {
64 self.turn_id = record.turn_id.clone();
65 }
66 push_unique(&mut self.run_ids, record.run_id.clone());
67 if let Some(attempt_id) = record.attempt_id.clone() {
68 push_unique(&mut self.attempt_ids, attempt_id);
69 }
70 self.push_payload_ids(&record.payload);
71 self.event_indexes.push(record.event_index.clone());
72 self.records.push(record.clone());
73 }
74
75 fn push_payload_ids(&mut self, payload: &JournalRecordPayload) {
76 match payload {
77 JournalRecordPayload::TurnLifecycle(record) => {
78 push_unique_opt(&mut self.message_ids, record.input_message_id.clone());
79 push_unique_opt(&mut self.message_ids, record.output_message_id.clone());
80 push_unique_opt(
81 &mut self.context_projection_ids,
82 record.context_projection_id.clone(),
83 );
84 for run_id in &record.run_ids {
85 push_unique(&mut self.run_ids, run_id.clone());
86 }
87 }
88 JournalRecordPayload::ContextProjection(record) => {
89 push_unique(
90 &mut self.context_projection_ids,
91 record.projection_id.clone(),
92 );
93 }
94 JournalRecordPayload::Message(record) => {
95 push_unique(&mut self.message_ids, record.message_id.clone());
96 }
97 JournalRecordPayload::EffectIntent(record) => {
98 push_unique(&mut self.effect_ids, record.effect_id.clone());
99 }
100 JournalRecordPayload::EffectResult(record) => {
101 push_unique(&mut self.effect_ids, record.effect_id.clone());
102 }
103 JournalRecordPayload::Tool(record) => {
104 push_unique(&mut self.tool_call_ids, record.tool_call_id.clone());
105 }
106 _ => {}
107 }
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.records.is_empty()
113 }
114}
115
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct RunTrace {
120 pub run_id: Option<RunId>,
122 pub session_id: Option<SessionId>,
124 pub turn_traces: Vec<TurnTrace>,
126 pub records: Vec<JournalRecord>,
128}
129
130impl RunTrace {
131 pub fn from_records<'a>(
133 run_id: &RunId,
134 records: impl IntoIterator<Item = &'a JournalRecord>,
135 ) -> Self {
136 let matching = records
137 .into_iter()
138 .filter(|record| &record.run_id == run_id)
139 .cloned()
140 .collect::<Vec<_>>();
141 let mut trace = RunTrace {
142 run_id: Some(run_id.clone()),
143 session_id: matching.iter().find_map(|record| record.session_id.clone()),
144 turn_traces: Vec::new(),
145 records: matching.clone(),
146 };
147 for turn_id in ordered_turn_ids(&matching) {
148 trace
149 .turn_traces
150 .push(TurnTrace::from_records(&turn_id, matching.iter()));
151 }
152 trace
153 }
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
157pub struct SessionTimeline {
160 pub session_id: SessionId,
162 pub turns: Vec<TurnTrace>,
164}
165
166impl SessionTimeline {
167 pub fn from_records<'a>(
169 session_id: &SessionId,
170 records: impl IntoIterator<Item = &'a JournalRecord>,
171 ) -> Self {
172 let matching = records
173 .into_iter()
174 .filter(|record| record.session_id.as_ref() == Some(session_id))
175 .cloned()
176 .collect::<Vec<_>>();
177 let turns = ordered_turn_ids(&matching)
178 .into_iter()
179 .map(|turn_id| TurnTrace::from_records(&turn_id, matching.iter()))
180 .collect();
181 Self {
182 session_id: session_id.clone(),
183 turns,
184 }
185 }
186}
187
188fn ordered_turn_ids(records: &[JournalRecord]) -> Vec<TurnId> {
189 let mut turn_ids = Vec::new();
190 for record in records {
191 if let Some(turn_id) = record.turn_id.clone() {
192 push_unique(&mut turn_ids, turn_id);
193 }
194 }
195 turn_ids
196}
197
198fn push_unique<T: Eq>(items: &mut Vec<T>, value: T) {
199 if !items.contains(&value) {
200 items.push(value);
201 }
202}
203
204fn push_unique_opt<T: Eq>(items: &mut Vec<T>, value: Option<T>) {
205 if let Some(value) = value {
206 push_unique(items, value);
207 }
208}