Skip to main content

codetether_agent/tool/
okr.rs

1//! OKR Tool - Manage Objectives and Key Results for goal tracking.
2//!
3//! Exposes full CRUD operations on OKRs and OKR Runs so that LLMs can
4//! create, query, update, and delete objectives and their execution runs.
5
6use 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
17/// Lazily-initialized shared OKR repository
18static 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    // Run-specific fields
61    #[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            // ===== OKR CRUD =====
184            "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            // ===== Run CRUD =====
291            "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 =====
399            "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}