Skip to main content

do_memory_mcp/server/tools/
episode_complete.rs

1//! Episode completion tool for MCP server
2//!
3//! This module provides the tool for finalizing episodes by recording
4//! the outcome and triggering the learning cycle.
5
6use crate::server::MemoryMCPServer;
7use anyhow::{Result, anyhow};
8use do_memory_core::TaskOutcome;
9use serde_json::{Value, json};
10use tracing::debug;
11use tracing::info;
12use uuid::Uuid;
13
14impl MemoryMCPServer {
15    /// Complete an episode with an outcome
16    ///
17    /// This tool finalizes an episode by recording the outcome and
18    /// triggering the learning cycle (reward calculation, reflection,
19    /// pattern extraction).
20    ///
21    /// # Arguments (from JSON)
22    ///
23    /// * `episode_id` - UUID of the episode to complete
24    /// * `outcome_type` - Type of outcome ("success", "partial_success", "failure")
25    /// * `verdict` - Description of the outcome (required for success/partial)
26    /// * `artifacts` - Array of artifact names (optional, for success)
27    /// * `completed` - Array of completed items (required for partial_success)
28    /// * `failed` - Array of failed items (required for partial_success)
29    /// * `reason` - Failure reason (required for failure)
30    /// * `error_details` - Detailed error information (optional, for failure)
31    pub async fn complete_episode_tool(&self, args: Value) -> Result<Value> {
32        debug!("Completing episode with args: {}", args);
33
34        // Extract required fields
35        let episode_id_str = args
36            .get("episode_id")
37            .and_then(|v| v.as_str())
38            .ok_or_else(|| anyhow!("Missing required field: episode_id"))?;
39
40        let episode_id = Uuid::parse_str(episode_id_str)
41            .map_err(|e| anyhow!("Invalid episode_id format: {}", e))?;
42
43        let outcome_type = args
44            .get("outcome_type")
45            .and_then(|v| v.as_str())
46            .ok_or_else(|| anyhow!("Missing required field: outcome_type"))?;
47
48        // Parse outcome based on type
49        let outcome = match outcome_type {
50            "success" => {
51                let verdict = args
52                    .get("verdict")
53                    .and_then(|v| v.as_str())
54                    .ok_or_else(|| anyhow!("Missing required field for success: verdict"))?
55                    .to_string();
56
57                let artifacts = args
58                    .get("artifacts")
59                    .and_then(|v| v.as_array())
60                    .map(|arr| {
61                        arr.iter()
62                            .filter_map(|v| v.as_str().map(|s| s.to_string()))
63                            .collect()
64                    })
65                    .unwrap_or_default();
66
67                TaskOutcome::Success { verdict, artifacts }
68            }
69            "partial_success" => {
70                let verdict = args
71                    .get("verdict")
72                    .and_then(|v| v.as_str())
73                    .ok_or_else(|| anyhow!("Missing required field for partial_success: verdict"))?
74                    .to_string();
75
76                let completed = args
77                    .get("completed")
78                    .and_then(|v| v.as_array())
79                    .ok_or_else(|| {
80                        anyhow!("Missing required field for partial_success: completed")
81                    })?
82                    .iter()
83                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
84                    .collect();
85
86                let failed = args
87                    .get("failed")
88                    .and_then(|v| v.as_array())
89                    .ok_or_else(|| anyhow!("Missing required field for partial_success: failed"))?
90                    .iter()
91                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
92                    .collect();
93
94                TaskOutcome::PartialSuccess {
95                    verdict,
96                    completed,
97                    failed,
98                }
99            }
100            "failure" => {
101                let reason = args
102                    .get("reason")
103                    .and_then(|v| v.as_str())
104                    .ok_or_else(|| anyhow!("Missing required field for failure: reason"))?
105                    .to_string();
106
107                let error_details = args
108                    .get("error_details")
109                    .and_then(|v| v.as_str())
110                    .map(|s| s.to_string());
111
112                TaskOutcome::Failure {
113                    reason,
114                    error_details,
115                }
116            }
117            _ => {
118                return Err(anyhow!(
119                    "Invalid outcome_type: {}. Must be one of: success, partial_success, failure",
120                    outcome_type
121                ));
122            }
123        };
124
125        // Complete the episode
126        self.memory
127            .complete_episode(episode_id, outcome.clone())
128            .await
129            .map_err(|e| anyhow!("Failed to complete episode: {}", e))?;
130
131        info!(
132            episode_id = %episode_id,
133            outcome_type = %outcome_type,
134            "Completed episode via MCP"
135        );
136
137        Ok(json!({
138            "success": true,
139            "episode_id": episode_id.to_string(),
140            "outcome_type": outcome_type,
141            "message": "Episode completed successfully. Learning cycle triggered (reward, reflection, patterns)."
142        }))
143    }
144}