1use indexmap::IndexMap;
9
10use crate::content::UserTextField;
11use crate::message::{Message, Sections, SystemMessage, ToolDecl, ToolRef};
12use crate::primitives::TimestampMs;
13use crate::stream::Context;
14
15#[derive(Clone, Debug, Default, PartialEq)]
17pub struct SystemState {
18 pub content: String,
20 pub sections: IndexMap<String, String>,
22 pub tools: Vec<ToolDecl>,
24 pub first_timestamp: Option<TimestampMs>,
26}
27
28impl SystemState {
29 pub fn fold<'a>(systems: impl IntoIterator<Item = &'a SystemMessage>) -> Self {
31 let mut content = Vec::new();
32 let mut sections = IndexMap::new();
33 let mut tools: IndexMap<String, ToolDecl> = IndexMap::new();
34 let mut first_timestamp = None;
35 for message in systems {
36 first_timestamp.get_or_insert(message.timestamp);
37 let text = message.content.text("\n");
38 if !text.is_empty() {
39 content.push(text);
40 }
41 for (name, value) in message.sections.iter().flatten() {
42 match value {
43 Some(text) => {
44 sections.insert(name.clone(), text.clone());
45 }
46 None => {
47 sections.shift_remove(name);
48 }
49 }
50 }
51 for removed in message.tools_removed.iter().flatten() {
52 tools.shift_remove(&removed.name);
53 }
54 for added in message.tools_added.iter().flatten() {
55 tools.insert(added.name.clone(), added.clone());
56 }
57 }
58 Self {
59 content: content.join("\n\n"),
60 sections,
61 tools: tools.into_values().collect(),
62 first_timestamp,
63 }
64 }
65
66 #[must_use]
69 pub fn into_message(self) -> Option<SystemMessage> {
70 if self.first_timestamp.is_none() && self.tools.is_empty() {
71 return None;
72 }
73 Some(SystemMessage {
74 content: UserTextField::Text(self.content),
75 sections: (!self.sections.is_empty()).then(|| {
76 self.sections
77 .into_iter()
78 .map(|(name, text)| (name, Some(text)))
79 .collect()
80 }),
81 tools_added: (!self.tools.is_empty()).then_some(self.tools),
82 tools_removed: None,
83 timestamp: self.first_timestamp.unwrap_or(0),
84 })
85 }
86}
87
88fn systems(messages: &[Message]) -> impl Iterator<Item = &SystemMessage> {
89 messages.iter().filter_map(|message| match message {
90 Message::System(system) => Some(system),
91 _ => None,
92 })
93}
94
95#[must_use]
97pub fn current_system(messages: &[Message]) -> SystemState {
98 SystemState::fold(systems(messages))
99}
100
101#[must_use]
103pub fn current_system_message(messages: &[Message]) -> Option<SystemMessage> {
104 current_system(messages).into_message()
105}
106
107#[must_use]
109pub fn current_tools(messages: &[Message]) -> Vec<ToolDecl> {
110 current_system(messages).tools
111}
112
113#[must_use]
115pub fn declared_tools(messages: &[Message]) -> Vec<ToolDecl> {
116 let mut definitions: IndexMap<String, ToolDecl> = IndexMap::new();
117 for added in systems(messages).flat_map(|m| m.tools_added.iter().flatten()) {
118 definitions.insert(added.name.clone(), added.clone());
119 }
120 definitions.into_values().collect()
121}
122
123#[must_use]
125pub fn system_message_text(message: &SystemMessage) -> String {
126 std::iter::once(message.content.text("\n"))
127 .chain(
128 message
129 .sections
130 .iter()
131 .flatten()
132 .filter_map(|(_, text)| text.clone()),
133 )
134 .filter(|part| !part.is_empty())
135 .collect::<Vec<_>>()
136 .join("\n\n")
137}
138
139#[must_use]
141pub fn render_system_message_update(message: &SystemMessage) -> String {
142 let mut parts = Vec::new();
143 let text = message.content.text("\n");
144 if !text.is_empty() {
145 parts.push(text);
146 }
147 for (name, value) in message.sections.iter().flatten() {
148 parts.push(match value {
149 None => format!("Removed system prompt section \"{name}\"."),
150 Some(value) => format!("Updated system prompt section \"{name}\":\n\n{value}"),
151 });
152 }
153 parts.join("\n\n")
154}
155
156#[must_use]
159pub fn diff_sections(
160 previous: &IndexMap<String, String>,
161 current: &IndexMap<String, String>,
162) -> Option<Sections> {
163 let mut patch = Sections::new();
164 for (name, text) in current {
165 if previous.get(name) != Some(text) {
166 patch.insert(name.clone(), Some(text.clone()));
167 }
168 }
169 for name in previous.keys() {
170 if !current.contains_key(name) {
171 patch.insert(name.clone(), None);
172 }
173 }
174 (!patch.is_empty()).then_some(patch)
175}
176
177#[must_use]
180pub fn initial_system_message(system_prompt: &str, tools: &[ToolDecl]) -> Option<SystemMessage> {
181 if system_prompt.is_empty() && tools.is_empty() {
182 return None;
183 }
184 Some(SystemMessage {
185 content: UserTextField::Text(system_prompt.to_owned()),
186 sections: None,
187 tools_added: (!tools.is_empty()).then(|| tools.to_vec()),
188 tools_removed: None,
189 timestamp: 0,
190 })
191}
192
193#[must_use]
195pub fn collapse_system_messages(context: Context) -> Context {
196 let head = current_system_message(&context.messages);
197 let rest = context
198 .messages
199 .into_iter()
200 .filter(|message| !matches!(message, Message::System(_)));
201 Context {
202 messages: head.map(Message::System).into_iter().chain(rest).collect(),
203 }
204}
205
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub enum ToolChangeSupport {
209 None,
211 Changes,
213 Additive,
215}
216
217#[derive(Clone, Debug, Default, PartialEq)]
219pub struct ResolvedTools {
220 pub request_tools: Vec<ToolDecl>,
222 pub folded_system_messages: usize,
224}
225
226#[must_use]
228pub fn resolve_transcript_tools(messages: &[Message], support: ToolChangeSupport) -> ResolvedTools {
229 let system_messages: Vec<&SystemMessage> = systems(messages).collect();
230 match support {
231 ToolChangeSupport::None => ResolvedTools {
232 request_tools: SystemState::fold(system_messages.iter().copied()).tools,
233 folded_system_messages: system_messages.len(),
234 },
235 ToolChangeSupport::Changes => ResolvedTools::default(),
236 ToolChangeSupport::Additive => {
237 let leading = usize::from(matches!(messages.first(), Some(Message::System(_))));
238 let folded =
239 leading.max(last_non_additive_change(&system_messages).map_or(0, |i| i + 1));
240 ResolvedTools {
241 request_tools: SystemState::fold(system_messages[..folded].iter().copied()).tools,
242 folded_system_messages: folded,
243 }
244 }
245 }
246}
247
248fn last_non_additive_change(system_messages: &[&SystemMessage]) -> Option<usize> {
249 let mut declared = std::collections::HashSet::new();
250 let mut last = None;
251 for (index, message) in system_messages.iter().enumerate() {
252 if message
253 .tools_removed
254 .as_ref()
255 .is_some_and(|removed| !removed.is_empty())
256 {
257 last = Some(index);
258 }
259 for tool in message.tools_added.iter().flatten() {
260 if !declared.insert(tool.name.as_str()) {
261 last = Some(index);
262 }
263 }
264 }
265 last
266}
267
268#[must_use]
270pub fn has_non_additive_tool_changes(messages: &[Message]) -> bool {
271 last_non_additive_change(&systems(messages).collect::<Vec<_>>()).is_some()
272}
273
274#[must_use]
276pub fn tool_state_changes(
277 previous: &[ToolDecl],
278 current: &[ToolDecl],
279) -> (Vec<ToolDecl>, Vec<ToolRef>) {
280 let find = |tools: &[ToolDecl], name: &str| tools.iter().find(|t| t.name == name).cloned();
281 let added = current
282 .iter()
283 .filter(|tool| find(previous, &tool.name).as_ref() != Some(*tool))
284 .cloned()
285 .collect();
286 let removed = previous
287 .iter()
288 .filter(|tool| find(current, &tool.name).as_ref() != Some(*tool))
289 .map(|tool| ToolRef {
290 name: tool.name.clone(),
291 })
292 .collect();
293 (added, removed)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::message::UserMessage;
300 use serde_json::json;
301
302 fn system(raw: serde_json::Value) -> Message {
303 serde_json::from_value(raw).unwrap_or(Message::User(UserMessage::default()))
304 }
305
306 #[test]
307 fn replay_patches_sections_and_tools() {
308 let messages = vec![
309 system(
310 json!({"role":"system","content":"base","sections":{"a":"A","b":"B"},
311 "toolsAdded":[{"name":"read","description":"r","parameters":{}},{"name":"write","description":"w","parameters":{}}],"timestamp":5}),
312 ),
313 Message::User(UserMessage::default()),
314 system(
315 json!({"role":"system","content":"","sections":{"a":null,"c":"C","b":"B2"},"toolsRemoved":[{"name":"read"}],"timestamp":9}),
316 ),
317 ];
318 let current = current_system_message(&messages);
319 assert_eq!(
320 current.and_then(|m| serde_json::to_value(Message::System(m)).ok()),
321 Some(
322 json!({"role":"system","content":"base","sections":{"b":"B2","c":"C"},
323 "toolsAdded":[{"name":"write","description":"w","parameters":{}}],"timestamp":5})
324 )
325 );
326 assert!(has_non_additive_tool_changes(&messages));
327 let resolved = resolve_transcript_tools(&messages, ToolChangeSupport::Additive);
328 assert_eq!(resolved.folded_system_messages, 2);
329 assert_eq!(
330 collapse_system_messages(Context { messages })
331 .messages
332 .len(),
333 2
334 );
335 }
336
337 #[test]
338 fn no_system_messages_yields_none() {
339 assert_eq!(
340 current_system_message(&[Message::User(UserMessage::default())]),
341 None
342 );
343 }
344
345 #[test]
346 fn diff_sections_matches_pi() {
347 let previous: IndexMap<String, String> = [("a", "1"), ("b", "2")]
348 .map(|(k, v)| (k.to_owned(), v.to_owned()))
349 .into_iter()
350 .collect();
351 let current: IndexMap<String, String> = [("b", "3"), ("c", "4")]
352 .map(|(k, v)| (k.to_owned(), v.to_owned()))
353 .into_iter()
354 .collect();
355 let patch = diff_sections(&previous, ¤t);
356 assert_eq!(
357 serde_json::to_value(patch).ok(),
358 Some(json!({"b":"3","c":"4","a":null}))
359 );
360 assert_eq!(diff_sections(¤t, ¤t), None);
361 }
362}