Skip to main content

arcan_core/
protocol.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
6#[serde(rename_all = "snake_case")]
7pub enum Role {
8    System,
9    User,
10    Assistant,
11    Tool,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
15pub struct ChatMessage {
16    pub role: Role,
17    pub content: String,
18    /// For tool result messages, the ID of the tool call this responds to.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub tool_call_id: Option<String>,
21}
22
23impl ChatMessage {
24    pub fn system(content: impl Into<String>) -> Self {
25        Self {
26            role: Role::System,
27            content: content.into(),
28            tool_call_id: None,
29        }
30    }
31
32    pub fn user(content: impl Into<String>) -> Self {
33        Self {
34            role: Role::User,
35            content: content.into(),
36            tool_call_id: None,
37        }
38    }
39
40    pub fn assistant(content: impl Into<String>) -> Self {
41        Self {
42            role: Role::Assistant,
43            content: content.into(),
44            tool_call_id: None,
45        }
46    }
47
48    pub fn tool(content: impl Into<String>) -> Self {
49        Self {
50            role: Role::Tool,
51            content: content.into(),
52            tool_call_id: None,
53        }
54    }
55
56    pub fn tool_result(call_id: impl Into<String>, content: impl Into<String>) -> Self {
57        Self {
58            role: Role::Tool,
59            content: content.into(),
60            tool_call_id: Some(call_id.into()),
61        }
62    }
63}
64
65/// MCP-compatible behavioral annotations for tools.
66#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
67pub struct ToolAnnotations {
68    /// Tool does not modify its environment.
69    #[serde(default)]
70    pub read_only: bool,
71    /// Tool may perform destructive updates.
72    #[serde(default)]
73    pub destructive: bool,
74    /// Repeated calls with same args produce same result.
75    #[serde(default)]
76    pub idempotent: bool,
77    /// Tool interacts with external entities (network, APIs).
78    #[serde(default)]
79    pub open_world: bool,
80    /// Tool requires user confirmation before execution.
81    #[serde(default)]
82    pub requires_confirmation: bool,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
86pub struct ToolDefinition {
87    pub name: String,
88    pub description: String,
89    pub input_schema: Value,
90
91    // ── MCP-aligned fields (all optional, backward-compatible) ──
92    /// Human-readable display name (MCP: title).
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub title: Option<String>,
95    /// JSON Schema for structured output (MCP: outputSchema).
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub output_schema: Option<Value>,
98    /// Behavioral hints (MCP: annotations).
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub annotations: Option<ToolAnnotations>,
101
102    // ── Arcan extensions ──
103    /// Tool category for grouping ("filesystem", "code", "shell", "mcp").
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub category: Option<String>,
106    /// Tags for filtering and matching (skills.sh compatible).
107    #[serde(default, skip_serializing_if = "Vec::is_empty")]
108    pub tags: Vec<String>,
109    /// Maximum execution timeout in seconds.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub timeout_secs: Option<u32>,
112}
113
114/// Typed content block in a tool result (MCP-compatible).
115#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
116#[serde(tag = "type", rename_all = "snake_case")]
117pub enum ToolContent {
118    Text { text: String },
119    Image { data: String, mime_type: String },
120    Json { value: Value },
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
124pub struct ToolCall {
125    pub call_id: String,
126    pub tool_name: String,
127    #[serde(default)]
128    pub input: Value,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
132pub struct ToolResult {
133    pub call_id: String,
134    pub tool_name: String,
135    #[serde(default)]
136    pub output: Value,
137    /// MCP-style typed content blocks (optional, alongside output for compat).
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub content: Option<Vec<ToolContent>>,
140    /// Whether this result represents an error (MCP: isError).
141    #[serde(default)]
142    pub is_error: bool,
143    pub state_patch: Option<StatePatch>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
147pub struct ToolResultSummary {
148    pub call_id: String,
149    pub tool_name: String,
150    #[serde(default)]
151    pub output: Value,
152}
153
154impl From<&ToolResult> for ToolResultSummary {
155    fn from(value: &ToolResult) -> Self {
156        Self {
157            call_id: value.call_id.clone(),
158            tool_name: value.tool_name.clone(),
159            output: value.output.clone(),
160        }
161    }
162}
163
164#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
165#[serde(rename_all = "snake_case")]
166pub enum StatePatchFormat {
167    JsonPatch,
168    MergePatch,
169}
170
171#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
172#[serde(rename_all = "snake_case")]
173pub enum StatePatchSource {
174    Model,
175    Tool,
176    System,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
180pub struct StatePatch {
181    pub format: StatePatchFormat,
182    #[serde(default)]
183    pub patch: Value,
184    pub source: StatePatchSource,
185}
186
187#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
188#[serde(rename_all = "snake_case")]
189pub enum ModelStopReason {
190    EndTurn,
191    ToolUse,
192    NeedsUser,
193    MaxTokens,
194    Safety,
195    Unknown,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
199#[serde(tag = "kind", rename_all = "snake_case")]
200pub enum ModelDirective {
201    Text { delta: String },
202    ToolCall { call: ToolCall },
203    StatePatch { patch: StatePatch },
204    FinalAnswer { text: String },
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
208pub struct ModelTurn {
209    pub directives: Vec<ModelDirective>,
210    pub stop_reason: ModelStopReason,
211    /// Token usage for this turn (if reported by the provider).
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub usage: Option<TokenUsage>,
214}
215
216#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
217#[serde(rename_all = "snake_case")]
218pub struct TokenUsage {
219    /// Tokens in the input/prompt.
220    #[serde(default)]
221    pub input_tokens: u64,
222    /// Tokens in the output/completion.
223    #[serde(default)]
224    pub output_tokens: u64,
225    /// Tokens from cache reads (Anthropic-specific).
226    #[serde(default)]
227    pub cache_read_tokens: u64,
228    /// Tokens written to cache (Anthropic-specific).
229    #[serde(default)]
230    pub cache_creation_tokens: u64,
231}
232
233impl TokenUsage {
234    /// Accumulate another usage into this one.
235    pub fn accumulate(&mut self, other: &TokenUsage) {
236        self.input_tokens += other.input_tokens;
237        self.output_tokens += other.output_tokens;
238        self.cache_read_tokens += other.cache_read_tokens;
239        self.cache_creation_tokens += other.cache_creation_tokens;
240    }
241
242    pub fn total(&self) -> u64 {
243        self.input_tokens + self.output_tokens
244    }
245}
246
247#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
248#[serde(rename_all = "snake_case")]
249pub enum RunStopReason {
250    Completed,
251    NeedsUser,
252    BlockedByPolicy,
253    BudgetExceeded,
254    Cancelled,
255    Error,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
259#[serde(tag = "part_type", rename_all = "snake_case")]
260pub enum AgentEvent {
261    RunStarted {
262        run_id: String,
263        session_id: String,
264        provider: String,
265        max_iterations: u32,
266    },
267    IterationStarted {
268        run_id: String,
269        session_id: String,
270        iteration: u32,
271    },
272    ModelOutput {
273        run_id: String,
274        session_id: String,
275        iteration: u32,
276        stop_reason: ModelStopReason,
277        directive_count: usize,
278        #[serde(default, skip_serializing_if = "Option::is_none")]
279        usage: Option<TokenUsage>,
280    },
281    TextDelta {
282        run_id: String,
283        session_id: String,
284        iteration: u32,
285        delta: String,
286    },
287    ToolCallRequested {
288        run_id: String,
289        session_id: String,
290        iteration: u32,
291        call: ToolCall,
292    },
293    ToolCallCompleted {
294        run_id: String,
295        session_id: String,
296        iteration: u32,
297        result: ToolResultSummary,
298    },
299    ToolCallFailed {
300        run_id: String,
301        session_id: String,
302        iteration: u32,
303        call_id: String,
304        tool_name: String,
305        error: String,
306    },
307    StatePatched {
308        run_id: String,
309        session_id: String,
310        iteration: u32,
311        patch: StatePatch,
312        revision: u64,
313    },
314    RunErrored {
315        run_id: String,
316        session_id: String,
317        error: String,
318    },
319    RunFinished {
320        run_id: String,
321        session_id: String,
322        reason: RunStopReason,
323        total_iterations: u32,
324        final_answer: Option<String>,
325    },
326}
327
328impl AgentEvent {
329    pub fn as_sse_data(&self) -> Result<String, serde_json::Error> {
330        let payload = serde_json::to_string(self)?;
331        Ok(format!("data: {payload}\n\n"))
332    }
333}