Skip to main content

opendev_tools_impl/
task_complete.rs

1//! Task complete tool — signal explicit task completion.
2//!
3//! Instead of relying on implicit termination (no tool calls = done),
4//! agents call this tool to end the ReAct loop. This provides:
5//! - Explicit completion signal (no ambiguity)
6//! - Required summary of what was accomplished
7//! - Natural error recovery (agent keeps trying until this is called)
8//! - Clean conversation history
9
10use std::collections::HashMap;
11
12use opendev_tools_core::{BaseTool, ToolContext, ToolResult};
13
14/// Valid completion statuses.
15const VALID_STATUSES: &[&str] = &["success", "partial", "failed"];
16
17/// Tool that signals explicit task completion.
18#[derive(Debug)]
19pub struct TaskCompleteTool;
20
21#[async_trait::async_trait]
22impl BaseTool for TaskCompleteTool {
23    fn name(&self) -> &str {
24        "task_complete"
25    }
26
27    fn description(&self) -> &str {
28        "Call this tool when you have completed the user's request. \
29         You MUST call this tool to end the conversation. \
30         Provide your response to the user in the result parameter."
31    }
32
33    fn parameter_schema(&self) -> serde_json::Value {
34        serde_json::json!({
35            "type": "object",
36            "properties": {
37                "result": {
38                    "type": "string",
39                    "description": "Your response to the user — what was accomplished or your conversational reply"
40                },
41                "status": {
42                    "type": "string",
43                    "description": "Completion status: 'success', 'partial', or 'failed'",
44                    "enum": VALID_STATUSES,
45                    "default": "success"
46                }
47            },
48            "required": ["result"]
49        })
50    }
51
52    async fn execute(
53        &self,
54        args: HashMap<String, serde_json::Value>,
55        _ctx: &ToolContext,
56    ) -> ToolResult {
57        let summary = match args
58            .get("result")
59            .or_else(|| args.get("summary"))
60            .and_then(|v| v.as_str())
61        {
62            Some(s) if !s.trim().is_empty() => s.trim(),
63            _ => return ToolResult::fail("Result is required for task_complete"),
64        };
65
66        let status = args
67            .get("status")
68            .and_then(|v| v.as_str())
69            .unwrap_or("success");
70
71        if !VALID_STATUSES.contains(&status) {
72            return ToolResult::fail(format!(
73                "Invalid status '{status}'. Must be one of: {}",
74                VALID_STATUSES.join(", ")
75            ));
76        }
77
78        let output = format!("Task completed ({status}): {summary}");
79
80        let mut metadata = HashMap::new();
81        metadata.insert("_completion".into(), serde_json::json!(true));
82        metadata.insert("summary".into(), serde_json::json!(summary));
83        metadata.insert("status".into(), serde_json::json!(status));
84
85        ToolResult::ok_with_metadata(output, metadata)
86    }
87}
88
89#[cfg(test)]
90#[path = "task_complete_tests.rs"]
91mod tests;