1use std::collections::HashMap;
22use std::path::PathBuf;
23
24use crate::app::McpServerConfig;
25use crate::models::ChatMessage;
26use crate::models::ReasoningLevel;
27use crate::models::tool_call::ToolCall as ModelToolCall;
28use crate::runtime::TaskStatus;
29use crate::session::ConversationHistory;
30
31use super::compaction::{CompactionArchive, CompactionRecord, CompactionRequest};
32use super::ids::{ToolCallId, TurnId};
33use super::runtime::ManagedProcess;
34
35#[allow(clippy::large_enum_variant)]
43#[derive(Debug, Clone)]
44pub enum Cmd {
45 CallModel { turn: TurnId, request: ChatRequest },
49 CompactConversation {
52 turn: TurnId,
53 request: CompactionRequest,
54 },
55 ExecuteTool {
64 turn: TurnId,
65 call_id: ToolCallId,
66 source: ModelToolCall,
67 model_id: String,
68 },
69 CancelScope(TurnId),
75
76 SaveConversation(ConversationHistory),
80 SaveCompactionArchive {
86 archive: CompactionArchive,
87 record: CompactionRecord,
88 conversation: ConversationHistory,
89 },
90 SaveProcess(ManagedProcess),
92 PersistLastModel(String),
94 PersistReasoningFor {
96 model_id: String,
97 level: ReasoningLevel,
98 },
99 RefreshInstructions,
102 LoadConversation(String),
106 ListConversations,
111 ListRuntimeTasks { limit: usize },
113 LoadRuntimeTask { id: String },
115 ListRuntimeProcesses { limit: usize },
117 ShowRuntimeProcessLogs { id: String },
119 StopRuntimeProcess { id: String },
121 RestartRuntimeProcess { id: String },
123 OpenRuntimeTarget { target: String },
125 ShowRuntimePorts,
127 ListRuntimeApprovals,
129 DecideRuntimeApproval { id: String, decision: String },
131 ListRuntimeCheckpoints { limit: usize },
133 ListRuntimeMemory,
135 ListRuntimePlugins,
137 UpdateRuntimeTaskStatus {
139 id: String,
140 status: TaskStatus,
141 final_report: Option<String>,
142 },
143 CreateRuntimeCheckpoint { paths: Vec<PathBuf> },
145 RestoreRuntimeCheckpoint { id: String },
147 RememberRuntimeMemory { key: String, value: String },
149 EditRuntimeMemory { id: String, value: String },
151 ForgetRuntimeMemory { id: String },
153 ShowRuntimeModelInfo { model: String },
155
156 InitMcpServers(HashMap<String, McpServerConfig>),
160 StopMcpServer { name: String },
162
163 PullOllamaModel { model: String },
166
167 OpenInSystem(PathBuf),
171
172 DismissStatusAfter { ms: u64 },
176
177 WriteImageToTemp {
182 path: PathBuf,
183 bytes: Vec<u8>,
184 format: String,
185 },
186
187 ReadClipboard,
194
195 Exit,
199 SetTerminalTitle(String),
203}
204
205#[derive(Debug, Clone)]
210pub struct ChatRequest {
211 pub model_id: String,
212 pub messages: Vec<ChatMessage>,
213 pub system_prompt: String,
214 pub instructions: Option<String>,
217 pub reasoning: ReasoningLevel,
218 pub temperature: f32,
219 pub max_tokens: usize,
220 pub tools: Vec<ToolDefinition>,
223}
224
225#[derive(Debug, Clone)]
229pub struct ToolDefinition {
230 pub name: String,
231 pub description: String,
232 pub input_schema: serde_json::Value,
233}
234
235impl ToolDefinition {
236 pub fn to_openai_json(&self) -> serde_json::Value {
241 serde_json::json!({
242 "type": "function",
243 "function": {
244 "name": self.name,
245 "description": self.description,
246 "parameters": self.input_schema,
247 }
248 })
249 }
250}
251
252impl Cmd {
253 pub fn tag(&self) -> &'static str {
256 match self {
257 Cmd::CallModel { .. } => "call_model",
258 Cmd::CompactConversation { .. } => "compact_conversation",
259 Cmd::ExecuteTool { .. } => "execute_tool",
260 Cmd::CancelScope(_) => "cancel_scope",
261 Cmd::SaveConversation(_) => "save_conversation",
262 Cmd::SaveCompactionArchive { .. } => "save_compaction_archive",
263 Cmd::SaveProcess(_) => "save_process",
264 Cmd::PersistLastModel(_) => "persist_last_model",
265 Cmd::PersistReasoningFor { .. } => "persist_reasoning_for",
266 Cmd::RefreshInstructions => "refresh_instructions",
267 Cmd::LoadConversation(_) => "load_conversation",
268 Cmd::ListConversations => "list_conversations",
269 Cmd::ListRuntimeTasks { .. } => "list_runtime_tasks",
270 Cmd::LoadRuntimeTask { .. } => "load_runtime_task",
271 Cmd::ListRuntimeProcesses { .. } => "list_runtime_processes",
272 Cmd::ShowRuntimeProcessLogs { .. } => "show_runtime_process_logs",
273 Cmd::StopRuntimeProcess { .. } => "stop_runtime_process",
274 Cmd::RestartRuntimeProcess { .. } => "restart_runtime_process",
275 Cmd::OpenRuntimeTarget { .. } => "open_runtime_target",
276 Cmd::ShowRuntimePorts => "show_runtime_ports",
277 Cmd::ListRuntimeApprovals => "list_runtime_approvals",
278 Cmd::DecideRuntimeApproval { .. } => "decide_runtime_approval",
279 Cmd::ListRuntimeCheckpoints { .. } => "list_runtime_checkpoints",
280 Cmd::ListRuntimeMemory => "list_runtime_memory",
281 Cmd::ListRuntimePlugins => "list_runtime_plugins",
282 Cmd::UpdateRuntimeTaskStatus { .. } => "update_runtime_task_status",
283 Cmd::CreateRuntimeCheckpoint { .. } => "create_runtime_checkpoint",
284 Cmd::RestoreRuntimeCheckpoint { .. } => "restore_runtime_checkpoint",
285 Cmd::RememberRuntimeMemory { .. } => "remember_runtime_memory",
286 Cmd::EditRuntimeMemory { .. } => "edit_runtime_memory",
287 Cmd::ForgetRuntimeMemory { .. } => "forget_runtime_memory",
288 Cmd::ShowRuntimeModelInfo { .. } => "show_runtime_model_info",
289 Cmd::InitMcpServers(_) => "init_mcp_servers",
290 Cmd::StopMcpServer { .. } => "stop_mcp_server",
291 Cmd::PullOllamaModel { .. } => "pull_ollama_model",
292 Cmd::OpenInSystem(_) => "open_in_system",
293 Cmd::DismissStatusAfter { .. } => "dismiss_status_after",
294 Cmd::WriteImageToTemp { .. } => "write_image_to_temp",
295 Cmd::ReadClipboard => "read_clipboard",
296 Cmd::Exit => "exit",
297 Cmd::SetTerminalTitle(_) => "set_terminal_title",
298 }
299 }
300
301 pub fn is_turn_scoped(&self) -> bool {
305 matches!(
306 self,
307 Cmd::CallModel { .. } | Cmd::CompactConversation { .. } | Cmd::ExecuteTool { .. }
308 )
309 }
310
311 pub fn summary(&self) -> String {
315 match self {
316 Cmd::CallModel { turn, request } => format!(
317 "call_model(turn={}, model={}, msgs={})",
318 turn,
319 request.model_id,
320 request.messages.len()
321 ),
322 Cmd::CompactConversation { turn, request } => format!(
323 "compact_conversation(turn={}, model={}, trigger={}, msgs={})",
324 turn,
325 request.chat.model_id,
326 request.trigger.as_str(),
327 request.chat.messages.len()
328 ),
329 Cmd::ExecuteTool {
330 turn,
331 call_id,
332 source,
333 model_id: _,
334 } => format!(
335 "execute_tool(turn={}, call={}, fn={})",
336 turn, call_id, source.function.name
337 ),
338 Cmd::CancelScope(turn) => format!("cancel_scope(turn={})", turn),
339 Cmd::SaveConversation(c) => format!("save_conversation(id={})", c.id),
340 Cmd::SaveCompactionArchive {
341 archive, record, ..
342 } => format!(
343 "save_compaction_archive(conversation={}, id={})",
344 archive.conversation_id, record.id
345 ),
346 Cmd::SaveProcess(p) => format!("save_process(id={}, pid={})", p.id, p.pid),
347 Cmd::PersistLastModel(m) => format!("persist_last_model({})", m),
348 Cmd::PersistReasoningFor { model_id, level } => {
349 format!("persist_reasoning_for({}, {:?})", model_id, level)
350 },
351 Cmd::RefreshInstructions => "refresh_instructions".to_string(),
352 Cmd::LoadConversation(id) => format!("load_conversation({})", id),
353 Cmd::ListConversations => "list_conversations".to_string(),
354 Cmd::ListRuntimeTasks { limit } => format!("list_runtime_tasks(limit={})", limit),
355 Cmd::LoadRuntimeTask { id } => format!("load_runtime_task({})", id),
356 Cmd::ListRuntimeProcesses { limit } => {
357 format!("list_runtime_processes(limit={})", limit)
358 },
359 Cmd::ShowRuntimeProcessLogs { id } => format!("show_runtime_process_logs({})", id),
360 Cmd::StopRuntimeProcess { id } => format!("stop_runtime_process({})", id),
361 Cmd::RestartRuntimeProcess { id } => format!("restart_runtime_process({})", id),
362 Cmd::OpenRuntimeTarget { target } => format!("open_runtime_target({})", target),
363 Cmd::ShowRuntimePorts => "show_runtime_ports".to_string(),
364 Cmd::ListRuntimeApprovals => "list_runtime_approvals".to_string(),
365 Cmd::DecideRuntimeApproval { id, decision } => {
366 format!("decide_runtime_approval({}, {})", id, decision)
367 },
368 Cmd::ListRuntimeCheckpoints { limit } => {
369 format!("list_runtime_checkpoints(limit={})", limit)
370 },
371 Cmd::ListRuntimeMemory => "list_runtime_memory".to_string(),
372 Cmd::ListRuntimePlugins => "list_runtime_plugins".to_string(),
373 Cmd::UpdateRuntimeTaskStatus { id, status, .. } => {
374 format!("update_runtime_task_status({}, {})", id, status)
375 },
376 Cmd::CreateRuntimeCheckpoint { paths } => {
377 format!("create_runtime_checkpoint(n={})", paths.len())
378 },
379 Cmd::RestoreRuntimeCheckpoint { id } => format!("restore_runtime_checkpoint({})", id),
380 Cmd::RememberRuntimeMemory { key, .. } => {
381 format!("remember_runtime_memory({})", key)
382 },
383 Cmd::EditRuntimeMemory { id, .. } => format!("edit_runtime_memory({})", id),
384 Cmd::ForgetRuntimeMemory { id } => format!("forget_runtime_memory({})", id),
385 Cmd::ShowRuntimeModelInfo { model } => format!("show_runtime_model_info({})", model),
386 Cmd::InitMcpServers(m) => format!("init_mcp_servers(n={})", m.len()),
387 Cmd::StopMcpServer { name } => format!("stop_mcp_server({})", name),
388 Cmd::PullOllamaModel { model } => format!("pull_ollama_model({})", model),
389 Cmd::OpenInSystem(p) => format!("open_in_system({})", p.display()),
390 Cmd::DismissStatusAfter { ms } => format!("dismiss_status_after({}ms)", ms),
391 Cmd::WriteImageToTemp {
392 path,
393 format,
394 bytes,
395 } => format!(
396 "write_image_to_temp(path={}, fmt={}, n={})",
397 path.display(),
398 format,
399 bytes.len()
400 ),
401 Cmd::ReadClipboard => "read_clipboard".to_string(),
402 Cmd::Exit => "exit".to_string(),
403 Cmd::SetTerminalTitle(t) => format!("set_terminal_title({})", t),
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn turn_scoped_variants_marked_correctly() {
414 let request = ChatRequest {
415 model_id: "m".to_string(),
416 messages: vec![],
417 system_prompt: String::new(),
418 instructions: None,
419 reasoning: ReasoningLevel::Medium,
420 temperature: 0.7,
421 max_tokens: 4096,
422 tools: vec![],
423 };
424 assert!(
425 Cmd::CallModel {
426 turn: TurnId(1),
427 request,
428 }
429 .is_turn_scoped()
430 );
431 assert!(
432 !Cmd::SaveConversation(ConversationHistory::new("/p".to_string(), "m".to_string()))
433 .is_turn_scoped()
434 );
435 assert!(!Cmd::RefreshInstructions.is_turn_scoped());
436 assert!(!Cmd::Exit.is_turn_scoped());
437 }
438
439 #[test]
440 fn cmd_tags_are_stable() {
441 assert_eq!(Cmd::Exit.tag(), "exit");
442 assert_eq!(Cmd::RefreshInstructions.tag(), "refresh_instructions");
443 assert_eq!(Cmd::CancelScope(TurnId(1)).tag(), "cancel_scope");
444 }
445
446 #[test]
447 fn cmd_summary_includes_identifying_fields() {
448 let c = Cmd::CancelScope(TurnId(42));
449 let s = c.summary();
450 assert!(s.contains("turn#42"));
451 }
452}