Skip to main content

tapes_client/core/models/
trace.rs

1//! Trace shapes: one user-visible turn, its header, and its spend.
2
3use serde::{Deserialize, Serialize};
4
5use super::ContractModel;
6use super::span::{SpanItem, SpanLinkItem};
7
8/// One user-visible turn's header. session_id / harness ids are not
9/// duplicated here — they belong to the session.
10///
11/// Models the contract's `TraceItem` schema.
12#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
13#[serde(default)]
14#[non_exhaustive]
15pub struct TraceItem {
16    /// The contract's `duration_ns`.
17    pub duration_ns: i64,
18
19    /// The contract's `ended_at`, an RFC 3339 timestamp.
20    pub ended_at: String,
21
22    /// The contract's `main_usage`.
23    #[serde(deserialize_with = "super::null_default")]
24    pub main_usage: MainUsage,
25
26    /// The derive-time fold of the closing conversation- spine llm call's
27    /// text output — the answer line for collapsed turn cards, so summary
28    /// consumers never need spans.
29    pub response_preview: String,
30
31    /// The capture origin of the turn's rows ("wire" | "transcript"),
32    /// promoted from raw_turns.source.
33    pub source: String,
34
35    /// The contract's `span_count`.
36    pub span_count: i32,
37
38    /// The contract's `started_at`, an RFC 3339 timestamp.
39    pub started_at: String,
40
41    /// The contract's `status`.
42    pub status: String,
43
44    /// A typed deriver signal ("post-compaction" for a compaction
45    /// continuation, "shadow-opener" for a shadow-only opener), promoted out
46    /// of the old metadata grab-bag.
47    pub synthetic: String,
48
49    /// The contract's `trace_id`.
50    pub trace_id: String,
51
52    /// The contract's `usage`.
53    #[serde(deserialize_with = "super::null_default")]
54    pub usage: TraceUsage,
55
56    /// Served explicitly (not omitempty): a synthetic opener has an empty
57    /// prompt, and dropping the key turns the empty string into `undefined`
58    /// on the wire, which breaks consumers that expect a string (e.g.
59    pub user_prompt: String,
60}
61
62impl ContractModel for TraceItem {
63    const SCHEMA: &'static str = "TraceItem";
64}
65
66/// A trace's total token/cost rollup. Fields are pinned (no omitempty) so the
67/// object shape is uniform across traces.
68///
69/// Models the contract's `TraceUsage` schema.
70#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
71#[serde(default)]
72#[non_exhaustive]
73pub struct TraceUsage {
74    /// The contract's `cache_creation_tokens`.
75    pub cache_creation_tokens: i64,
76
77    /// The contract's `cache_read_tokens`.
78    pub cache_read_tokens: i64,
79
80    /// The contract's `cost_usd`.
81    pub cost_usd: f64,
82
83    /// The contract's `input_tokens`.
84    pub input_tokens: i64,
85
86    /// The contract's `output_tokens`.
87    pub output_tokens: i64,
88}
89
90impl ContractModel for TraceUsage {
91    const SCHEMA: &'static str = "TraceUsage";
92}
93
94/// The task token slice of a trace: the main agent and its subagents
95/// (call_kind=main across every thread), no cache split or cost (those live
96/// on the total Usage).
97///
98/// Models the contract's `MainUsage` schema.
99#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
100#[serde(default)]
101#[non_exhaustive]
102pub struct MainUsage {
103    /// The contract's `input_tokens`.
104    pub input_tokens: i64,
105
106    /// The contract's `output_tokens`.
107    pub output_tokens: i64,
108}
109
110impl ContractModel for MainUsage {
111    const SCHEMA: &'static str = "MainUsage";
112}
113
114/// One trace with its spans. In the composite session response links are
115/// session-scoped (top level); the single-trace endpoint sets Links to the
116/// edges touching that trace.
117///
118/// Models the contract's `TraceDetail` schema.
119#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
120#[serde(default)]
121#[non_exhaustive]
122pub struct TraceDetail {
123    /// The contract's `links`.
124    #[serde(deserialize_with = "super::null_default")]
125    pub links: Vec<SpanLinkItem>,
126
127    /// Continues the standalone `GET /v1/traces/{id}` walk from the last span
128    /// of this page (pass it as `cursor`). Empty once the page reached the
129    /// trace's last span — and always empty on the copies the composite
130    /// session response embeds, where a trace is served whole.
131    #[serde(deserialize_with = "super::null_default")]
132    pub next_cursor: String,
133
134    /// The contract's `schema`.
135    pub schema: String,
136
137    /// The contract's `spans`.
138    #[serde(deserialize_with = "super::null_default")]
139    pub spans: Vec<SpanItem>,
140
141    /// The contract's `trace`.
142    #[serde(deserialize_with = "super::null_default")]
143    pub trace: TraceItem,
144}
145
146impl ContractModel for TraceDetail {
147    const SCHEMA: &'static str = "TraceDetail";
148}
149
150/// The standalone trace lookup's response: one page of a trace's spans, with
151/// its header, its links, and the owning session — which this caller, unlike
152/// the session-scoped composite's, does not already know and needs to
153/// navigate.
154///
155/// `session_id` is the one property in the whole contract marked `required`,
156/// so it is the one field here without a default: a document missing it is
157/// refused rather than read as `""`, because the schema publishes it as a
158/// guarantee and a client that silently defaulted it would hide a server
159/// that broke one.
160///
161/// Models the contract's `StandaloneTraceDetail` schema.
162#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
163#[non_exhaustive]
164pub struct StandaloneTraceDetail {
165    /// The trace's whole link set — every edge touching this trace, other
166    /// traces included — repeated on every page.
167    #[serde(default, deserialize_with = "super::null_default")]
168    pub links: Vec<SpanLinkItem>,
169
170    /// Continues the walk from the last span of this page (pass it as
171    /// `cursor`). Empty once the page reached the trace's last span. A page
172    /// may close short of `limit` on its byte budget, so its absence — not the
173    /// page's length — is what means "no more".
174    #[serde(default, deserialize_with = "super::null_default")]
175    pub next_cursor: String,
176
177    /// The contract's `schema`.
178    #[serde(default)]
179    pub schema: String,
180
181    /// The session this trace belongs to. Required by the contract.
182    pub session_id: String,
183
184    /// This page's spans, in presentation order (`seq`).
185    #[serde(default, deserialize_with = "super::null_default")]
186    pub spans: Vec<SpanItem>,
187
188    /// The trace header, repeated on every page.
189    #[serde(default, deserialize_with = "super::null_default")]
190    pub trace: TraceItem,
191}
192
193impl ContractModel for StandaloneTraceDetail {
194    const SCHEMA: &'static str = "StandaloneTraceDetail";
195}
196
197impl StandaloneTraceDetail {
198    /// Take this page's paged part out, leaving the envelope behind.
199    ///
200    /// Only `spans` is paged; `trace`, `links`, `schema`, and `session_id`
201    /// repeat on every page and stay put. What comes out is exactly
202    /// [`crate::page::Page`], so a walk over a trace's spans reaches the same
203    /// loop, the same three spellings of "no more pages", and the same guard
204    /// against a repeated cursor as every listing. `next_cursor` goes with the
205    /// page — it belongs to the walk, not to the trace — so the envelope left
206    /// behind is what a whole trace looks like once the walk is done.
207    ///
208    /// Not `into_page`: the envelope carries facts a page cannot, and a
209    /// conversion that discarded them would have to be undone by every walk.
210    #[must_use]
211    pub fn take_page(&mut self) -> crate::page::Page<SpanItem> {
212        crate::page::Page {
213            items: std::mem::take(&mut self.spans),
214            next_cursor: Some(std::mem::take(&mut self.next_cursor)),
215        }
216    }
217}
218
219/// The summaries list for one session. `schema` stamps the projection
220/// generation the rows were derived against — the same stamp the composite
221/// carries — so every trace-grain response is self-describing, not just the
222/// composite.
223///
224/// Models the contract's `TraceListResponse` schema.
225#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
226#[serde(default)]
227#[non_exhaustive]
228pub struct TraceListResponse {
229    /// The contract's `items`.
230    #[serde(deserialize_with = "super::null_default")]
231    pub items: Vec<TraceItem>,
232
233    /// The contract's `schema`.
234    pub schema: String,
235}
236
237impl ContractModel for TraceListResponse {
238    const SCHEMA: &'static str = "TraceListResponse";
239}