oxios_kernel/tools/
program_tool.rs1use std::sync::Arc;
8
9use async_trait::async_trait;
10use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
11use serde_json::{json, Value};
12use tokio::sync::oneshot;
13
14use super::exec_tool::ExecTool;
15use crate::program::{ProgramHostRequirements, ToolDef};
16use crate::KernelHandle;
17
18pub struct ProgramTool {
23 full_name: String,
25 description: String,
27 binary: String,
29 default_args: Vec<String>,
31 exec_tool: Arc<ExecTool>,
33}
34
35impl ProgramTool {
36 pub fn from_kernel(kernel: &KernelHandle) -> Self {
45 let exec = Arc::new(ExecTool::from_kernel(kernel));
46 Self {
47 full_name: "program".to_string(),
48 description: "Run installable program tools. Pass {name: tool-name, args: [...]}"
49 .to_string(),
50 binary: "".to_string(),
51 default_args: Vec::new(),
52 exec_tool: exec,
53 }
54 }
55
56 pub fn from_definition(
61 program_name: &str,
62 tool_def: &ToolDef,
63 _host_requirements: &ProgramHostRequirements,
64 exec: Arc<ExecTool>,
65 ) -> Self {
66 let parts: Vec<&str> = tool_def.command.split_whitespace().collect();
68 let binary = parts.first().unwrap_or(&"").to_string();
69 let default_args = parts.iter().skip(1).map(|s| s.to_string()).collect();
70
71 Self {
72 full_name: format!("program:{}:{}", program_name, tool_def.name),
73 description: tool_def.description.clone(),
74 binary,
75 default_args,
76 exec_tool: exec,
77 }
78 }
79}
80
81impl std::fmt::Debug for ProgramTool {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("ProgramTool")
84 .field("full_name", &self.full_name)
85 .field("binary", &self.binary)
86 .finish()
87 }
88}
89
90#[async_trait]
91impl AgentTool for ProgramTool {
92 fn name(&self) -> &str {
93 &self.full_name
94 }
95
96 fn label(&self) -> &str {
97 &self.full_name
98 }
99
100 fn description(&self) -> &str {
101 &self.description
102 }
103
104 fn parameters_schema(&self) -> Value {
105 json!({
106 "type": "object",
107 "properties": {
108 "args": {
109 "type": "array",
110 "items": { "type": "string" },
111 "description": "Additional arguments to pass to the command"
112 }
113 }
114 })
115 }
116
117 async fn execute(
118 &self,
119 _tool_call_id: &str,
120 params: Value,
121 signal: Option<oneshot::Receiver<()>>,
122 _ctx: &ToolContext,
123 ) -> Result<AgentToolResult, String> {
124 let user_args: Vec<String> = params
126 .get("args")
127 .and_then(|v| v.as_array())
128 .map(|arr| {
129 arr.iter()
130 .filter_map(|v| v.as_str().map(String::from))
131 .collect()
132 })
133 .unwrap_or_default();
134
135 let all_args: Vec<String> = self
137 .default_args
138 .iter()
139 .chain(user_args.iter())
140 .cloned()
141 .collect();
142
143 let exec_params = json!({
145 "binary": self.binary,
146 "args": all_args,
147 });
148 let ctx = oxi_sdk::ToolContext::default();
149 self.exec_tool
150 .execute(&format!("pg:{}", self.full_name), exec_params, signal, &ctx)
151 .await
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
161 fn test_command_parsing() {
162 let tool_def = ToolDef {
163 name: "create_pr".to_string(),
164 description: "Create a PR".to_string(),
165 arguments: vec![],
166 command: "gh pr create".to_string(),
167 };
168 let host_reqs = ProgramHostRequirements::default();
169
170 let exec_config = Arc::new(crate::config::ExecConfig::default());
171 let exec_access = Arc::new(parking_lot::Mutex::new(
172 crate::access_manager::AccessManager::new(),
173 ));
174 let exec = Arc::new(ExecTool::new(exec_config, exec_access));
175
176 let tool = ProgramTool::from_definition("github", &tool_def, &host_reqs, exec);
177
178 assert_eq!(tool.full_name, "program:github:create_pr");
179 assert_eq!(tool.binary, "gh");
180 assert_eq!(tool.default_args, vec!["pr", "create"]);
181 }
182
183 #[test]
185 fn test_single_word_command() {
186 let tool_def = ToolDef {
187 name: "status".to_string(),
188 description: "Show git status".to_string(),
189 arguments: vec![],
190 command: "git".to_string(),
191 };
192 let host_reqs = ProgramHostRequirements::default();
193
194 let exec_config = Arc::new(crate::config::ExecConfig::default());
195 let exec_access = Arc::new(parking_lot::Mutex::new(
196 crate::access_manager::AccessManager::new(),
197 ));
198 let exec = Arc::new(ExecTool::new(exec_config, exec_access));
199
200 let tool = ProgramTool::from_definition("git-tools", &tool_def, &host_reqs, exec);
201
202 assert_eq!(tool.full_name, "program:git-tools:status");
203 assert_eq!(tool.binary, "git");
204 assert!(tool.default_args.is_empty());
205 }
206
207 #[test]
209 fn test_command_with_flags() {
210 let tool_def = ToolDef {
211 name: "fetch".to_string(),
212 description: "Fetch from remote".to_string(),
213 arguments: vec![],
214 command: "git fetch --all --prune".to_string(),
215 };
216 let host_reqs = ProgramHostRequirements::default();
217
218 let exec_config = Arc::new(crate::config::ExecConfig::default());
219 let exec_access = Arc::new(parking_lot::Mutex::new(
220 crate::access_manager::AccessManager::new(),
221 ));
222 let exec = Arc::new(ExecTool::new(exec_config, exec_access));
223
224 let tool = ProgramTool::from_definition("git-tools", &tool_def, &host_reqs, exec);
225
226 assert_eq!(tool.binary, "git");
227 assert_eq!(tool.default_args, vec!["fetch", "--all", "--prune"]);
228 }
229}