1use super::{Tool, ToolResult};
7use anyhow::{Context, Result};
8use async_trait::async_trait;
9use serde::Deserialize;
10use serde_json::{Value, json};
11use std::sync::Arc;
12use tokio::sync::OnceCell;
13use uuid::Uuid;
14
15use crate::okr::{KeyResult, Okr, OkrRepository, OkrRun, OkrRunStatus, OkrStatus};
16
17static OKR_REPO: OnceCell<Arc<OkrRepository>> = OnceCell::const_new();
19
20async fn get_repo() -> Result<&'static Arc<OkrRepository>> {
21 OKR_REPO
22 .get_or_try_init(|| async {
23 let repo = OkrRepository::from_config().await?;
24 Ok::<_, anyhow::Error>(Arc::new(repo))
25 })
26 .await
27}
28
29pub struct OkrTool;
30
31impl Default for OkrTool {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl OkrTool {
38 pub fn new() -> Self {
39 Self
40 }
41}
42
43#[derive(Deserialize)]
44struct Params {
45 action: String,
46 #[serde(default)]
47 id: Option<String>,
48 #[serde(default)]
49 title: Option<String>,
50 #[serde(default)]
51 description: Option<String>,
52 #[serde(default)]
53 status: Option<String>,
54 #[serde(default)]
55 owner: Option<String>,
56 #[serde(default)]
57 tenant_id: Option<String>,
58 #[serde(default)]
59 key_results: Option<Vec<KrParam>>,
60 #[serde(default)]
62 okr_id: Option<String>,
63 #[serde(default)]
64 name: Option<String>,
65 #[serde(default)]
66 correlation_id: Option<String>,
67 #[serde(default)]
68 session_id: Option<String>,
69 #[serde(default)]
70 checkpoint_id: Option<String>,
71}
72
73#[derive(Deserialize)]
74struct KrParam {
75 title: String,
76 target_value: f64,
77 #[serde(default = "default_unit")]
78 unit: String,
79}
80
81fn default_unit() -> String {
82 "%".to_string()
83}
84
85fn parse_uuid(s: &str, field: &str) -> Result<Uuid> {
86 Uuid::parse_str(s).with_context(|| format!("Invalid UUID for {field}: {s}"))
87}
88
89fn parse_okr_status(s: &str) -> Result<OkrStatus> {
90 match s {
91 "draft" => Ok(OkrStatus::Draft),
92 "active" => Ok(OkrStatus::Active),
93 "completed" => Ok(OkrStatus::Completed),
94 "cancelled" => Ok(OkrStatus::Cancelled),
95 "on_hold" => Ok(OkrStatus::OnHold),
96 _ => anyhow::bail!(
97 "Unknown OKR status: {s}. Use: draft, active, completed, cancelled, on_hold"
98 ),
99 }
100}
101
102fn parse_run_status(s: &str) -> Result<OkrRunStatus> {
103 match s {
104 "draft" => Ok(OkrRunStatus::Draft),
105 "pending_approval" => Ok(OkrRunStatus::PendingApproval),
106 "approved" => Ok(OkrRunStatus::Approved),
107 "running" => Ok(OkrRunStatus::Running),
108 "paused" => Ok(OkrRunStatus::Paused),
109 "waiting_approval" => Ok(OkrRunStatus::WaitingApproval),
110 "completed" => Ok(OkrRunStatus::Completed),
111 "failed" => Ok(OkrRunStatus::Failed),
112 "denied" => Ok(OkrRunStatus::Denied),
113 "cancelled" => Ok(OkrRunStatus::Cancelled),
114 _ => anyhow::bail!("Unknown run status: {s}"),
115 }
116}
117
118#[async_trait]
119impl Tool for OkrTool {
120 fn id(&self) -> &str {
121 "okr"
122 }
123 fn name(&self) -> &str {
124 "OKR Manager"
125 }
126 fn description(&self) -> &str {
127 "Manage Objectives and Key Results (OKRs) and their execution runs. \
128 Actions: create_okr, get_okr, update_okr, delete_okr, list_okrs, query_okrs, \
129 create_run, get_run, update_run, delete_run, list_runs, query_runs, stats."
130 }
131 fn parameters(&self) -> Value {
132 json!({
133 "type": "object",
134 "properties": {
135 "action": {
136 "type": "string",
137 "enum": [
138 "create_okr", "get_okr", "update_okr", "delete_okr",
139 "list_okrs", "query_okrs",
140 "create_run", "get_run", "update_run", "delete_run",
141 "list_runs", "query_runs",
142 "stats"
143 ],
144 "description": "Action to perform"
145 },
146 "id": {"type": "string", "description": "UUID of the OKR or run"},
147 "title": {"type": "string", "description": "OKR title (for create/update)"},
148 "description": {"type": "string", "description": "OKR description"},
149 "status": {
150 "type": "string",
151 "description": "Status filter or new status. OKR: draft/active/completed/cancelled/on_hold. Run: draft/pending_approval/approved/running/paused/waiting_approval/completed/failed/denied/cancelled"
152 },
153 "owner": {"type": "string", "description": "Owner filter or assignment"},
154 "tenant_id": {"type": "string", "description": "Tenant ID filter or assignment"},
155 "key_results": {
156 "type": "array",
157 "items": {
158 "type": "object",
159 "properties": {
160 "title": {"type": "string"},
161 "target_value": {"type": "number"},
162 "unit": {"type": "string", "default": "%"}
163 },
164 "required": ["title", "target_value"]
165 },
166 "description": "Key results to add (for create_okr)"
167 },
168 "okr_id": {"type": "string", "description": "Parent OKR UUID (for run operations)"},
169 "name": {"type": "string", "description": "Run name (for create_run)"},
170 "correlation_id": {"type": "string", "description": "Correlation ID for run queries"},
171 "session_id": {"type": "string", "description": "Session ID for run queries"},
172 "checkpoint_id": {"type": "string", "description": "Relay checkpoint ID for run queries"}
173 },
174 "required": ["action"]
175 })
176 }
177
178 async fn execute(&self, params: Value) -> Result<ToolResult> {
179 let p: Params = serde_json::from_value(params).context("Invalid OKR tool params")?;
180 let repo = get_repo().await?;
181
182 match p.action.as_str() {
183 "create_okr" => {
185 let title = p.title.ok_or_else(|| anyhow::anyhow!("title required"))?;
186 let desc = p.description.unwrap_or_default();
187 let mut okr = Okr::new(&title, &desc);
188
189 if let Some(owner) = p.owner {
190 okr.owner = Some(owner);
191 }
192 if let Some(tid) = p.tenant_id {
193 okr.tenant_id = Some(tid);
194 }
195
196 let krs = p
197 .key_results
198 .ok_or_else(|| anyhow::anyhow!("key_results required (at least one)"))?;
199 for kr_p in krs {
200 let kr = KeyResult::new(okr.id, &kr_p.title, kr_p.target_value, &kr_p.unit);
201 okr.add_key_result(kr);
202 }
203
204 let created = repo.create_okr(okr).await?;
205 Ok(ToolResult::success(serde_json::to_string_pretty(&created)?)
206 .with_metadata("okr_id", json!(created.id.to_string())))
207 }
208
209 "get_okr" => {
210 let id = parse_uuid(
211 p.id.as_deref()
212 .ok_or_else(|| anyhow::anyhow!("id required"))?,
213 "id",
214 )?;
215 match repo.get_okr(id).await? {
216 Some(okr) => Ok(ToolResult::success(serde_json::to_string_pretty(&okr)?)),
217 None => Ok(ToolResult::error(format!("OKR not found: {id}"))),
218 }
219 }
220
221 "update_okr" => {
222 let id = parse_uuid(
223 p.id.as_deref()
224 .ok_or_else(|| anyhow::anyhow!("id required"))?,
225 "id",
226 )?;
227 let mut okr = repo
228 .get_okr(id)
229 .await?
230 .ok_or_else(|| anyhow::anyhow!("OKR not found: {id}"))?;
231
232 if let Some(title) = p.title {
233 okr.title = title;
234 }
235 if let Some(desc) = p.description {
236 okr.description = desc;
237 }
238 if let Some(status_str) = p.status {
239 okr.status = parse_okr_status(&status_str)?;
240 }
241 if let Some(owner) = p.owner {
242 okr.owner = Some(owner);
243 }
244 if let Some(tid) = p.tenant_id {
245 okr.tenant_id = Some(tid);
246 }
247
248 let updated = repo.update_okr(okr).await?;
249 Ok(ToolResult::success(serde_json::to_string_pretty(&updated)?))
250 }
251
252 "delete_okr" => {
253 let id = parse_uuid(
254 p.id.as_deref()
255 .ok_or_else(|| anyhow::anyhow!("id required"))?,
256 "id",
257 )?;
258 let deleted = repo.delete_okr(id).await?;
259 if deleted {
260 Ok(ToolResult::success(format!("Deleted OKR: {id}")))
261 } else {
262 Ok(ToolResult::error(format!("OKR not found: {id}")))
263 }
264 }
265
266 "list_okrs" => {
267 let okrs = repo.list_okrs().await?;
268 if okrs.is_empty() {
269 return Ok(ToolResult::success("No OKRs found"));
270 }
271 Ok(ToolResult::success(serde_json::to_string_pretty(&okrs)?)
272 .with_metadata("count", json!(okrs.len())))
273 }
274
275 "query_okrs" => {
276 let okrs = if let Some(status_str) = p.status {
277 let status = parse_okr_status(&status_str)?;
278 repo.query_okrs_by_status(status).await?
279 } else if let Some(owner) = p.owner {
280 repo.query_okrs_by_owner(&owner).await?
281 } else if let Some(tid) = p.tenant_id {
282 repo.query_okrs_by_tenant(&tid).await?
283 } else {
284 anyhow::bail!("query_okrs requires status, owner, or tenant_id");
285 };
286 Ok(ToolResult::success(serde_json::to_string_pretty(&okrs)?)
287 .with_metadata("count", json!(okrs.len())))
288 }
289
290 "create_run" => {
292 let okr_id = parse_uuid(
293 p.okr_id
294 .as_deref()
295 .ok_or_else(|| anyhow::anyhow!("okr_id required"))?,
296 "okr_id",
297 )?;
298 let name = p.name.ok_or_else(|| anyhow::anyhow!("name required"))?;
299 let mut run = OkrRun::new(okr_id, &name);
300
301 if let Some(cid) = p.correlation_id {
302 run.correlation_id = Some(cid);
303 }
304 if let Some(sid) = p.session_id {
305 run.session_id = Some(sid);
306 }
307
308 let created = repo.create_run(run).await?;
309 Ok(ToolResult::success(serde_json::to_string_pretty(&created)?)
310 .with_metadata("run_id", json!(created.id.to_string())))
311 }
312
313 "get_run" => {
314 let id = parse_uuid(
315 p.id.as_deref()
316 .ok_or_else(|| anyhow::anyhow!("id required"))?,
317 "id",
318 )?;
319 match repo.get_run(id).await? {
320 Some(run) => Ok(ToolResult::success(serde_json::to_string_pretty(&run)?)),
321 None => Ok(ToolResult::error(format!("Run not found: {id}"))),
322 }
323 }
324
325 "update_run" => {
326 let id = parse_uuid(
327 p.id.as_deref()
328 .ok_or_else(|| anyhow::anyhow!("id required"))?,
329 "id",
330 )?;
331 let mut run = repo
332 .get_run(id)
333 .await?
334 .ok_or_else(|| anyhow::anyhow!("Run not found: {id}"))?;
335
336 if let Some(name) = p.name {
337 run.name = name;
338 }
339 if let Some(status_str) = p.status {
340 run.status = parse_run_status(&status_str)?;
341 }
342 if let Some(cid) = p.correlation_id {
343 run.correlation_id = Some(cid);
344 }
345 if let Some(sid) = p.session_id {
346 run.session_id = Some(sid);
347 }
348
349 let updated = repo.update_run(run).await?;
350 Ok(ToolResult::success(serde_json::to_string_pretty(&updated)?))
351 }
352
353 "delete_run" => {
354 let id = parse_uuid(
355 p.id.as_deref()
356 .ok_or_else(|| anyhow::anyhow!("id required"))?,
357 "id",
358 )?;
359 let deleted = repo.delete_run(id).await?;
360 if deleted {
361 Ok(ToolResult::success(format!("Deleted run: {id}")))
362 } else {
363 Ok(ToolResult::error(format!("Run not found: {id}")))
364 }
365 }
366
367 "list_runs" => {
368 let runs = repo.list_runs().await?;
369 if runs.is_empty() {
370 return Ok(ToolResult::success("No runs found"));
371 }
372 Ok(ToolResult::success(serde_json::to_string_pretty(&runs)?)
373 .with_metadata("count", json!(runs.len())))
374 }
375
376 "query_runs" => {
377 let runs = if let Some(okr_id_str) = p.okr_id {
378 let okr_id = parse_uuid(&okr_id_str, "okr_id")?;
379 repo.query_runs_by_okr(okr_id).await?
380 } else if let Some(status_str) = p.status {
381 let status = parse_run_status(&status_str)?;
382 repo.query_runs_by_status(status).await?
383 } else if let Some(cid) = p.correlation_id {
384 repo.query_runs_by_correlation(&cid).await?
385 } else if let Some(cpid) = p.checkpoint_id {
386 repo.query_runs_by_checkpoint(&cpid).await?
387 } else if let Some(sid) = p.session_id {
388 repo.query_runs_by_session(&sid).await?
389 } else {
390 anyhow::bail!(
391 "query_runs requires okr_id, status, correlation_id, checkpoint_id, or session_id"
392 );
393 };
394 Ok(ToolResult::success(serde_json::to_string_pretty(&runs)?)
395 .with_metadata("count", json!(runs.len())))
396 }
397
398 "stats" => {
400 let stats = repo.stats().await?;
401 Ok(ToolResult::success(serde_json::to_string_pretty(&stats)?))
402 }
403
404 _ => Ok(ToolResult::error(format!(
405 "Unknown action: {}. Use: create_okr, get_okr, update_okr, delete_okr, \
406 list_okrs, query_okrs, create_run, get_run, update_run, delete_run, \
407 list_runs, query_runs, stats",
408 p.action
409 ))),
410 }
411 }
412}