1use std::fmt::Write as _;
13use std::future::Future;
14use std::pin::Pin;
15
16use zeph_commands::CommandError;
17use zeph_commands::traits::agent::AgentAccess;
18use zeph_memory::{GraphExtractionConfig, MessageId, extract_and_store};
19
20use super::{Agent, error::AgentError};
21use crate::channel::Channel;
22
23impl<C: Channel + Send + 'static> AgentAccess for Agent<C> {
24 fn memory_tiers<'a>(
27 &'a mut self,
28 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
29 Box::pin(async move {
30 let Some(memory) = self.memory_state.persistence.memory.clone() else {
31 return Ok("Memory not configured.".to_owned());
32 };
33 match memory.sqlite().count_messages_by_tier().await {
34 Ok((episodic, semantic)) => {
35 let mut out = String::new();
36 let _ = writeln!(out, "Memory tiers:");
37 let _ = writeln!(out, " Working: (current context window — virtual)");
38 let _ = writeln!(out, " Episodic: {episodic} messages");
39 let _ = writeln!(out, " Semantic: {semantic} facts");
40 Ok(out.trim_end().to_owned())
41 }
42 Err(e) => Ok(format!("Failed to query tier stats: {e}")),
43 }
44 })
45 }
46
47 fn memory_promote<'a>(
48 &'a mut self,
49 ids_str: &'a str,
50 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
51 Box::pin(async move {
52 let Some(memory) = self.memory_state.persistence.memory.clone() else {
53 return Ok("Memory not configured.".to_owned());
54 };
55 let ids: Vec<MessageId> = ids_str
56 .split_whitespace()
57 .filter_map(|s| s.parse::<i64>().ok().map(MessageId))
58 .collect();
59 if ids.is_empty() {
60 return Ok(
61 "Usage: /memory promote <id> [id...]\nExample: /memory promote 42 43 44"
62 .to_owned(),
63 );
64 }
65 match memory.sqlite().manual_promote(&ids).await {
66 Ok(count) => Ok(format!("Promoted {count} message(s) to semantic tier.")),
67 Err(e) => Ok(format!("Promotion failed: {e}")),
68 }
69 })
70 }
71
72 fn graph_stats<'a>(
75 &'a mut self,
76 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
77 Box::pin(async move {
78 let Some(memory) = self.memory_state.persistence.memory.as_ref() else {
79 return Ok("Graph memory is not enabled.".to_owned());
80 };
81 let Some(store) = memory.graph_store.as_ref() else {
82 return Ok("Graph memory is not enabled.".to_owned());
83 };
84
85 let (entities, edges, communities, distribution) = tokio::join!(
86 store.entity_count(),
87 store.active_edge_count(),
88 store.community_count(),
89 store.edge_type_distribution()
90 );
91 let mut msg = format!(
92 "Graph memory: {} entities, {} edges, {} communities",
93 entities.unwrap_or(0),
94 edges.unwrap_or(0),
95 communities.unwrap_or(0)
96 );
97 if let Ok(dist) = distribution
98 && !dist.is_empty()
99 {
100 let dist_str: Vec<String> = dist.iter().map(|(t, c)| format!("{t}={c}")).collect();
101 write!(msg, "\nEdge types: {}", dist_str.join(", ")).unwrap_or(());
102 }
103 Ok(msg)
104 })
105 }
106
107 fn graph_entities<'a>(
108 &'a mut self,
109 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
110 Box::pin(async move {
111 let Some(memory) = self.memory_state.persistence.memory.as_ref() else {
112 return Ok("Graph memory is not enabled.".to_owned());
113 };
114 let Some(store) = memory.graph_store.as_ref() else {
115 return Ok("Graph memory is not enabled.".to_owned());
116 };
117
118 let entities = store
119 .all_entities()
120 .await
121 .map_err(|e| CommandError::new(e.to_string()))?;
122 if entities.is_empty() {
123 return Ok("No entities found.".to_owned());
124 }
125
126 let total = entities.len();
127 let display: Vec<String> = entities
128 .iter()
129 .take(50)
130 .map(|e| {
131 format!(
132 " {:<40} {:<15} {}",
133 e.name,
134 e.entity_type.as_str(),
135 e.last_seen_at.split('T').next().unwrap_or(&e.last_seen_at)
136 )
137 })
138 .collect();
139 let mut msg = format!(
140 "Entities ({total} total):\n {:<40} {:<15} {}\n{}",
141 "NAME",
142 "TYPE",
143 "LAST SEEN",
144 display.join("\n")
145 );
146 if total > 50 {
147 write!(msg, "\n ...and {} more", total - 50).unwrap_or(());
148 }
149 Ok(msg)
150 })
151 }
152
153 fn graph_facts<'a>(
154 &'a mut self,
155 name: &'a str,
156 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
157 Box::pin(async move {
158 let Some(memory) = self.memory_state.persistence.memory.as_ref() else {
159 return Ok("Graph memory is not enabled.".to_owned());
160 };
161 let Some(store) = memory.graph_store.as_ref() else {
162 return Ok("Graph memory is not enabled.".to_owned());
163 };
164
165 let matches = store
166 .find_entity_by_name(name)
167 .await
168 .map_err(|e| CommandError::new(e.to_string()))?;
169 if matches.is_empty() {
170 return Ok(format!("No entity found matching '{name}'."));
171 }
172
173 let entity = &matches[0];
174 let edges = store
175 .edges_for_entity(entity.id)
176 .await
177 .map_err(|e| CommandError::new(e.to_string()))?;
178 if edges.is_empty() {
179 return Ok(format!("Entity '{}' has no known facts.", entity.name));
180 }
181
182 let mut entity_names: std::collections::HashMap<i64, String> =
183 std::collections::HashMap::new();
184 entity_names.insert(entity.id, entity.name.clone());
185 for edge in &edges {
186 let other_id = if edge.source_entity_id == entity.id {
187 edge.target_entity_id
188 } else {
189 edge.source_entity_id
190 };
191 entity_names.entry(other_id).or_default();
192 }
193 for (&id, name_val) in &mut entity_names {
194 if name_val.is_empty() {
195 if let Ok(Some(other)) = store.find_entity_by_id(id).await {
196 *name_val = other.name;
197 } else {
198 *name_val = format!("#{id}");
199 }
200 }
201 }
202
203 let lines: Vec<String> = edges
204 .iter()
205 .map(|e| {
206 let src = entity_names
207 .get(&e.source_entity_id)
208 .cloned()
209 .unwrap_or_else(|| format!("#{}", e.source_entity_id));
210 let tgt = entity_names
211 .get(&e.target_entity_id)
212 .cloned()
213 .unwrap_or_else(|| format!("#{}", e.target_entity_id));
214 format!(
215 " {} --[{}/{}]--> {}: {} (confidence: {:.2})",
216 src, e.relation, e.edge_type, tgt, e.fact, e.confidence
217 )
218 })
219 .collect();
220 Ok(format!(
221 "Facts for '{}':\n{}",
222 entity.name,
223 lines.join("\n")
224 ))
225 })
226 }
227
228 fn graph_history<'a>(
229 &'a mut self,
230 name: &'a str,
231 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
232 Box::pin(async move {
233 let Some(memory) = self.memory_state.persistence.memory.as_ref() else {
234 return Ok("Graph memory is not enabled.".to_owned());
235 };
236 let Some(store) = memory.graph_store.as_ref() else {
237 return Ok("Graph memory is not enabled.".to_owned());
238 };
239
240 let matches = store
241 .find_entity_by_name(name)
242 .await
243 .map_err(|e| CommandError::new(e.to_string()))?;
244 if matches.is_empty() {
245 return Ok(format!("No entity found matching '{name}'."));
246 }
247
248 let entity = &matches[0];
249 let edges = store
250 .edge_history_for_entity(entity.id, 50)
251 .await
252 .map_err(|e| CommandError::new(e.to_string()))?;
253 if edges.is_empty() {
254 return Ok(format!("Entity '{}' has no edge history.", entity.name));
255 }
256
257 let mut entity_names: std::collections::HashMap<i64, String> =
258 std::collections::HashMap::new();
259 entity_names.insert(entity.id, entity.name.clone());
260 for edge in &edges {
261 for &id in &[edge.source_entity_id, edge.target_entity_id] {
262 entity_names.entry(id).or_default();
263 }
264 }
265 for (&id, name_val) in &mut entity_names {
266 if name_val.is_empty() {
267 if let Ok(Some(other)) = store.find_entity_by_id(id).await {
268 *name_val = other.name;
269 } else {
270 *name_val = format!("#{id}");
271 }
272 }
273 }
274
275 let n = edges.len();
276 let lines: Vec<String> = edges
277 .iter()
278 .map(|e| {
279 let status = if e.valid_to.is_some() {
280 let date = e
281 .valid_to
282 .as_deref()
283 .and_then(|s| s.split('T').next().or_else(|| s.split(' ').next()))
284 .unwrap_or("?");
285 format!("[expired {date}]")
286 } else {
287 "[active]".to_string()
288 };
289 let src = entity_names
290 .get(&e.source_entity_id)
291 .cloned()
292 .unwrap_or_else(|| format!("#{}", e.source_entity_id));
293 let tgt = entity_names
294 .get(&e.target_entity_id)
295 .cloned()
296 .unwrap_or_else(|| format!("#{}", e.target_entity_id));
297 format!(
298 " {status} {} --[{}/{}]--> {}: {} (confidence: {:.2})",
299 src, e.relation, e.edge_type, tgt, e.fact, e.confidence
300 )
301 })
302 .collect();
303 Ok(format!(
304 "Edge history for '{}' ({n} edges):\n{}",
305 entity.name,
306 lines.join("\n")
307 ))
308 })
309 }
310
311 fn graph_communities<'a>(
312 &'a mut self,
313 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
314 Box::pin(async move {
315 let Some(memory) = self.memory_state.persistence.memory.as_ref() else {
316 return Ok("Graph memory is not enabled.".to_owned());
317 };
318 let Some(store) = memory.graph_store.as_ref() else {
319 return Ok("Graph memory is not enabled.".to_owned());
320 };
321
322 let communities = store
323 .all_communities()
324 .await
325 .map_err(|e| CommandError::new(e.to_string()))?;
326 if communities.is_empty() {
327 return Ok("No communities detected yet. Run graph backfill first.".to_owned());
328 }
329
330 let lines: Vec<String> = communities
331 .iter()
332 .map(|c| format!(" [{}]: {}", c.name, c.summary))
333 .collect();
334 Ok(format!(
335 "Communities ({}):\n{}",
336 communities.len(),
337 lines.join("\n")
338 ))
339 })
340 }
341
342 fn graph_backfill<'a>(
343 &'a mut self,
344 limit: Option<usize>,
345 progress_cb: &'a mut (dyn FnMut(String) + Send),
346 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
347 Box::pin(async move {
348 let Some(memory) = self.memory_state.persistence.memory.clone() else {
349 return Ok("Graph memory is not enabled.".to_owned());
350 };
351 let Some(store) = memory.graph_store.clone() else {
352 return Ok("Graph memory is not enabled.".to_owned());
353 };
354
355 let total = store.unprocessed_message_count().await.unwrap_or(0);
356 let cap = limit.unwrap_or(usize::MAX);
357
358 progress_cb(format!(
359 "Starting graph backfill... ({total} unprocessed messages)"
360 ));
361
362 let batch_size = 50usize;
363 let mut processed = 0usize;
364 let mut total_entities = 0usize;
365 let mut total_edges = 0usize;
366
367 let graph_cfg = self.memory_state.extraction.graph_config.clone();
368 let provider = self.provider.clone();
369
370 loop {
371 let remaining_cap = cap.saturating_sub(processed);
372 if remaining_cap == 0 {
373 break;
374 }
375 let batch_limit = batch_size.min(remaining_cap);
376 let messages = store
377 .unprocessed_messages_for_backfill(batch_limit)
378 .await
379 .map_err(|e| CommandError::new(e.to_string()))?;
380 if messages.is_empty() {
381 break;
382 }
383
384 let ids: Vec<zeph_memory::types::MessageId> =
385 messages.iter().map(|(id, _)| *id).collect();
386
387 for (_id, content) in &messages {
388 if content.trim().is_empty() {
389 continue;
390 }
391 let extraction_cfg = GraphExtractionConfig {
392 max_entities: graph_cfg.max_entities_per_message,
393 max_edges: graph_cfg.max_edges_per_message,
394 extraction_timeout_secs: graph_cfg.extraction_timeout_secs,
395 community_refresh_interval: 0,
396 expired_edge_retention_days: graph_cfg.expired_edge_retention_days,
397 max_entities_cap: graph_cfg.max_entities,
398 community_summary_max_prompt_bytes: graph_cfg
399 .community_summary_max_prompt_bytes,
400 community_summary_concurrency: graph_cfg.community_summary_concurrency,
401 lpa_edge_chunk_size: graph_cfg.lpa_edge_chunk_size,
402 note_linking: zeph_memory::NoteLinkingConfig::default(),
403 link_weight_decay_lambda: graph_cfg.link_weight_decay_lambda,
404 link_weight_decay_interval_secs: graph_cfg.link_weight_decay_interval_secs,
405 belief_revision_enabled: graph_cfg.belief_revision.enabled,
406 belief_revision_similarity_threshold: graph_cfg
407 .belief_revision
408 .similarity_threshold,
409 conversation_id: None,
410 };
411 let pool = store.pool().clone();
412 match extract_and_store(
413 content.clone(),
414 vec![],
415 provider.clone(),
416 pool,
417 extraction_cfg,
418 None,
419 None,
420 )
421 .await
422 {
423 Ok(result) => {
424 total_entities += result.stats.entities_upserted;
425 total_edges += result.stats.edges_inserted;
426 }
427 Err(e) => {
428 tracing::warn!("backfill extraction error: {e:#}");
429 }
430 }
431 }
432
433 store
434 .mark_messages_graph_processed(&ids)
435 .await
436 .map_err(|e| CommandError::new(e.to_string()))?;
437 processed += messages.len();
438
439 progress_cb(format!(
440 "Backfill progress: {processed} messages processed, \
441 {total_entities} entities, {total_edges} edges"
442 ));
443 }
444
445 Ok(format!(
446 "Backfill complete: {total_entities} entities, {total_edges} edges \
447 extracted from {processed} messages"
448 ))
449 })
450 }
451
452 fn guidelines<'a>(
455 &'a mut self,
456 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
457 Box::pin(async move {
458 const MAX_DISPLAY_CHARS: usize = 4096;
459
460 let Some(memory) = &self.memory_state.persistence.memory else {
461 return Ok("No memory backend initialised.".to_owned());
462 };
463
464 let cid = self.memory_state.persistence.conversation_id;
465 let sqlite = memory.sqlite();
466
467 let (version, text) = sqlite
468 .load_compression_guidelines(cid)
469 .await
470 .map_err(|e: zeph_memory::MemoryError| CommandError::new(e.to_string()))?;
471
472 if version == 0 || text.is_empty() {
473 return Ok("No compression guidelines generated yet.".to_owned());
474 }
475
476 let (_, created_at) = sqlite
477 .load_compression_guidelines_meta(cid)
478 .await
479 .unwrap_or((0, String::new()));
480
481 let (body, truncated) = if text.len() > MAX_DISPLAY_CHARS {
482 let end = text.floor_char_boundary(MAX_DISPLAY_CHARS);
483 (&text[..end], true)
484 } else {
485 (text.as_str(), false)
486 };
487
488 let mut output =
489 format!("Compression Guidelines (v{version}, updated {created_at}):\n\n{body}");
490 if truncated {
491 output.push_str("\n\n[truncated]");
492 }
493 Ok(output)
494 })
495 }
496
497 fn handle_model<'a>(
500 &'a mut self,
501 arg: &'a str,
502 ) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
503 Box::pin(async move {
504 let input = if arg.is_empty() {
505 "/model".to_owned()
506 } else {
507 format!("/model {arg}")
508 };
509 self.handle_model_command_as_string(&input).await
510 })
511 }
512
513 fn handle_provider<'a>(
514 &'a mut self,
515 arg: &'a str,
516 ) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
517 Box::pin(async move { self.handle_provider_command_as_string(arg) })
518 }
519
520 fn handle_policy<'a>(
523 &'a mut self,
524 args: &'a str,
525 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
526 Box::pin(async move { Ok(self.handle_policy_command_as_string(args)) })
527 }
528
529 #[cfg(feature = "scheduler")]
532 fn list_scheduled_tasks<'a>(
533 &'a mut self,
534 ) -> Pin<Box<dyn Future<Output = Result<Option<String>, CommandError>> + Send + 'a>> {
535 Box::pin(async move {
536 let result = self
537 .handle_scheduler_list_as_string()
538 .await
539 .map_err(|e| CommandError::new(e.to_string()))?;
540 Ok(Some(result))
541 })
542 }
543
544 #[cfg(not(feature = "scheduler"))]
545 fn list_scheduled_tasks<'a>(
546 &'a mut self,
547 ) -> Pin<Box<dyn Future<Output = Result<Option<String>, CommandError>> + Send + 'a>> {
548 Box::pin(async move { Ok(None) })
549 }
550
551 fn lsp_status<'a>(
554 &'a mut self,
555 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
556 Box::pin(async move {
557 self.handle_lsp_status_as_string()
558 .await
559 .map_err(|e| CommandError::new(e.to_string()))
560 })
561 }
562
563 fn compact_context<'a>(
566 &'a mut self,
567 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
568 Box::pin(self.compact_context_command())
569 }
570
571 fn reset_conversation<'a>(
574 &'a mut self,
575 keep_plan: bool,
576 no_digest: bool,
577 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
578 Box::pin(async move {
579 match self.reset_conversation(keep_plan, no_digest).await {
580 Ok((old_id, new_id)) => {
581 let old = old_id.map_or_else(|| "none".to_string(), |id| id.0.to_string());
582 let new = new_id.map_or_else(|| "none".to_string(), |id| id.0.to_string());
583 let keep_note = if keep_plan { " (plan preserved)" } else { "" };
584 Ok(format!(
585 "New conversation started. Previous: {old} → Current: {new}{keep_note}"
586 ))
587 }
588 Err(e) => Ok(format!("Failed to start new conversation: {e}")),
589 }
590 })
591 }
592
593 fn cache_stats(&self) -> String {
596 self.tool_orchestrator.cache_stats()
597 }
598
599 fn session_status<'a>(
602 &'a mut self,
603 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
604 Box::pin(async move { Ok(self.handle_status_as_string()) })
605 }
606
607 fn guardrail_status(&self) -> String {
610 self.format_guardrail_status()
611 }
612
613 fn focus_status(&self) -> String {
616 self.format_focus_status()
617 }
618
619 fn sidequest_status(&self) -> String {
622 self.format_sidequest_status()
623 }
624
625 fn load_image<'a>(
628 &'a mut self,
629 path: &'a str,
630 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
631 Box::pin(async move { Ok(self.handle_image_as_string(path)) })
632 }
633
634 fn handle_mcp<'a>(
637 &'a mut self,
638 args: &'a str,
639 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
640 let args_owned = args.to_owned();
643 let parts: Vec<String> = args_owned.split_whitespace().map(str::to_owned).collect();
644 let sub = parts.first().cloned().unwrap_or_default();
645
646 match sub.as_str() {
647 "list" => {
648 let manager = self.mcp.manager.clone();
650 let tools_snapshot: Vec<(String, String)> = self
651 .mcp
652 .tools
653 .iter()
654 .map(|t| (t.server_id.clone(), t.name.clone()))
655 .collect();
656 Box::pin(async move {
657 use std::fmt::Write;
658 let Some(manager) = manager else {
659 return Ok("MCP is not enabled.".to_owned());
660 };
661 let server_ids = manager.list_servers().await;
662 if server_ids.is_empty() {
663 return Ok("No MCP servers connected.".to_owned());
664 }
665 let mut output = String::from("Connected MCP servers:\n");
666 let mut total = 0usize;
667 for id in &server_ids {
668 let count = tools_snapshot.iter().filter(|(sid, _)| sid == id).count();
669 total += count;
670 let _ = writeln!(output, "- {id} ({count} tools)");
671 }
672 let _ = write!(output, "Total: {total} tool(s)");
673 Ok(output)
674 })
675 }
676 "tools" => {
677 let server_id = parts.get(1).cloned();
679 let owned_tools: Vec<(String, String)> = if let Some(ref sid) = server_id {
680 self.mcp
681 .tools
682 .iter()
683 .filter(|t| &t.server_id == sid)
684 .map(|t| (t.name.clone(), t.description.clone()))
685 .collect()
686 } else {
687 Vec::new()
688 };
689 Box::pin(async move {
690 use std::fmt::Write;
691 let Some(server_id) = server_id else {
692 return Ok("Usage: /mcp tools <server_id>".to_owned());
693 };
694 if owned_tools.is_empty() {
695 return Ok(format!("No tools found for server '{server_id}'."));
696 }
697 let mut output =
698 format!("Tools for '{server_id}' ({} total):\n", owned_tools.len());
699 for (name, desc) in &owned_tools {
700 if desc.is_empty() {
701 let _ = writeln!(output, "- {name}");
702 } else {
703 let _ = writeln!(output, "- {name} — {desc}");
704 }
705 }
706 Ok(output)
707 })
708 }
709 _ => Box::pin(async move {
716 self.handle_mcp_command(&args_owned)
717 .await
718 .map_err(|e| CommandError::new(e.to_string()))
719 }),
720 }
721 }
722
723 fn handle_skill<'a>(
726 &'a mut self,
727 args: &'a str,
728 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
729 let args_owned = args.to_owned();
730 Box::pin(async move {
731 self.handle_skill_command_as_string(&args_owned)
732 .await
733 .map_err(|e| CommandError::new(e.to_string()))
734 })
735 }
736
737 fn handle_skills<'a>(
740 &'a mut self,
741 args: &'a str,
742 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
743 let args_owned = args.to_owned();
744 Box::pin(async move {
745 self.handle_skills_as_string(&args_owned)
746 .await
747 .map_err(|e| CommandError::new(e.to_string()))
748 })
749 }
750
751 fn handle_feedback_command<'a>(
754 &'a mut self,
755 args: &'a str,
756 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
757 let args_owned = args.to_owned();
758 Box::pin(async move {
759 self.handle_feedback_as_string(&args_owned)
760 .await
761 .map_err(|e| CommandError::new(e.to_string()))
762 })
763 }
764
765 #[cfg(feature = "scheduler")]
768 fn handle_plan<'a>(
769 &'a mut self,
770 input: &'a str,
771 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
772 Box::pin(async move {
773 self.dispatch_plan_command_as_string(input)
774 .await
775 .map_err(|e| CommandError::new(e.to_string()))
776 })
777 }
778
779 #[cfg(not(feature = "scheduler"))]
780 fn handle_plan<'a>(
781 &'a mut self,
782 _input: &'a str,
783 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
784 Box::pin(async move { Ok(String::new()) })
785 }
786
787 fn handle_experiment<'a>(
790 &'a mut self,
791 input: &'a str,
792 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
793 Box::pin(async move {
794 self.handle_experiment_command_as_string(input)
795 .await
796 .map_err(|e| CommandError::new(e.to_string()))
797 })
798 }
799
800 fn handle_agent_dispatch<'a>(
803 &'a mut self,
804 input: &'a str,
805 ) -> Pin<Box<dyn Future<Output = Result<Option<String>, CommandError>> + Send + 'a>> {
806 Box::pin(async move {
807 match self.dispatch_agent_command(input).await {
808 Some(Err(e)) => Err(CommandError::new(e.to_string())),
809 Some(Ok(())) | None => Ok(None),
810 }
811 })
812 }
813}
814
815impl From<AgentError> for CommandError {
817 fn from(e: AgentError) -> Self {
818 Self(e.to_string())
819 }
820}