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 revision: Revision,
55 #[serde(skip)]
56 replacement_revision: Revision,
57 pub(super) state: ItemState,
58 #[serde(flatten)]
59 pub(super) content: ConversationContent,
60}
61
62impl ConversationItem {
63 pub fn id(&self) -> ConversationItemId {
64 self.id
65 }
66
67 pub fn message_id(&self) -> Option<&acp::MessageId> {
68 self.message_id.as_ref()
69 }
70
71 pub fn revision(&self) -> Revision {
72 self.revision
73 }
74
75 pub fn replacement_revision(&self) -> Revision {
76 self.replacement_revision
77 }
78
79 pub fn state(&self) -> ItemState {
80 self.state
81 }
82
83 pub fn content(&self) -> &ConversationContent {
84 &self.content
85 }
86
87 pub fn text(&self) -> Option<Cow<'_, str>> {
88 match &self.content {
89 ConversationContent::User(blocks) => Some(match blocks.as_slice() {
90 [] | [acp::ContentBlock::Text(_)] => plain_text(blocks),
91 blocks => Cow::Owned(map_content_blocks_to_text(&display_content_blocks(blocks))),
92 }),
93 ConversationContent::Assistant(blocks) | ConversationContent::Thought(blocks) => Some(plain_text(blocks)),
94 ConversationContent::Notice(text) => Some(Cow::Borrowed(text)),
95 ConversationContent::Tool(_) => None,
96 }
97 }
98
99 pub fn is_open(&self) -> bool {
100 self.state == ItemState::Open
101 }
102
103 pub(super) fn new(
104 id: ConversationItemId,
105 revision: Revision,
106 state: ItemState,
107 content: ConversationContent,
108 ) -> Self {
109 Self { id, message_id: None, revision, replacement_revision: revision, state, content }
110 }
111
112 pub(super) fn touch(&mut self, revision: Revision, rewrites: bool) {
113 if rewrites {
114 self.replacement_revision = revision;
115 }
116 self.revision = revision;
117 }
118}
119
120impl ConversationId {
121 pub(super) fn next() -> Self {
122 static NEXT: AtomicU64 = AtomicU64::new(1);
123 Self(NEXT.fetch_add(1, Ordering::Relaxed))
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub(super) enum MessageRole {
129 User,
130 Assistant,
131 Thought,
132}
133
134impl MessageRole {
135 pub(super) fn content(self, blocks: Vec<acp::ContentBlock>) -> ConversationContent {
136 match self {
137 Self::User => ConversationContent::User(blocks),
138 Self::Assistant => ConversationContent::Assistant(blocks),
139 Self::Thought => ConversationContent::Thought(blocks),
140 }
141 }
142
143 pub(super) fn blocks_mut(self, content: &mut ConversationContent) -> Option<&mut Vec<acp::ContentBlock>> {
144 match (self, content) {
145 (Self::User, ConversationContent::User(blocks))
146 | (Self::Assistant, ConversationContent::Assistant(blocks))
147 | (Self::Thought, ConversationContent::Thought(blocks)) => Some(blocks),
148 _ => None,
149 }
150 }
151}
152
153pub(super) fn append_block(blocks: &mut Vec<acp::ContentBlock>, block: &acp::ContentBlock) -> bool {
154 match (blocks.last_mut(), block) {
155 (_, acp::ContentBlock::Text(text)) if text.text.is_empty() => false,
156 (Some(acp::ContentBlock::Text(last)), acp::ContentBlock::Text(text)) => {
157 last.text.push_str(&text.text);
158 true
159 }
160 _ => {
161 blocks.push(block.clone());
162 true
163 }
164 }
165}
166
167impl Revision {
168 pub(super) fn advance(&mut self) -> Self {
169 self.0 = self.0.saturating_add(1);
170 *self
171 }
172}
173
174fn plain_text(blocks: &[acp::ContentBlock]) -> Cow<'_, str> {
175 match blocks {
176 [] => Cow::Borrowed(""),
177 [acp::ContentBlock::Text(text)] => Cow::Borrowed(&text.text),
178 blocks => Cow::Owned(map_content_blocks_to_text(blocks)),
179 }
180}