Skip to main content

poe2_agent/
trace.rs

1//! Agent reasoning trace — a structured record of everything that happens
2//! during a single `respond()` call.
3//!
4//! The trace captures the full internal reasoning loop: user input, each
5//! LLM round's tool calls and results, streamed text output, and final
6//! token usage. Consumers (e.g. the web backend) decide how to persist it
7//! (JSON file, database, etc.).
8
9use serde::Serialize;
10
11use crate::llm::Usage;
12
13/// Complete trace of a single agent response.
14#[derive(Debug, Clone, Serialize)]
15pub struct AgentTrace {
16    /// ISO-8601 timestamp when the response started.
17    pub started_at: String,
18    /// The user message that triggered this response.
19    pub user_message: String,
20    /// Conversation history provided as context (user and assistant turns).
21    pub history: Vec<TraceMessage>,
22    /// Ordered list of events that occurred during the response.
23    pub events: Vec<TraceEvent>,
24    /// Cumulative token usage across all LLM rounds.
25    pub usage: Option<TraceUsage>,
26}
27
28/// A message from conversation history.
29#[derive(Debug, Clone, Serialize)]
30pub struct TraceMessage {
31    pub role: String,
32    pub content: String,
33}
34
35/// A single event in the agent reasoning loop.
36#[derive(Debug, Clone, Serialize)]
37#[serde(tag = "type")]
38pub enum TraceEvent {
39    /// The LLM decided to call a tool.
40    #[serde(rename = "tool_call")]
41    ToolCall {
42        /// Tool name (e.g. "search_trade", "get_build_stats").
43        name: String,
44        /// Raw JSON arguments the LLM passed.
45        arguments: String,
46    },
47    /// A tool returned a result.
48    #[serde(rename = "tool_result")]
49    ToolResult {
50        /// Tool name.
51        name: String,
52        /// The full result content (JSON string).
53        content: String,
54    },
55    /// A chunk of streamed text from the LLM's final answer.
56    #[serde(rename = "text_delta")]
57    TextDelta {
58        /// The text fragment.
59        text: String,
60    },
61    /// The agent produced a build mutation (item creation/modification).
62    #[serde(rename = "build_mutation")]
63    BuildMutation {
64        /// Label describing the mutation.
65        label: String,
66    },
67}
68
69/// Token usage summary, mirroring [`Usage`] in a serializable form.
70#[derive(Debug, Clone, Serialize)]
71pub struct TraceUsage {
72    pub input_tokens: u32,
73    pub output_tokens: u32,
74    pub cached_tokens: u32,
75    pub total_tokens: u32,
76}
77
78impl From<&Usage> for TraceUsage {
79    fn from(u: &Usage) -> Self {
80        Self {
81            input_tokens: u.input_tokens,
82            output_tokens: u.output_tokens,
83            cached_tokens: u.cached_tokens(),
84            total_tokens: u.total_tokens,
85        }
86    }
87}
88
89/// Builder that accumulates trace events during an agent response.
90///
91/// Created at the start of `respond()`, written to as events occur,
92/// then finalized into an [`AgentTrace`].
93pub(crate) struct TraceBuilder {
94    started_at: String,
95    user_message: String,
96    history: Vec<TraceMessage>,
97    events: Vec<TraceEvent>,
98}
99
100impl TraceBuilder {
101    /// Start a new trace for a response to `user_message`.
102    pub fn new(user_message: &str, history: impl IntoIterator<Item = TraceMessage>) -> Self {
103        Self {
104            started_at: now_iso8601(),
105            user_message: user_message.to_owned(),
106            history: history.into_iter().collect(),
107            events: Vec::new(),
108        }
109    }
110
111    /// Record a tool call.
112    pub fn tool_call(&mut self, name: &str, arguments: &str) {
113        self.events.push(TraceEvent::ToolCall {
114            name: name.to_owned(),
115            arguments: arguments.to_owned(),
116        });
117    }
118
119    /// Record a tool result.
120    pub fn tool_result(&mut self, name: &str, content: &str) {
121        self.events.push(TraceEvent::ToolResult {
122            name: name.to_owned(),
123            content: content.to_owned(),
124        });
125    }
126
127    /// Record a text delta from the final streamed answer.
128    pub fn text_delta(&mut self, text: &str) {
129        self.events.push(TraceEvent::TextDelta {
130            text: text.to_owned(),
131        });
132    }
133
134    /// Record a build mutation.
135    pub fn build_mutation(&mut self, label: &str) {
136        self.events.push(TraceEvent::BuildMutation {
137            label: label.to_owned(),
138        });
139    }
140
141    /// Finalize the trace with usage stats.
142    pub fn finish(self, usage: &Usage) -> AgentTrace {
143        AgentTrace {
144            started_at: self.started_at,
145            user_message: self.user_message,
146            history: self.history,
147            events: self.events,
148            usage: Some(TraceUsage::from(usage)),
149        }
150    }
151}
152
153fn now_iso8601() -> String {
154    // Use std SystemTime — no chrono dependency needed.
155    let now = std::time::SystemTime::now();
156    let duration = now
157        .duration_since(std::time::UNIX_EPOCH)
158        .unwrap_or_default();
159    let secs = duration.as_secs();
160
161    // Format as UTC: YYYY-MM-DDTHH:MM:SSZ
162    let days = secs / 86400;
163    let time_secs = secs % 86400;
164    let hours = time_secs / 3600;
165    let minutes = (time_secs % 3600) / 60;
166    let seconds = time_secs % 60;
167
168    // Days since epoch to Y-M-D (civil calendar).
169    let (y, m, d) = days_to_ymd(days);
170    format!("{y:04}-{m:02}-{d:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
171}
172
173/// Convert days since Unix epoch to (year, month, day).
174fn days_to_ymd(days: u64) -> (u64, u64, u64) {
175    // Algorithm from Howard Hinnant's date library (public domain).
176    let z = days + 719468;
177    let era = z / 146097;
178    let doe = z - era * 146097;
179    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
180    let y = yoe + era * 400;
181    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
182    let mp = (5 * doy + 2) / 153;
183    let d = doy - (153 * mp + 2) / 5 + 1;
184    let m = if mp < 10 { mp + 3 } else { mp - 9 };
185    let y = if m <= 2 { y + 1 } else { y };
186    (y, m, d)
187}