Skip to main content

lash_core/plugin/
history.rs

1//! History rewriting and turn-context transform plugin contracts.
2//!
3//! Split out of `plugin/mod.rs` purely for file size. All types keep
4//! their original module path via `pub use` in `plugin/mod.rs`.
5
6use std::sync::{Arc, OnceLock};
7
8use crate::SessionPolicy;
9use crate::SessionStateEnvelope;
10use crate::runtime::PersistedSessionState;
11
12use super::PluginError;
13
14/// Reason the history pipeline is being invoked.
15#[derive(Clone, Debug)]
16pub enum RewriteTrigger {
17    /// User invoked `/compact` (or an equivalent plugin command).
18    Manual { instructions: Option<String> },
19    /// Session config changed to a smaller context window.
20    WindowShrink {
21        old_max: Option<usize>,
22        new_max: Option<usize>,
23    },
24    /// Reserved for future scheduled compactors — not fired by any call
25    /// site today.
26    Periodic,
27}
28
29/// Metadata accumulated as a history rewrite pipeline runs.
30#[derive(Clone, Debug, Default)]
31pub struct HistoryRewriteMetadata {
32    pub summarized_token_count: Option<u64>,
33    pub pruned_message_count: u32,
34    pub produced_summary: bool,
35}
36
37/// Mutable state passed through the history rewrite pipeline.
38#[derive(Clone, Debug)]
39pub struct HistoryState {
40    pub messages: Vec<crate::Message>,
41    pub tool_calls: Vec<crate::ToolCallRecord>,
42    pub metadata: HistoryRewriteMetadata,
43}
44
45impl HistoryState {
46    pub fn from_state(state: &SessionStateEnvelope) -> Self {
47        let read_view = state.read_view();
48        Self {
49            messages: read_view.messages().to_vec(),
50            tool_calls: read_view.tool_calls().to_vec(),
51            metadata: HistoryRewriteMetadata::default(),
52        }
53    }
54}
55
56#[derive(Clone, Debug)]
57pub struct SessionReadView(Arc<SessionReadState>);
58
59#[derive(Debug)]
60struct SessionReadState {
61    meta: SessionReadMeta,
62    graph: SessionReadGraph,
63    read_model: crate::session_graph::SessionReadModel,
64    chronological_projection: OnceLock<Arc<crate::ChronologicalProjection>>,
65}
66
67#[derive(Clone, Debug)]
68struct SessionReadMeta {
69    session_id: String,
70    policy: SessionPolicy,
71    turn_index: usize,
72    token_usage: crate::TokenUsage,
73    last_prompt_usage: Option<crate::runtime::PromptUsage>,
74    mode_turn_options: crate::ModeTurnOptions,
75}
76
77impl SessionReadMeta {
78    fn from_state_ref(state: &SessionStateEnvelope) -> Self {
79        Self {
80            session_id: state.session_id.clone(),
81            policy: state.policy.clone(),
82            turn_index: state.turn_index,
83            token_usage: state.token_usage.clone(),
84            last_prompt_usage: state.last_prompt_usage.clone(),
85            mode_turn_options: state.mode_turn_options.clone(),
86        }
87    }
88
89    fn from_state_owned(state: SessionStateEnvelope) -> Self {
90        Self {
91            session_id: state.session_id,
92            policy: state.policy,
93            turn_index: state.turn_index,
94            token_usage: state.token_usage,
95            last_prompt_usage: state.last_prompt_usage,
96            mode_turn_options: state.mode_turn_options,
97        }
98    }
99
100    fn from_persisted_ref(state: &PersistedSessionState) -> Self {
101        Self {
102            session_id: state.session_id.clone(),
103            policy: state.policy.clone(),
104            turn_index: state.turn_index,
105            token_usage: state.token_usage.clone(),
106            last_prompt_usage: state.last_prompt_usage.clone(),
107            mode_turn_options: state.mode_turn_options.clone(),
108        }
109    }
110
111    fn with_policy(mut self, policy: SessionPolicy) -> Self {
112        self.policy = policy;
113        self
114    }
115
116    fn with_turn_index(mut self, turn_index: usize) -> Self {
117        self.turn_index = turn_index;
118        self
119    }
120
121    fn with_mode_turn_options(mut self, mode_turn_options: crate::ModeTurnOptions) -> Self {
122        self.mode_turn_options = mode_turn_options;
123        self
124    }
125
126    fn to_owned_state(&self, session_graph: crate::SessionGraph) -> SessionStateEnvelope {
127        SessionStateEnvelope {
128            session_id: self.session_id.clone(),
129            policy: self.policy.clone(),
130            session_graph,
131            turn_index: self.turn_index,
132            token_usage: self.token_usage.clone(),
133            last_prompt_usage: self.last_prompt_usage.clone(),
134            mode_turn_options: self.mode_turn_options.clone(),
135        }
136    }
137}
138
139#[derive(Debug)]
140enum SessionReadGraph {
141    Owned(Arc<crate::SessionGraph>),
142    Derived {
143        cache: OnceLock<Arc<crate::SessionGraph>>,
144        base_graph: Option<Arc<crate::SessionGraph>>,
145    },
146}
147
148impl SessionReadView {
149    pub fn from_derived_message_view(
150        mut state: SessionStateEnvelope,
151        active_events: Arc<Vec<crate::SessionEventRecord>>,
152        messages: Arc<Vec<crate::Message>>,
153        tool_calls: Arc<Vec<crate::ToolCallRecord>>,
154    ) -> Self {
155        Self(Arc::new(SessionReadState {
156            meta: SessionReadMeta::from_state_owned({
157                state.session_graph = crate::SessionGraph::default();
158                state
159            }),
160            graph: SessionReadGraph::Derived {
161                cache: OnceLock::new(),
162                base_graph: None,
163            },
164            read_model: crate::session_graph::SessionReadModel {
165                active_events,
166                messages,
167                tool_calls,
168                prompt_render_cache: Arc::new(crate::BaseRenderCache::new()),
169            },
170            chronological_projection: OnceLock::new(),
171        }))
172    }
173
174    fn from_graph_message_sequence_meta(
175        meta: SessionReadMeta,
176        base_graph: Arc<crate::SessionGraph>,
177        messages: crate::MessageSequence,
178        tool_calls: Arc<Vec<crate::ToolCallRecord>>,
179    ) -> Self {
180        let base_read_model = base_graph.read_model();
181        let active_events = base_read_model.active_events;
182        Self(Arc::new(SessionReadState {
183            meta,
184            graph: SessionReadGraph::Derived {
185                cache: OnceLock::new(),
186                base_graph: Some(base_graph),
187            },
188            read_model: crate::session_graph::SessionReadModel {
189                active_events,
190                messages: messages.shared(),
191                tool_calls,
192                prompt_render_cache: Arc::new(crate::BaseRenderCache::new()),
193            },
194            chronological_projection: OnceLock::new(),
195        }))
196    }
197
198    pub fn from_exported_state(state: &SessionStateEnvelope) -> Self {
199        let read_model = state.session_graph.read_model();
200        Self(Arc::new(SessionReadState {
201            meta: SessionReadMeta::from_state_ref(state),
202            graph: SessionReadGraph::Owned(Arc::new(state.session_graph.clone())),
203            read_model,
204            chronological_projection: OnceLock::new(),
205        }))
206    }
207
208    pub fn from_persisted_state(state: &PersistedSessionState) -> Self {
209        let graph = Arc::new(state.session_graph.clone());
210        let read_model = graph.read_model();
211        Self(Arc::new(SessionReadState {
212            meta: SessionReadMeta::from_persisted_ref(state),
213            graph: SessionReadGraph::Owned(graph),
214            read_model,
215            chronological_projection: OnceLock::new(),
216        }))
217    }
218
219    pub(crate) fn from_runtime_state(
220        state: &PersistedSessionState,
221        policy: SessionPolicy,
222        mode_turn_options: crate::ModeTurnOptions,
223    ) -> Self {
224        let graph = Arc::new(state.session_graph.clone());
225        let read_model = graph.read_model();
226        Self(Arc::new(SessionReadState {
227            meta: SessionReadMeta::from_persisted_ref(state)
228                .with_policy(policy)
229                .with_mode_turn_options(mode_turn_options),
230            graph: SessionReadGraph::Owned(graph),
231            read_model,
232            chronological_projection: OnceLock::new(),
233        }))
234    }
235
236    pub(crate) fn derived_from_persisted_state(
237        state: &PersistedSessionState,
238        policy: SessionPolicy,
239        turn_index: usize,
240        mode_turn_options: crate::ModeTurnOptions,
241        base_graph: Arc<crate::SessionGraph>,
242        messages: crate::MessageSequence,
243        tool_calls: Arc<Vec<crate::ToolCallRecord>>,
244    ) -> Self {
245        Self::from_graph_message_sequence_meta(
246            SessionReadMeta::from_persisted_ref(state)
247                .with_policy(policy)
248                .with_turn_index(turn_index)
249                .with_mode_turn_options(mode_turn_options),
250            base_graph,
251            messages,
252            tool_calls,
253        )
254    }
255
256    fn graph_arc(&self) -> &Arc<crate::SessionGraph> {
257        match &self.0.graph {
258            SessionReadGraph::Owned(graph) => graph,
259            SessionReadGraph::Derived { cache, base_graph } => cache.get_or_init(|| {
260                let mut graph = base_graph
261                    .as_ref()
262                    .map(|graph| graph.as_ref().clone())
263                    .unwrap_or_default();
264                graph.replace_active_read_state(
265                    self.0.read_model.messages.as_slice(),
266                    self.0.read_model.tool_calls.as_slice(),
267                );
268                Arc::new(graph)
269            }),
270        }
271    }
272
273    pub fn session_id(&self) -> &str {
274        &self.0.meta.session_id
275    }
276
277    pub fn policy(&self) -> &SessionPolicy {
278        &self.0.meta.policy
279    }
280
281    fn session_graph(&self) -> &crate::SessionGraph {
282        self.graph_arc().as_ref()
283    }
284
285    pub fn materialized_session_graph(&self) -> crate::SessionGraph {
286        self.session_graph().clone()
287    }
288
289    pub fn messages(&self) -> &[crate::Message] {
290        self.0.read_model.messages.as_slice()
291    }
292
293    pub fn tool_calls(&self) -> &[crate::ToolCallRecord] {
294        self.0.read_model.tool_calls.as_slice()
295    }
296
297    pub fn active_events(&self) -> &[crate::SessionEventRecord] {
298        self.0.read_model.active_events.as_slice()
299    }
300
301    pub fn chronological_projection(&self) -> crate::ChronologicalProjection {
302        crate::ChronologicalProjection::from_read_model(&self.0.read_model)
303    }
304
305    pub(crate) fn shared_chronological_projection(&self) -> Arc<crate::ChronologicalProjection> {
306        Arc::clone(self.0.chronological_projection.get_or_init(|| {
307            Arc::new(crate::ChronologicalProjection::from_read_model(
308                &self.0.read_model,
309            ))
310        }))
311    }
312
313    pub fn message_tree(&self) -> Vec<crate::SessionMessageTreeNode> {
314        self.session_graph().message_tree()
315    }
316
317    pub fn turn_index(&self) -> usize {
318        self.0.meta.turn_index
319    }
320
321    pub fn token_usage(&self) -> &crate::TokenUsage {
322        &self.0.meta.token_usage
323    }
324
325    pub fn last_prompt_usage(&self) -> Option<&crate::runtime::PromptUsage> {
326        self.0.meta.last_prompt_usage.as_ref()
327    }
328
329    pub fn mode_turn_options(&self) -> &crate::ModeTurnOptions {
330        &self.0.meta.mode_turn_options
331    }
332
333    pub fn to_owned_state(&self) -> SessionStateEnvelope {
334        self.0.meta.to_owned_state(self.session_graph().clone())
335    }
336}
337
338/// Context passed to a turn-context transform.
339#[derive(Clone)]
340pub struct TurnTransformContext {
341    pub session_id: String,
342    pub state: SessionReadView,
343    pub prompt_usage: Option<crate::runtime::PromptUsage>,
344    pub max_context_tokens: Option<usize>,
345    pub host: Arc<dyn super::HistoryHost>,
346}
347
348/// Context passed to a history rewriter.
349#[derive(Clone)]
350pub struct RewriteContext {
351    pub session_id: String,
352    pub trigger: RewriteTrigger,
353    pub state: SessionReadView,
354    pub host: Arc<dyn super::HistoryHost>,
355}
356
357#[derive(Debug, thiserror::Error, Clone)]
358pub enum HistoryError {
359    #[error("history pipeline error: {0}")]
360    Pipeline(String),
361    #[error("history session error: {0}")]
362    Session(String),
363}
364
365impl From<PluginError> for HistoryError {
366    fn from(value: PluginError) -> Self {
367        Self::Session(value.to_string())
368    }
369}
370
371/// Prepares the ephemeral turn context presented to the model.
372#[async_trait::async_trait]
373pub trait TurnContextTransform: Send + Sync {
374    fn id(&self) -> &'static str;
375    async fn transform(
376        &self,
377        ctx: &TurnTransformContext,
378        input: crate::session_model::context::PreparedContext,
379    ) -> Result<crate::session_model::context::PreparedContext, HistoryError>;
380}
381
382/// Performs a permanent transform on persisted history (compaction,
383/// overflow recovery, manual `/compact`, ...).
384#[async_trait::async_trait]
385pub trait HistoryRewriter: Send + Sync {
386    fn id(&self) -> &'static str;
387    fn accepts(&self, _trigger: &RewriteTrigger) -> bool {
388        true
389    }
390    async fn rewrite(
391        &self,
392        ctx: &RewriteContext,
393        input: HistoryState,
394    ) -> Result<HistoryState, HistoryError>;
395}