1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[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#[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#[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#[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#[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#[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#[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#[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}