Skip to main content

cairn_runtime/
message.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum Role {
7    User,
8    Assistant,
9}
10
11/// Provider-neutral content block.
12///
13/// Provider-specific data that must round-trip (e.g. signed reasoning) is kept
14/// opaquely in `provider_data`, tagged with the provider that produced it, so
15/// adapters can pass it back to the same provider and drop it for others.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum Content {
19    Text {
20        text: String,
21    },
22    ToolUse {
23        id: String,
24        name: String,
25        input: Value,
26    },
27    ToolResult {
28        tool_use_id: String,
29        content: Value,
30        #[serde(default)]
31        is_error: bool,
32    },
33    Thinking {
34        text: String,
35        #[serde(default, skip_serializing_if = "Option::is_none")]
36        provider_data: Option<Value>,
37    },
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct Message {
42    pub role: Role,
43    pub content: Vec<Content>,
44}
45
46impl Message {
47    pub fn user(text: impl Into<String>) -> Self {
48        Self { role: Role::User, content: vec![Content::Text { text: text.into() }] }
49    }
50
51    pub fn assistant(content: Vec<Content>) -> Self {
52        Self { role: Role::Assistant, content }
53    }
54
55    /// Concatenated text blocks.
56    pub fn text(&self) -> String {
57        content_text(&self.content)
58    }
59}
60
61pub fn content_text(content: &[Content]) -> String {
62    content
63        .iter()
64        .filter_map(|c| match c {
65            Content::Text { text } => Some(text.as_str()),
66            _ => None,
67        })
68        .collect::<Vec<_>>()
69        .join("")
70}