oxios_kernel/tools/kernel/
budget_tool.rs1use std::sync::Arc;
16
17use async_trait::async_trait;
18use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
19use serde_json::{json, Value};
20use tokio::sync::oneshot;
21
22use crate::budget::{BudgetLimit, BudgetManager};
23use crate::kernel_handle::KernelHandle;
24use crate::types::AgentId;
25
26pub struct BudgetTool {
40 budget_manager: Arc<BudgetManager>,
41}
42
43impl BudgetTool {
44 pub fn from_kernel(kernel: &KernelHandle) -> Self {
48 Self {
49 budget_manager: kernel.agents.budget_manager.clone(),
50 }
51 }
52}
53
54impl std::fmt::Debug for BudgetTool {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("BudgetTool").finish()
57 }
58}
59
60#[async_trait]
61impl AgentTool for BudgetTool {
62 fn name(&self) -> &str {
63 "budget"
64 }
65
66 fn label(&self) -> &str {
67 "Budget"
68 }
69
70 fn description(&self) -> &'static str {
71 "Manage agent budgets — check remaining tokens/calls, set limits, reserve tokens, or reset the budget window. \
72 Actions: check, set, reserve, reset."
73 }
74
75 fn parameters_schema(&self) -> Value {
76 json!({
77 "type": "object",
78 "properties": {
79 "action": {
80 "type": "string",
81 "enum": ["check", "set", "reserve", "reset"],
82 "description": "Budget operation to perform"
83 },
84 "agent_id": {
85 "type": "string",
86 "description": "Agent UUID to operate on"
87 },
88 "limit": {
89 "type": "integer",
90 "description": "Token budget limit (set action only, default: 100000)"
91 },
92 "tokens": {
93 "type": "integer",
94 "description": "Number of tokens to reserve (reserve action only)"
95 }
96 },
97 "required": ["action", "agent_id"]
98 })
99 }
100
101 async fn execute(
102 &self,
103 _tool_call_id: &str,
104 params: Value,
105 _signal: Option<oneshot::Receiver<()>>,
106 _ctx: &ToolContext,
107 ) -> Result<AgentToolResult, String> {
108 let action = params
109 .get("action")
110 .and_then(|v| v.as_str())
111 .ok_or_else(|| "Missing required parameter: action".to_string())?;
112
113 let agent_id_str = params
114 .get("agent_id")
115 .and_then(|v| v.as_str())
116 .ok_or_else(|| "Missing required parameter: agent_id".to_string())?;
117
118 let agent_id: AgentId = match uuid::Uuid::parse_str(agent_id_str) {
119 Ok(id) => id,
120 Err(e) => return Ok(AgentToolResult::error(format!("Invalid agent_id: {e}"))),
121 };
122
123 match action {
124 "check" => {
125 let info = self.budget_manager.remaining(&agent_id);
126 Ok(AgentToolResult::success(
127 serde_json::to_string_pretty(&json!({
128 "agent_id": agent_id_str,
129 "tokens_remaining": info.tokens_remaining,
130 "calls_remaining": info.calls_remaining,
131 "window_remaining_secs": info.window_remaining_secs,
132 "is_exhausted": info.is_exhausted,
133 }))
134 .unwrap_or_default(),
135 ))
136 }
137
138 "set" => {
139 let token_limit = params["limit"].as_u64().unwrap_or(100_000);
140
141 let limit = BudgetLimit {
142 agent_id,
143 token_budget: token_limit,
144 calls_budget: 1_000,
145 window_secs: 3_600, };
147
148 self.budget_manager.set_budget(limit);
149
150 Ok(AgentToolResult::success(format!(
151 "Budget set for agent '{}': {} tokens, 1000 calls, 1h window.",
152 agent_id_str, token_limit,
153 )))
154 }
155
156 "reserve" => {
157 let tokens = params["tokens"].as_u64().unwrap_or(0);
158 if tokens == 0 {
159 return Ok(AgentToolResult::error(
160 "reserve requires 'tokens' parameter (> 0)",
161 ));
162 }
163
164 match self.budget_manager.reserve(&agent_id, tokens) {
165 Ok(()) => Ok(AgentToolResult::success(format!(
166 "Reserved {} tokens for agent '{}'.",
167 tokens, agent_id_str,
168 ))),
169 Err(exceeded) => Ok(AgentToolResult::error(format!(
170 "Budget exceeded: {}",
171 exceeded
172 ))),
173 }
174 }
175
176 "reset" => {
177 self.budget_manager.reset_window(&agent_id);
178 Ok(AgentToolResult::success(format!(
179 "Budget window reset for agent '{}'.",
180 agent_id_str,
181 )))
182 }
183
184 other => Err(format!(
185 "Unknown budget action '{}'. Valid: check, set, reserve, reset",
186 other
187 )),
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_schema_structure() {
198 let schema = json!({
199 "type": "object",
200 "properties": {
201 "action": {
202 "type": "string",
203 "enum": ["check", "set", "reserve", "reset"]
204 },
205 "agent_id": { "type": "string" },
206 "limit": { "type": "integer" },
207 "tokens": { "type": "integer" }
208 },
209 "required": ["action", "agent_id"]
210 });
211
212 let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
213 assert_eq!(actions.len(), 4);
214 assert!(actions.iter().any(|a| a == "check"));
215 assert!(actions.iter().any(|a| a == "set"));
216 assert!(actions.iter().any(|a| a == "reserve"));
217 assert!(actions.iter().any(|a| a == "reset"));
218
219 let required = schema["required"].as_array().unwrap();
220 assert!(required.iter().any(|r| r.as_str() == Some("action")));
221 assert!(required.iter().any(|r| r.as_str() == Some("agent_id")));
222 }
223}