Skip to main content

sgr_agent/
reasoning_tool.rs

1//! ReasoningToolBuilder — build a structured reasoning tool from custom fields.
2//!
3//! Equivalent to Python SGR's `NextStepToolsBuilder` pattern.
4//! Agent defines reasoning schema fields, builder creates ToolDef.
5
6use crate::tool::ToolDef;
7use serde_json::{Value, json};
8
9/// Builder for reasoning/think tools with custom schema fields.
10///
11/// ```ignore
12/// let think = ReasoningToolBuilder::new("think")
13///     .description("Reason about the task before acting")
14///     .field("task_type", json!({"type": "string", "enum": ["search", "edit", "delete"]}))
15///     .field("plan", json!({"type": "string"}))
16///     .field("security", json!({"type": "string", "enum": ["safe", "blocked"]}))
17///     .optional("confidence", json!({"type": "number"}))
18///     .build();
19/// ```
20pub struct ReasoningToolBuilder {
21    name: String,
22    description: String,
23    properties: serde_json::Map<String, Value>,
24    required: Vec<String>,
25}
26
27impl ReasoningToolBuilder {
28    pub fn new(name: impl Into<String>) -> Self {
29        Self {
30            name: name.into(),
31            description: String::new(),
32            properties: serde_json::Map::new(),
33            required: Vec::new(),
34        }
35    }
36
37    pub fn description(mut self, desc: impl Into<String>) -> Self {
38        self.description = desc.into();
39        self
40    }
41
42    /// Add a required field to the reasoning schema.
43    pub fn field(mut self, name: impl Into<String>, schema: Value) -> Self {
44        let name = name.into();
45        self.required.push(name.clone());
46        self.properties.insert(name, schema);
47        self
48    }
49
50    /// Add an optional field (not in required array).
51    pub fn optional(mut self, name: impl Into<String>, schema: Value) -> Self {
52        self.properties.insert(name.into(), schema);
53        self
54    }
55
56    /// Build the ToolDef.
57    pub fn build(self) -> ToolDef {
58        ToolDef {
59            name: self.name,
60            description: self.description,
61            parameters: json!({
62                "type": "object",
63                "properties": self.properties,
64                "required": self.required,
65                "additionalProperties": false
66            }),
67        }
68    }
69}
70
71/// Preset: minimal reasoning tool (situation + plan + done).
72pub fn minimal_reasoning(name: &str) -> ToolDef {
73    ReasoningToolBuilder::new(name)
74        .description("Assess situation and plan next action")
75        .field(
76            "situation",
77            json!({"type": "string", "description": "Current state assessment"}),
78        )
79        .field(
80            "plan",
81            json!({"type": "string", "description": "Next action to take"}),
82        )
83        .field(
84            "done",
85            json!({"type": "boolean", "description": "true when task complete"}),
86        )
87        .build()
88}
89
90/// Preset: agent reasoning with task routing (PAC1/CRM style).
91pub fn routed_reasoning(name: &str, task_types: &[&str], security_levels: &[&str]) -> ToolDef {
92    let tt_enum: Vec<Value> = task_types
93        .iter()
94        .map(|s| Value::String(s.to_string()))
95        .collect();
96    let sec_enum: Vec<Value> = security_levels
97        .iter()
98        .map(|s| Value::String(s.to_string()))
99        .collect();
100
101    ReasoningToolBuilder::new(name)
102        .description("Reason about the task. ALWAYS call this AND an action tool together.")
103        .field("task_type", json!({"type": "string", "enum": tt_enum}))
104        .field("security", json!({"type": "string", "enum": sec_enum}))
105        .field("reasoning", json!({"type": "string", "description": "What you observe + self-check (Am I repeating? Right file? Evidence?)"}))
106        .field("next_action", json!({"type": "string", "description": "What you will do now and why"}))
107        .optional("confidence", json!({"type": "number", "description": "0.0-1.0 how sure you are"}))
108        .build()
109}
110
111/// Build reasoning tool from AgentRuntime context.
112/// Adapts schema based on runtime signals (inbox, threats, OTP).
113pub fn from_runtime(name: &str, runtime: &dyn crate::agent_runtime::AgentRuntime) -> ToolDef {
114    let ctx = runtime.context_summary();
115    let desc = if ctx.is_empty() {
116        "Reason about the task. ALWAYS call this AND an action tool together.".to_string()
117    } else {
118        format!(
119            "Reason about the task [{}]. ALWAYS call this AND an action tool together.",
120            ctx
121        )
122    };
123
124    let mut b = ReasoningToolBuilder::new(name)
125        .description(desc)
126        .field(
127            "reasoning",
128            json!({"type": "string", "description": "What you observe + self-check"}),
129        )
130        .field(
131            "next_action",
132            json!({"type": "string", "description": "What you will do now"}),
133        );
134
135    if runtime.has_otp() {
136        b = b.optional(
137            "otp_action",
138            json!({"type": "string", "enum": ["verify", "process", "deny"]}),
139        );
140    }
141    if runtime.has_threat() {
142        b = b.optional(
143            "threat_assessment",
144            json!({"type": "string", "enum": ["safe", "suspicious", "blocked"]}),
145        );
146    }
147    b = b.optional(
148        "confidence",
149        json!({"type": "number", "description": "0.0-1.0"}),
150    );
151    b.build()
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn builder_creates_valid_schema() {
160        let tool = ReasoningToolBuilder::new("think")
161            .description("Test reasoning")
162            .field("plan", json!({"type": "string"}))
163            .field("done", json!({"type": "boolean"}))
164            .optional("confidence", json!({"type": "number"}))
165            .build();
166
167        assert_eq!(tool.name, "think");
168        assert_eq!(tool.parameters["required"].as_array().unwrap().len(), 2);
169        assert!(tool.parameters["properties"]["confidence"].is_object());
170    }
171
172    #[test]
173    fn minimal_preset() {
174        let tool = minimal_reasoning("reason");
175        assert_eq!(tool.name, "reason");
176        assert_eq!(tool.parameters["required"].as_array().unwrap().len(), 3);
177    }
178
179    #[test]
180    fn routed_preset() {
181        let tool = routed_reasoning("think", &["search", "edit"], &["safe", "blocked"]);
182        assert_eq!(
183            tool.parameters["properties"]["task_type"]["enum"]
184                .as_array()
185                .unwrap()
186                .len(),
187            2
188        );
189    }
190}