sgr_agent/
reasoning_tool.rs1use crate::tool::ToolDef;
7use serde_json::{Value, json};
8
9pub 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 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 pub fn optional(mut self, name: impl Into<String>, schema: Value) -> Self {
52 self.properties.insert(name.into(), schema);
53 self
54 }
55
56 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
71pub 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
90pub 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
111pub 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}