Skip to main content

hematite/agent/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4// ── Role ──────────────────────────────────────────────────────────────────────
5
6#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum Role {
9    System,
10    User,
11    Assistant,
12    Tool,
13}
14
15// ── Message Content ───────────────────────────────────────────────────────────
16
17#[derive(Serialize, Deserialize, Clone, Debug)]
18#[serde(untagged)]
19pub enum MessageContent {
20    Text(String),
21    Parts(Vec<ContentPart>),
22}
23
24#[derive(Serialize, Deserialize, Clone, Debug)]
25#[serde(tag = "type")]
26pub enum ContentPart {
27    #[serde(rename = "text")]
28    Text { text: String },
29    #[serde(rename = "image_url")]
30    ImageUrl { image_url: ImageUrlSource },
31}
32
33#[derive(Serialize, Deserialize, Clone, Debug)]
34pub struct ImageUrlSource {
35    pub url: String,
36}
37
38impl Default for MessageContent {
39    fn default() -> Self {
40        MessageContent::Text(String::new())
41    }
42}
43
44impl MessageContent {
45    pub fn as_str(&self) -> &str {
46        match self {
47            MessageContent::Text(s) => s,
48            MessageContent::Parts(parts) => {
49                for part in parts {
50                    if let ContentPart::Text { text } = part {
51                        return text;
52                    }
53                }
54                ""
55            }
56        }
57    }
58}
59
60// ── Chat Message ──────────────────────────────────────────────────────────────
61
62#[derive(Serialize, Deserialize, Clone, Debug)]
63pub struct ChatMessage {
64    pub role: String,
65    pub content: MessageContent,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub tool_calls: Option<Vec<ToolCallResponse>>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub tool_call_id: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub name: Option<String>,
72}
73
74impl ChatMessage {
75    pub fn system(content: &str) -> Self {
76        Self {
77            role: "system".into(),
78            content: MessageContent::Text(content.into()),
79            tool_calls: None,
80            tool_call_id: None,
81            name: None,
82        }
83    }
84    pub fn user(content: &str) -> Self {
85        Self {
86            role: "user".into(),
87            content: MessageContent::Text(content.into()),
88            tool_calls: None,
89            tool_call_id: None,
90            name: None,
91        }
92    }
93    pub fn user_with_image(content: &str, image_url: &str) -> Self {
94        Self {
95            role: "user".into(),
96            content: MessageContent::Parts(vec![
97                ContentPart::Text {
98                    text: content.into(),
99                },
100                ContentPart::ImageUrl {
101                    image_url: ImageUrlSource {
102                        url: image_url.into(),
103                    },
104                },
105            ]),
106            tool_calls: None,
107            tool_call_id: None,
108            name: None,
109        }
110    }
111    pub fn assistant_text(content: &str) -> Self {
112        Self {
113            role: "assistant".into(),
114            content: MessageContent::Text(content.into()),
115            tool_calls: None,
116            tool_call_id: None,
117            name: None,
118        }
119    }
120    pub fn assistant_tool_calls(content: &str, calls: Vec<ToolCallResponse>) -> Self {
121        Self {
122            role: "assistant".into(),
123            content: MessageContent::Text(content.into()),
124            tool_calls: Some(calls),
125            tool_call_id: None,
126            name: None,
127        }
128    }
129    pub fn tool_result(tool_call_id: &str, fn_name: &str, content: &str) -> Self {
130        Self {
131            role: "tool".into(),
132            content: MessageContent::Text(content.to_string()),
133            tool_calls: None,
134            tool_call_id: Some(tool_call_id.into()),
135            name: Some(fn_name.into()),
136        }
137    }
138    pub fn tool_result_for_model(id: &str, name: &str, result: &str, _model: &str) -> Self {
139        Self::tool_result(id, name, result)
140    }
141}
142
143// ── Tool Call ─────────────────────────────────────────────────────────────────
144
145#[derive(Serialize, Deserialize, Clone, Debug)]
146pub struct ToolCallResponse {
147    pub id: String,
148    #[serde(rename = "type")]
149    pub call_type: String,
150    pub function: ToolCallFn,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub index: Option<i32>,
153}
154
155#[derive(Serialize, Deserialize, Clone, Debug)]
156pub struct ToolCallFn {
157    pub name: String,
158    #[serde(deserialize_with = "deserialize_arguments")]
159    pub arguments: Value,
160}
161
162fn deserialize_arguments<'de, D>(deserializer: D) -> Result<Value, D::Error>
163where
164    D: serde::Deserializer<'de>,
165{
166    let v: Value = serde::Deserialize::deserialize(deserializer)?;
167    if let Value::String(s) = &v {
168        if let Ok(parsed) = serde_json::from_str(s) {
169            return Ok(parsed);
170        }
171    }
172    Ok(v)
173}
174
175// ── Tool Definition ───────────────────────────────────────────────────────────
176
177#[derive(Serialize, Clone, Debug)]
178pub struct ToolDefinition {
179    #[serde(rename = "type")]
180    pub tool_type: String,
181    pub function: ToolFunction,
182    #[serde(skip_serializing, skip_deserializing)]
183    pub metadata: ToolMetadata,
184}
185
186#[derive(Serialize, Deserialize, Clone, Debug)]
187pub struct ToolFunction {
188    pub name: String,
189    pub description: String,
190    pub parameters: Value,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
194pub enum ToolCategory {
195    RepoRead,
196    RepoWrite,
197    Runtime,
198    Architecture,
199    Toolchain,
200    Verification,
201    Git,
202    Research,
203    Vision,
204    Lsp,
205    Workflow,
206    External,
207    Other,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
211pub struct ToolMetadata {
212    pub category: ToolCategory,
213    pub mutates_workspace: bool,
214    pub external_surface: bool,
215    pub trust_sensitive: bool,
216    pub read_only_friendly: bool,
217    pub plan_scope: bool,
218}
219
220// ── Token Usage ───────────────────────────────────────────────────────────────
221
222#[derive(Serialize, Deserialize, Clone, Debug, Default)]
223pub struct TokenUsage {
224    pub prompt_tokens: usize,
225    pub completion_tokens: usize,
226    pub total_tokens: usize,
227    #[serde(default)]
228    pub prompt_cache_hit_tokens: usize,
229    #[serde(default)]
230    pub cache_read_input_tokens: usize,
231}
232
233// ── State Enums ───────────────────────────────────────────────────────────────
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
236pub enum ProviderRuntimeState {
237    Booting,
238    Live,
239    Degraded,
240    Recovering,
241    EmptyResponse,
242    ContextWindow,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub enum OperatorCheckpointState {
247    Idle,
248    RecoveringProvider,
249    BudgetReduced,
250    HistoryCompacted,
251    BlockedContextWindow,
252    BlockedPolicy,
253    BlockedRecentFileEvidence,
254    BlockedExactLineWindow,
255    BlockedToolLoop,
256    BlockedVerification,
257}
258
259impl OperatorCheckpointState {
260    pub fn label(&self) -> &'static str {
261        match self {
262            Self::Idle => "idle",
263            Self::RecoveringProvider => "recovering_provider",
264            Self::BudgetReduced => "budget_reduced",
265            Self::HistoryCompacted => "history_compacted",
266            Self::BlockedContextWindow => "blocked_context_window",
267            Self::BlockedPolicy => "blocked_policy",
268            Self::BlockedRecentFileEvidence => "blocked_recent_file_evidence",
269            Self::BlockedExactLineWindow => "blocked_exact_line_window",
270            Self::BlockedToolLoop => "blocked_tool_loop",
271            Self::BlockedVerification => "blocked_verification",
272        }
273    }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
277pub enum McpRuntimeState {
278    Unconfigured,
279    Healthy,
280    Degraded,
281    Failed,
282}
283
284// ── Inference Event ───────────────────────────────────────────────────────────
285
286#[derive(Debug)]
287pub enum InferenceEvent {
288    Token(String),
289    MutedToken(String),
290    Thought(String),
291    VoiceStatus(String),
292    ToolCallStart {
293        id: String,
294        name: String,
295        args: String,
296    },
297    ToolCallResult {
298        id: String,
299        name: String,
300        result: String,
301        is_error: bool,
302    },
303    ApprovalRequired {
304        id: String,
305        name: String,
306        display: String,
307        diff: Option<String>,
308        mutation_label: Option<String>,
309        responder: tokio::sync::oneshot::Sender<bool>,
310    },
311    Done,
312    ChainImplementPlan,
313    Error(String),
314    ProviderStatus {
315        state: ProviderRuntimeState,
316        summary: String,
317    },
318    OperatorCheckpoint {
319        state: OperatorCheckpointState,
320        summary: String,
321    },
322    RecoveryRecipe {
323        summary: String,
324    },
325    McpStatus {
326        state: McpRuntimeState,
327        summary: String,
328    },
329    CompactionPressure {
330        estimated_tokens: usize,
331        threshold_tokens: usize,
332        percent: u8,
333    },
334    PromptPressure {
335        estimated_input_tokens: usize,
336        reserved_output_tokens: usize,
337        estimated_total_tokens: usize,
338        context_length: usize,
339        percent: u8,
340    },
341    TaskProgress {
342        id: String,
343        label: String,
344        progress: u8,
345    },
346    UsageUpdate(TokenUsage),
347    RuntimeProfile {
348        provider_name: String,
349        endpoint: String,
350        model_id: String,
351        context_length: usize,
352    },
353    TurnTiming {
354        context_prep_ms: u128,
355        inference_ms: u128,
356        execution_ms: u128,
357    },
358    VeinStatus {
359        file_count: usize,
360        embedded_count: usize,
361        docs_only: bool,
362    },
363    VeinContext {
364        paths: Vec<String>,
365    },
366    SoulReroll {
367        species: String,
368        rarity: String,
369        shiny: bool,
370        personality: String,
371    },
372    CopyDiveInCommand(String),
373    EmbedProfile {
374        model_id: Option<String>,
375    },
376    ShellLine(String),
377    TurnBudget(crate::agent::economics::TurnBudget),
378}