Skip to main content

git_iris/agents/tools/
workspace.rs

1//! Iris workspace tool for Rig
2//!
3//! This tool provides Iris with her own personal workspace for taking notes,
4//! creating task lists, and managing her workflow during complex operations.
5
6use anyhow::Result;
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::sync::{Arc, Mutex};
12
13use super::common::parameters_schema;
14
15// Use standard tool error macro for consistency
16crate::define_tool_error!(WorkspaceError);
17
18/// Workspace tool for Iris's note-taking and task management
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Workspace {
21    #[serde(skip)]
22    data: Arc<Mutex<WorkspaceData>>,
23}
24
25#[derive(Debug, Default)]
26struct WorkspaceData {
27    notes: Vec<String>,
28    tasks: Vec<WorkspaceTask>,
29}
30
31#[derive(Debug, Clone)]
32struct WorkspaceTask {
33    description: String,
34    status: TaskStatus,
35    priority: TaskPriority,
36}
37
38/// Workspace action type
39#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
40#[serde(rename_all = "snake_case")]
41pub enum WorkspaceAction {
42    /// Add a note to the workspace
43    AddNote,
44    /// Add a task to track
45    AddTask,
46    /// Update an existing task's status
47    UpdateTask,
48    /// Get summary of notes and tasks
49    #[default]
50    GetSummary,
51}
52
53/// Task priority level
54#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
55#[serde(rename_all = "lowercase")]
56pub enum TaskPriority {
57    Low,
58    #[default]
59    Medium,
60    High,
61    Critical,
62}
63
64/// Task status
65#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum TaskStatus {
68    #[default]
69    Pending,
70    InProgress,
71    Completed,
72    Blocked,
73}
74
75impl std::fmt::Display for TaskStatus {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            TaskStatus::Pending => write!(f, "pending"),
79            TaskStatus::InProgress => write!(f, "in_progress"),
80            TaskStatus::Completed => write!(f, "completed"),
81            TaskStatus::Blocked => write!(f, "blocked"),
82        }
83    }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
87pub struct WorkspaceArgs {
88    /// Action to perform
89    pub action: WorkspaceAction,
90    /// Content for note or task description
91    #[serde(default)]
92    pub content: Option<String>,
93    /// Priority level for tasks
94    #[serde(default)]
95    pub priority: Option<TaskPriority>,
96    /// Index of task to update (0-based)
97    #[serde(default)]
98    pub task_index: Option<usize>,
99    /// New status for task updates
100    #[serde(default)]
101    pub status: Option<TaskStatus>,
102}
103
104impl Default for Workspace {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl Workspace {
111    #[must_use]
112    pub fn new() -> Self {
113        Self {
114            data: Arc::new(Mutex::new(WorkspaceData::default())),
115        }
116    }
117}
118
119impl Tool for Workspace {
120    const NAME: &'static str = "workspace";
121    type Error = WorkspaceError;
122    type Args = WorkspaceArgs;
123    type Output = String;
124
125    async fn definition(&self, _: String) -> ToolDefinition {
126        ToolDefinition {
127            name: "workspace".to_string(),
128            description: "Iris's personal workspace for notes and task management. Use this to track progress, take notes on findings, and manage complex workflows.".to_string(),
129            parameters: parameters_schema::<WorkspaceArgs>(),
130        }
131    }
132
133    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
134        let mut data = self
135            .data
136            .lock()
137            .map_err(|e| WorkspaceError(e.to_string()))?;
138
139        let result = match args.action {
140            WorkspaceAction::AddNote => {
141                let content = args
142                    .content
143                    .ok_or_else(|| WorkspaceError("Content required for add_note".to_string()))?;
144                data.notes.push(content);
145                json!({
146                    "success": true,
147                    "message": "Note added successfully",
148                    "note_count": data.notes.len()
149                })
150            }
151            WorkspaceAction::AddTask => {
152                let content = args
153                    .content
154                    .ok_or_else(|| WorkspaceError("Content required for add_task".to_string()))?;
155                let priority = args.priority.unwrap_or_default();
156
157                data.tasks.push(WorkspaceTask {
158                    description: content,
159                    status: TaskStatus::Pending,
160                    priority,
161                });
162
163                json!({
164                    "success": true,
165                    "message": "Task added successfully",
166                    "task_count": data.tasks.len()
167                })
168            }
169            WorkspaceAction::UpdateTask => {
170                let task_index = args.task_index.ok_or_else(|| {
171                    WorkspaceError("task_index required for update_task".to_string())
172                })?;
173                let status = args
174                    .status
175                    .ok_or_else(|| WorkspaceError("status required for update_task".to_string()))?;
176
177                if task_index >= data.tasks.len() {
178                    return Err(WorkspaceError(format!(
179                        "Task index {task_index} out of range"
180                    )));
181                }
182
183                data.tasks[task_index].status = status.clone();
184
185                json!({
186                    "success": true,
187                    "message": format!("Task {} updated to {}", task_index, status),
188                })
189            }
190            WorkspaceAction::GetSummary => {
191                let pending = data
192                    .tasks
193                    .iter()
194                    .filter(|t| matches!(t.status, TaskStatus::Pending))
195                    .count();
196                let in_progress = data
197                    .tasks
198                    .iter()
199                    .filter(|t| matches!(t.status, TaskStatus::InProgress))
200                    .count();
201                let completed = data
202                    .tasks
203                    .iter()
204                    .filter(|t| matches!(t.status, TaskStatus::Completed))
205                    .count();
206
207                json!({
208                    "notes_count": data.notes.len(),
209                    "tasks": {
210                        "total": data.tasks.len(),
211                        "pending": pending,
212                        "in_progress": in_progress,
213                        "completed": completed
214                    },
215                    "recent_notes": data.notes.iter().rev().take(3).collect::<Vec<_>>(),
216                    "active_tasks": data.tasks.iter()
217                        .filter(|t| !matches!(t.status, TaskStatus::Completed))
218                        .map(|t| json!({
219                            "description": t.description,
220                            "status": t.status.to_string(),
221                            "priority": format!("{:?}", t.priority).to_lowercase()
222                        }))
223                        .collect::<Vec<_>>()
224                })
225            }
226        };
227
228        serde_json::to_string_pretty(&result).map_err(|e| WorkspaceError(e.to_string()))
229    }
230}