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}
212
213#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
214#[serde(rename_all = "snake_case")]
215pub enum RunStopReason {
216    Completed,
217    NeedsUser,
218    BlockedByPolicy,
219    BudgetExceeded,
220    Error,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
224#[serde(tag = "part_type", rename_all = "snake_case")]
225pub enum AgentEvent {
226    RunStarted {
227        run_id: String,
228        session_id: String,
229        provider: String,
230        max_iterations: u32,
231    },
232    IterationStarted {
233        run_id: String,
234        session_id: String,
235        iteration: u32,
236    },
237    ModelOutput {
238        run_id: String,
239        session_id: String,
240        iteration: u32,
241        stop_reason: ModelStopReason,
242        directive_count: usize,
243    },
244    TextDelta {
245        run_id: String,
246        session_id: String,
247        iteration: u32,
248        delta: String,
249    },
250    ToolCallRequested {
251        run_id: String,
252        session_id: String,
253        iteration: u32,
254        call: ToolCall,
255    },
256    ToolCallCompleted {
257        run_id: String,
258        session_id: String,
259        iteration: u32,
260        result: ToolResultSummary,
261    },
262    ToolCallFailed {
263        run_id: String,
264        session_id: String,
265        iteration: u32,
266        call_id: String,
267        tool_name: String,
268        error: String,
269    },
270    StatePatched {
271        run_id: String,
272        session_id: String,
273        iteration: u32,
274        patch: StatePatch,
275        revision: u64,
276    },
277    RunErrored {
278        run_id: String,
279        session_id: String,
280        error: String,
281    },
282    RunFinished {
283        run_id: String,
284        session_id: String,
285        reason: RunStopReason,
286        total_iterations: u32,
287        final_answer: Option<String>,
288    },
289}
290
291impl AgentEvent {
292    pub fn as_sse_data(&self) -> Result<String, serde_json::Error> {
293        let payload = serde_json::to_string(self)?;
294        Ok(format!("data: {payload}\n\n"))
295    }
296}