Skip to main content

sgr_agent/agents/
clarification.rs

1//! Built-in system tools for interactive agents.
2//!
3//! - `ClarificationTool` — pause the loop to ask the user a question
4//! - `PlanTool` — submit a structured implementation plan
5
6use crate::agent_tool::{Tool, ToolError, ToolOutput};
7use crate::context::AgentContext;
8use serde_json::Value;
9
10/// Ask the user a clarifying question. Pauses the agent loop until the user responds.
11///
12/// When used with `run_loop_interactive`, the loop calls `on_input(question)` and
13/// injects the user's response as the tool result. With plain `run_loop`, emits
14/// `LoopEvent::WaitingForInput` and continues with a placeholder.
15pub struct ClarificationTool;
16
17#[async_trait::async_trait]
18impl Tool for ClarificationTool {
19    fn name(&self) -> &str {
20        "ask_user"
21    }
22    fn description(&self) -> &str {
23        "Ask the user a clarifying question when you need more information to proceed"
24    }
25    fn is_system(&self) -> bool {
26        true
27    }
28    fn parameters_schema(&self) -> Value {
29        serde_json::json!({
30            "type": "object",
31            "properties": {
32                "question": {
33                    "type": "string",
34                    "description": "The question to ask the user"
35                }
36            },
37            "required": ["question"]
38        })
39    }
40    async fn execute(&self, args: Value, _ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
41        let question = args
42            .get("question")
43            .and_then(|v| v.as_str())
44            .unwrap_or("Could you provide more details?");
45        Ok(ToolOutput::waiting(question))
46    }
47}
48
49/// Submit a structured implementation plan.
50///
51/// The plan is stored in `ctx.custom["plan"]` for retrieval after the loop completes.
52/// Signals `done` — the planning phase is complete.
53///
54/// Expected args:
55/// ```json
56/// {
57///   "summary": "Add user authentication",
58///   "steps": [
59///     { "description": "Create auth module", "files": ["src/auth.rs"], "tool_hints": ["write_file"] },
60///     { "description": "Add tests", "files": ["tests/auth.rs"] }
61///   ]
62/// }
63/// ```
64pub struct PlanTool;
65
66#[async_trait::async_trait]
67impl Tool for PlanTool {
68    fn name(&self) -> &str {
69        "submit_plan"
70    }
71    fn description(&self) -> &str {
72        "Submit your implementation plan after analyzing the codebase. Call when ready to present the plan."
73    }
74    fn is_system(&self) -> bool {
75        true
76    }
77    fn parameters_schema(&self) -> Value {
78        serde_json::json!({
79            "type": "object",
80            "properties": {
81                "summary": {
82                    "type": "string",
83                    "description": "Brief summary of what the plan achieves"
84                },
85                "steps": {
86                    "type": "array",
87                    "description": "Ordered list of implementation steps",
88                    "items": {
89                        "type": "object",
90                        "properties": {
91                            "description": {
92                                "type": "string",
93                                "description": "What this step does"
94                            },
95                            "files": {
96                                "type": "array",
97                                "items": { "type": "string" },
98                                "description": "Files to create or modify"
99                            },
100                            "tool_hints": {
101                                "type": "array",
102                                "items": { "type": "string" },
103                                "description": "Tools likely needed for this step"
104                            }
105                        },
106                        "required": ["description"]
107                    }
108                }
109            },
110            "required": ["summary", "steps"]
111        })
112    }
113    async fn execute(&self, args: Value, ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
114        let summary = args
115            .get("summary")
116            .and_then(|v| v.as_str())
117            .unwrap_or("Plan submitted")
118            .to_string();
119        // Store the full plan in context
120        ctx.set("plan", args);
121        Ok(ToolOutput::done(format!("Plan submitted: {summary}")))
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[tokio::test]
130    async fn clarification_returns_waiting() {
131        let tool = ClarificationTool;
132        let mut ctx = AgentContext::new();
133        let args = serde_json::json!({"question": "Which database?"});
134        let output = tool.execute(args, &mut ctx).await.unwrap();
135        assert!(output.waiting);
136        assert!(!output.done);
137        assert_eq!(output.content, "Which database?");
138    }
139
140    #[tokio::test]
141    async fn clarification_default_question() {
142        let tool = ClarificationTool;
143        let mut ctx = AgentContext::new();
144        let output = tool.execute(serde_json::json!({}), &mut ctx).await.unwrap();
145        assert!(output.waiting);
146        assert!(output.content.contains("more details"));
147    }
148
149    #[tokio::test]
150    async fn plan_tool_stores_and_completes() {
151        let tool = PlanTool;
152        let mut ctx = AgentContext::new();
153        let args = serde_json::json!({
154            "summary": "Add auth",
155            "steps": [
156                {"description": "Create module", "files": ["src/auth.rs"]},
157                {"description": "Add tests"}
158            ]
159        });
160        let output = tool.execute(args, &mut ctx).await.unwrap();
161        assert!(output.done);
162        assert!(output.content.contains("Add auth"));
163
164        // Plan stored in context
165        let plan = ctx.get("plan").unwrap();
166        assert_eq!(plan["steps"].as_array().unwrap().len(), 2);
167    }
168
169    #[test]
170    fn clarification_is_system_tool() {
171        assert!(ClarificationTool.is_system());
172    }
173
174    #[test]
175    fn plan_is_system_tool() {
176        assert!(PlanTool.is_system());
177    }
178}