Skip to main content

lutum_protocol/
transcript.rs

1use std::{any::Any, fmt, sync::Arc};
2
3use crate::conversation::{AssistantTurnItem, RawJson, ToolCallId, ToolName};
4
5/// Role or category of a committed turn in the session transcript.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum TurnRole {
8    System,
9    Developer,
10    User,
11    Assistant,
12}
13
14/// Borrowed view of a tool-call item's key fields.
15pub struct ToolCallItemView<'a> {
16    pub id: &'a ToolCallId,
17    pub name: &'a ToolName,
18    pub arguments: &'a RawJson,
19}
20
21/// Borrowed view of a tool-result item's key fields.
22pub struct ToolResultItemView<'a> {
23    pub id: &'a ToolCallId,
24    pub name: &'a ToolName,
25    pub arguments: &'a RawJson,
26    pub result: &'a RawJson,
27}
28
29/// Read-only view of a single item within a committed turn.
30///
31/// Implementations should return `Some` for exactly one accessor that matches
32/// the item's kind, and `None` for all others.
33pub trait ItemView {
34    /// Returns the text content of this item, if it is a text item.
35    fn as_text(&self) -> Option<&str>;
36
37    /// Returns reasoning text, if this is a reasoning item.
38    fn as_reasoning(&self) -> Option<&str>;
39
40    /// Returns refusal text, if this is a refusal item.
41    fn as_refusal(&self) -> Option<&str>;
42
43    /// Returns tool-call details, if this is a tool-call item.
44    fn as_tool_call(&self) -> Option<ToolCallItemView<'_>>;
45
46    /// Returns tool-result details, if this is a tool-result item.
47    fn as_tool_result(&self) -> Option<ToolResultItemView<'_>>;
48}
49
50/// Read-only view of a committed turn in the session transcript.
51///
52/// Object-safe: items are accessed by index so no generic iterators are needed.
53pub trait TurnView: fmt::Debug + Send + Sync {
54    /// Role or category of this turn.
55    fn role(&self) -> TurnRole;
56
57    /// Number of items in this turn.
58    fn item_count(&self) -> usize;
59
60    /// Returns a reference to the item at `index`, or `None` if out of bounds.
61    fn item_at(&self, index: usize) -> Option<&dyn ItemView>;
62
63    /// Returns this turn as `Any` for adapter-specific downcasting.
64    fn as_any(&self) -> &dyn Any;
65}
66
67impl dyn TurnView {
68    pub fn downcast_ref<T: TurnView + 'static>(&self) -> Option<&T> {
69        self.as_any().downcast_ref::<T>()
70    }
71}
72
73// ── blanket helpers ───────────────────────────────────────────────────────────
74
75/// Iterator over items in a `TurnView`.
76pub struct TurnItemIter<'a> {
77    turn: &'a dyn TurnView,
78    index: usize,
79}
80
81impl<'a> TurnItemIter<'a> {
82    pub fn new(turn: &'a dyn TurnView) -> Self {
83        Self { turn, index: 0 }
84    }
85}
86
87impl<'a> Iterator for TurnItemIter<'a> {
88    type Item = &'a dyn ItemView;
89
90    fn next(&mut self) -> Option<Self::Item> {
91        let item = self.turn.item_at(self.index)?;
92        self.index += 1;
93        Some(item)
94    }
95}
96
97// ── core-provided assistant-turn view ─────────────────────────────────────────
98
99/// A committed assistant turn backed by the core `AssistantTurnItem` list.
100///
101/// Adapters that produce `AssistantTurn`-backed results can use this type
102/// directly.  Provider-specific adapters may define their own concrete type
103/// instead.
104#[derive(Debug)]
105pub struct AssistantTurnView {
106    items: Vec<CoreAssistantItemView>,
107}
108
109impl AssistantTurnView {
110    /// Construct a view from a slice of `AssistantTurnItem` values.
111    pub fn from_items(items: &[AssistantTurnItem]) -> Self {
112        Self {
113            items: items.iter().map(CoreAssistantItemView::from_item).collect(),
114        }
115    }
116}
117
118impl TurnView for AssistantTurnView {
119    fn role(&self) -> TurnRole {
120        TurnRole::Assistant
121    }
122
123    fn item_count(&self) -> usize {
124        self.items.len()
125    }
126
127    fn item_at(&self, index: usize) -> Option<&dyn ItemView> {
128        self.items.get(index).map(|v| v as &dyn ItemView)
129    }
130
131    fn as_any(&self) -> &dyn Any {
132        self
133    }
134}
135
136#[derive(Debug)]
137enum CoreAssistantItemKind {
138    Text(String),
139    Reasoning(String),
140    Refusal(String),
141    ToolCall {
142        id: ToolCallId,
143        name: ToolName,
144        arguments: RawJson,
145    },
146}
147
148#[derive(Debug)]
149struct CoreAssistantItemView {
150    kind: CoreAssistantItemKind,
151}
152
153impl CoreAssistantItemView {
154    fn from_item(item: &AssistantTurnItem) -> Self {
155        let kind = match item {
156            AssistantTurnItem::Text(t) => CoreAssistantItemKind::Text(t.clone()),
157            AssistantTurnItem::Reasoning(t) => CoreAssistantItemKind::Reasoning(t.clone()),
158            AssistantTurnItem::Refusal(t) => CoreAssistantItemKind::Refusal(t.clone()),
159            AssistantTurnItem::ToolCall {
160                id,
161                name,
162                arguments,
163            } => CoreAssistantItemKind::ToolCall {
164                id: id.clone(),
165                name: name.clone(),
166                arguments: arguments.clone(),
167            },
168        };
169        Self { kind }
170    }
171}
172
173impl ItemView for CoreAssistantItemView {
174    fn as_text(&self) -> Option<&str> {
175        match &self.kind {
176            CoreAssistantItemKind::Text(t) => Some(t),
177            _ => None,
178        }
179    }
180
181    fn as_reasoning(&self) -> Option<&str> {
182        match &self.kind {
183            CoreAssistantItemKind::Reasoning(t) => Some(t),
184            _ => None,
185        }
186    }
187
188    fn as_refusal(&self) -> Option<&str> {
189        match &self.kind {
190            CoreAssistantItemKind::Refusal(t) => Some(t),
191            _ => None,
192        }
193    }
194
195    fn as_tool_call(&self) -> Option<ToolCallItemView<'_>> {
196        match &self.kind {
197            CoreAssistantItemKind::ToolCall {
198                id,
199                name,
200                arguments,
201            } => Some(ToolCallItemView {
202                id,
203                name,
204                arguments,
205            }),
206            _ => None,
207        }
208    }
209
210    fn as_tool_result(&self) -> Option<ToolResultItemView<'_>> {
211        None // assistant items don't carry tool results
212    }
213}
214
215// ── Arc-erased committed turn ─────────────────────────────────────────────────
216
217/// A committed turn stored in the session, erased behind `Arc<dyn TurnView>`.
218///
219/// Using `Arc` allows `Session` (which is `Clone`) to share committed turns
220/// cheaply across branch sessions without copying.
221pub type CommittedTurn = Arc<dyn TurnView + Send + Sync>;