1use crate::content::{display_content_blocks, map_content_blocks_to_text};
2use agent_client_protocol::schema::v2 as acp;
3use schemars::JsonSchema;
4use serde::Serialize;
5use std::borrow::Cow;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use super::tool_calls::ToolCall;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
12pub struct ConversationId(u64);
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, JsonSchema)]
16pub struct ConversationItemId(pub(super) u64);
17
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, JsonSchema)]
19pub struct Revision(u64);
20
21impl Revision {
22 pub fn value(self) -> u64 {
23 self.0
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
30#[serde(rename_all = "snake_case")]
31pub enum ItemState {
32 Open,
33 Sealed,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
38#[serde(tag = "kind", content = "content", rename_all = "snake_case")]
39pub enum ConversationContent {
40 User(Vec<acp::ContentBlock>),
41 Assistant(Vec<acp::ContentBlock>),
42 Thought(Vec<acp::ContentBlock>),
43 Tool(ToolCall),
44 Notice(String),
46}
47
48#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
50#[serde(rename_all = "camelCase")]
51pub struct ConversationItem {
52 id: ConversationItemId,
53 pub(super) message_id: Option<acp::MessageId>,
54 #[serde(skip)]
55 pub(super) preserve_user_display: bool,
56 revision: Revision,
57 #[serde(skip)]
58 replacement_revision: Revision,
59 pub(super) state: ItemState,
60 #[serde(flatten)]
61 pub(super) content: ConversationContent,
62}
63
64impl ConversationItem {
65 pub fn id(&self) -> ConversationItemId {
66 self.id
67 }
68
69 pub fn message_id(&self) -> Option<&acp::MessageId> {
70 self.message_id.as_ref()
71 }
72
73 pub fn revision(&self) -> Revision {
74 self.revision
75 }
76
77 pub fn replacement_revision(&self) -> Revision {
78 self.replacement_revision
79 }
80
81 pub fn state(&self) -> ItemState {
82 self.state
83 }
84
85 pub fn content(&self) -> &ConversationContent {
86 &self.content
87 }
88
89 pub fn text(&self) -> Option<Cow<'_, str>> {
90 match &self.content {
91 ConversationContent::User(blocks) => Some(match blocks.as_slice() {
92 [] | [acp::ContentBlock::Text(_)] => plain_text(blocks),
93 blocks => Cow::Owned(map_content_blocks_to_text(&display_content_blocks(blocks))),
94 }),
95 ConversationContent::Assistant(blocks) | ConversationContent::Thought(blocks) => Some(plain_text(blocks)),
96 ConversationContent::Notice(text) => Some(Cow::Borrowed(text)),
97 ConversationContent::Tool(_) => None,
98 }
99 }
100
101 pub fn is_open(&self) -> bool {
102 self.state == ItemState::Open
103 }
104
105 pub(super) fn new(
106 id: ConversationItemId,
107 revision: Revision,
108 state: ItemState,
109 content: ConversationContent,
110 ) -> Self {
111 Self {
112 id,
113 message_id: None,
114 preserve_user_display: false,
115 revision,
116 replacement_revision: revision,
117 state,
118 content,
119 }
120 }
121
122 pub(super) fn touch(&mut self, revision: Revision, rewrites: bool) {
123 if rewrites {
124 self.replacement_revision = revision;
125 }
126 self.revision = revision;
127 }
128}
129
130impl ConversationId {
131 pub(super) fn next() -> Self {
132 static NEXT: AtomicU64 = AtomicU64::new(1);
133 Self(NEXT.fetch_add(1, Ordering::Relaxed))
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub(super) enum MessageRole {
139 User,
140 Assistant,
141 Thought,
142}
143
144impl MessageRole {
145 pub(super) fn content(self, blocks: Vec<acp::ContentBlock>) -> ConversationContent {
146 match self {
147 Self::User => ConversationContent::User(blocks),
148 Self::Assistant => ConversationContent::Assistant(blocks),
149 Self::Thought => ConversationContent::Thought(blocks),
150 }
151 }
152
153 pub(super) fn blocks_mut(self, content: &mut ConversationContent) -> Option<&mut Vec<acp::ContentBlock>> {
154 match (self, content) {
155 (Self::User, ConversationContent::User(blocks))
156 | (Self::Assistant, ConversationContent::Assistant(blocks))
157 | (Self::Thought, ConversationContent::Thought(blocks)) => Some(blocks),
158 _ => None,
159 }
160 }
161}
162
163pub(super) fn append_block(blocks: &mut Vec<acp::ContentBlock>, block: &acp::ContentBlock) -> bool {
164 match (blocks.last_mut(), block) {
165 (_, acp::ContentBlock::Text(text)) if text.text.is_empty() => false,
166 (Some(acp::ContentBlock::Text(last)), acp::ContentBlock::Text(text)) => {
167 last.text.push_str(&text.text);
168 true
169 }
170 _ => {
171 blocks.push(block.clone());
172 true
173 }
174 }
175}
176
177impl Revision {
178 pub(super) fn advance(&mut self) -> Self {
179 self.0 = self.0.saturating_add(1);
180 *self
181 }
182}
183
184fn plain_text(blocks: &[acp::ContentBlock]) -> Cow<'_, str> {
185 match blocks {
186 [] => Cow::Borrowed(""),
187 [acp::ContentBlock::Text(text)] => Cow::Borrowed(&text.text),
188 blocks => Cow::Owned(map_content_blocks_to_text(blocks)),
189 }
190}