Skip to main content

yapi_types/
session.rs

1//! Lines of a session file: one header, then entries forming a tree.
2//!
3//! Mirrors `packages/coding-agent/src/core/session-manager.ts` in pi `v1.0.0`. Each
4//! line is one JSON object; [`crate::json::to_string`] plus `\n` writes it.
5#![allow(
6    missing_docs,
7    reason = "fields mirror pi's TypeScript types; contracts are noted where they differ from the name"
8)]
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::message::{Content, Message, Usage};
14use crate::present;
15
16/// Session file format version that pi `v1.0.0` writes.
17pub const CURRENT_VERSION: u32 = 3;
18
19/// One line of a session file, tagged by `type`.
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "type", rename_all = "snake_case")]
22pub enum FileEntry {
23    /// The first line.
24    Session(SessionHeader),
25    Message(MessageEntry),
26    ThinkingLevelChange(ThinkingLevelChangeEntry),
27    ModelChange(ModelChangeEntry),
28    Usage(UsageEntry),
29    Compaction(CompactionEntry),
30    BranchSummary(BranchSummaryEntry),
31    /// Extension state; not part of the model context.
32    Custom(CustomEntry),
33    /// Extension message; part of the model context.
34    CustomMessage(CustomMessageEntry),
35    ContextEdit(ContextEditEntry),
36    Label(LabelEntry),
37    SessionInfo(SessionInfoEntry),
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct SessionHeader {
43    /// Absent in v1 files.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub version: Option<u32>,
46    pub id: String,
47    /// ISO 8601 time.
48    pub timestamp: String,
49    pub cwd: String,
50    /// Path of the session this one was forked or branched from.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub parent_session: Option<String>,
53    /// v1 only.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub provider: Option<String>,
56    /// v1 only.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub model_id: Option<String>,
59    /// v1 only.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub thinking_level: Option<String>,
62    /// v1 only.
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub branched_from: Option<String>,
65}
66
67/// Tree position shared by every entry.
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct EntryMeta {
71    /// 8 hex digits, or a UUID when a collision fallback was needed.
72    pub id: String,
73    /// `None` for the root entry.
74    pub parent_id: Option<String>,
75    /// ISO 8601 time.
76    pub timestamp: String,
77}
78
79#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
80pub struct MessageEntry {
81    #[serde(flatten)]
82    pub meta: EntryMeta,
83    pub message: Message,
84}
85
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct ThinkingLevelChangeEntry {
89    #[serde(flatten)]
90    pub meta: EntryMeta,
91    pub thinking_level: String,
92}
93
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct ModelChangeEntry {
97    #[serde(flatten)]
98    pub meta: EntryMeta,
99    pub provider: String,
100    pub model_id: String,
101}
102
103/// Usage outside model responses, such as cache warming.
104#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
105pub struct UsageEntry {
106    #[serde(flatten)]
107    pub meta: EntryMeta,
108    pub kind: String,
109    pub provider: String,
110    pub model: String,
111    pub usage: Usage,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub note: Option<String>,
114}
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct CompactionEntry {
119    #[serde(flatten)]
120    pub meta: EntryMeta,
121    pub summary: String,
122    /// `Some(None)` when nothing is kept; `None` in migrated v1 files whose kept index
123    /// pointed past the end.
124    #[serde(
125        default,
126        deserialize_with = "present",
127        skip_serializing_if = "Option::is_none"
128    )]
129    pub first_kept_entry_id: Option<Option<String>>,
130    pub tokens_before: u64,
131    #[serde(
132        default,
133        deserialize_with = "present",
134        skip_serializing_if = "Option::is_none"
135    )]
136    pub details: Option<Value>,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub usage: Option<Usage>,
139    /// Produced by an extension hook rather than pi's summarizer.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub from_hook: Option<bool>,
142    /// System prompt checkpoint at the cut; always a system message.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub system_message: Option<Box<Message>>,
145}
146
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct BranchSummaryEntry {
150    #[serde(flatten)]
151    pub meta: EntryMeta,
152    pub from_id: String,
153    pub summary: String,
154    #[serde(
155        default,
156        deserialize_with = "present",
157        skip_serializing_if = "Option::is_none"
158    )]
159    pub details: Option<Value>,
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub usage: Option<Usage>,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub from_hook: Option<bool>,
164}
165
166/// pi writes the tree position last for this entry.
167#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct CustomEntry {
170    pub custom_type: String,
171    #[serde(
172        default,
173        deserialize_with = "present",
174        skip_serializing_if = "Option::is_none"
175    )]
176    pub data: Option<Value>,
177    #[serde(flatten)]
178    pub meta: EntryMeta,
179}
180
181/// pi writes the tree position last for this entry.
182#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct CustomMessageEntry {
185    pub custom_type: String,
186    pub content: Content,
187    pub display: bool,
188    #[serde(
189        default,
190        deserialize_with = "present",
191        skip_serializing_if = "Option::is_none"
192    )]
193    pub details: Option<Value>,
194    #[serde(flatten)]
195    pub meta: EntryMeta,
196}
197
198/// Replaces or removes an earlier entry's contribution to the model context.
199#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct ContextEditEntry {
202    #[serde(flatten)]
203    pub meta: EntryMeta,
204    pub target_id: String,
205    /// `None` removes the target from the context.
206    pub replacement: Option<Replacement>,
207}
208
209#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
210pub struct Replacement {
211    pub content: Content,
212}
213
214#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct LabelEntry {
217    #[serde(flatten)]
218    pub meta: EntryMeta,
219    pub target_id: String,
220    /// Absent when the label was cleared.
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub label: Option<String>,
223}
224
225#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
226pub struct SessionInfoEntry {
227    #[serde(flatten)]
228    pub meta: EntryMeta,
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub name: Option<String>,
231}
232
233impl FileEntry {
234    /// The id, parent and timestamp of a session entry; `None` for the header.
235    pub fn meta(&self) -> Option<&EntryMeta> {
236        Some(match self {
237            FileEntry::Session(_) => return None,
238            FileEntry::Message(entry) => &entry.meta,
239            FileEntry::ThinkingLevelChange(entry) => &entry.meta,
240            FileEntry::ModelChange(entry) => &entry.meta,
241            FileEntry::Usage(entry) => &entry.meta,
242            FileEntry::Compaction(entry) => &entry.meta,
243            FileEntry::BranchSummary(entry) => &entry.meta,
244            FileEntry::Custom(entry) => &entry.meta,
245            FileEntry::CustomMessage(entry) => &entry.meta,
246            FileEntry::ContextEdit(entry) => &entry.meta,
247            FileEntry::Label(entry) => &entry.meta,
248            FileEntry::SessionInfo(entry) => &entry.meta,
249        })
250    }
251}