Skip to main content

talos_session/todo/
tools.rs

1//! AgentTool adapters for session-scoped Todo operations.
2
3use async_trait::async_trait;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::path::{Path, PathBuf};
8use talos_core::tool::{
9    AgentTool, ToolFamily, ToolNature, ToolPermissionFacet, ToolResourceKind, ToolResult,
10};
11use talos_core::tool_parameters;
12use uuid::Uuid;
13
14use super::formatting::{
15    format_created, format_mutation_result, format_query_result, format_updated,
16};
17use super::model::{
18    CreateTodo, TodoCreateInput, TodoDeleteInput, TodoDependencyInput, TodoError, TodoQuery,
19    TodoQueryInput, TodoUpdate, TodoUpdateInput, TodoUpdateStatusInput,
20};
21use super::repository::TodoRepository;
22
23/// Agent tool that creates a session todo item.
24#[derive(Debug, Clone)]
25pub struct TodoCreateTool {
26    db_path: PathBuf,
27    session_id: Uuid,
28}
29
30impl TodoCreateTool {
31    /// Create a todo creation tool bound to one session's SQLite database path.
32    #[must_use]
33    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
34        Self {
35            db_path,
36            session_id,
37        }
38    }
39
40    /// Create a todo creation tool bound to one session, using the standard
41    /// database path under a sessions directory.
42    #[must_use]
43    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
44        Self::new(sessions_dir.join("todos.sqlite"), session_id)
45    }
46}
47
48#[async_trait]
49impl AgentTool for TodoCreateTool {
50    fn name(&self) -> &str {
51        "todo_create"
52    }
53
54    fn description(&self) -> &str {
55        "Create a session-scoped todo item for agent planning"
56    }
57
58    fn parameters(&self) -> Value {
59        tool_parameters!(TodoCreateInput)
60    }
61
62    async fn execute(&self, input: Value) -> ToolResult {
63        let input: TodoCreateInput = match serde_json::from_value(input) {
64            Ok(input) => input,
65            Err(err) => return ToolResult::error(format!("Invalid todo_create input: {err}")),
66        };
67        let repo = match open_tool_repo(&self.db_path) {
68            Ok(repo) => repo,
69            Err(err) => return ToolResult::error(err.to_string()),
70        };
71        match repo.create(CreateTodo {
72            session_id: self.session_id,
73            title: input.title,
74            description: input.description,
75            priority: input.priority,
76            assigned_to_turn: input.assigned_to_turn,
77            tags: input.tags,
78        }) {
79            Ok(item) => {
80                let all = repo.list_all(self.session_id).unwrap_or_default();
81                ToolResult::success(format_mutation_result(&format_created(&item), &all))
82            }
83            Err(err) => ToolResult::error(err.to_string()),
84        }
85    }
86
87    fn family(&self) -> ToolFamily {
88        ToolFamily::Extension
89    }
90
91    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
92        vec![todo_permission_facet(self.session_id)]
93    }
94
95    fn summary_fields(&self) -> &'static [&'static str] {
96        &["title", "priority"]
97    }
98}
99
100/// Input for the `todo_create_batch` tool.
101#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
102pub struct TodoCreateBatchInput {
103    /// Items to create (idempotent per title within the session).
104    #[serde(default)]
105    pub items: Vec<TodoCreateInput>,
106}
107
108/// Agent tool that creates multiple session todo items in one call.
109#[derive(Debug, Clone)]
110pub struct TodoCreateBatchTool {
111    db_path: PathBuf,
112    session_id: Uuid,
113}
114
115impl TodoCreateBatchTool {
116    /// Create a batch todo creation tool bound to one session's SQLite database path.
117    #[must_use]
118    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
119        Self {
120            db_path,
121            session_id,
122        }
123    }
124
125    /// Create a batch todo creation tool bound to one session, using the standard
126    /// database path under a sessions directory.
127    #[must_use]
128    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
129        Self::new(sessions_dir.join("todos.sqlite"), session_id)
130    }
131}
132
133#[async_trait]
134impl AgentTool for TodoCreateBatchTool {
135    fn name(&self) -> &str {
136        "todo_create_batch"
137    }
138
139    fn description(&self) -> &str {
140        "Create multiple session-scoped todo items in one call (idempotent per title)"
141    }
142
143    fn parameters(&self) -> Value {
144        tool_parameters!(TodoCreateBatchInput)
145    }
146
147    async fn execute(&self, input: Value) -> ToolResult {
148        let input: TodoCreateBatchInput = match serde_json::from_value(input) {
149            Ok(input) => input,
150            Err(err) => {
151                return ToolResult::error(format!("Invalid todo_create_batch input: {err}"));
152            }
153        };
154        if input.items.is_empty() {
155            return ToolResult::error("todo_create_batch requires at least one item");
156        }
157        let repo = match open_tool_repo(&self.db_path) {
158            Ok(repo) => repo,
159            Err(err) => return ToolResult::error(err.to_string()),
160        };
161        let create_inputs: Vec<CreateTodo> = input
162            .items
163            .into_iter()
164            .map(|item| CreateTodo {
165                session_id: self.session_id,
166                title: item.title,
167                description: item.description,
168                priority: item.priority,
169                assigned_to_turn: item.assigned_to_turn,
170                tags: item.tags,
171            })
172            .collect();
173        match repo.create_batch(create_inputs) {
174            Ok(items) => {
175                let created_count = items.len();
176                let action = format!("Created {created_count} todo(s)");
177                let all = repo.list_all(self.session_id).unwrap_or_default();
178                ToolResult::success(format_mutation_result(&action, &all))
179            }
180            Err(err) => ToolResult::error(err.to_string()),
181        }
182    }
183
184    fn family(&self) -> ToolFamily {
185        ToolFamily::Extension
186    }
187
188    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
189        vec![todo_permission_facet(self.session_id)]
190    }
191
192    fn summary_fields(&self) -> &'static [&'static str] {
193        &["items"]
194    }
195}
196
197/// Input for the `todo_update_batch` tool.
198#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
199pub struct TodoUpdateBatchInput {
200    /// Items to update (each must include `id`; remaining fields are optional).
201    #[serde(default)]
202    pub items: Vec<TodoUpdateInput>,
203}
204
205/// Agent tool that updates multiple session todo items in one call.
206#[derive(Debug, Clone)]
207pub struct TodoUpdateBatchTool {
208    db_path: PathBuf,
209    session_id: Uuid,
210}
211
212impl TodoUpdateBatchTool {
213    /// Create a batch todo update tool bound to one session's SQLite database path.
214    #[must_use]
215    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
216        Self {
217            db_path,
218            session_id,
219        }
220    }
221
222    /// Create a batch todo update tool bound to one session, using the standard
223    /// database path under a sessions directory.
224    #[must_use]
225    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
226        Self::new(sessions_dir.join("todos.sqlite"), session_id)
227    }
228}
229
230#[async_trait]
231impl AgentTool for TodoUpdateBatchTool {
232    fn name(&self) -> &str {
233        "todo_update_batch"
234    }
235
236    fn description(&self) -> &str {
237        "Update mutable fields on multiple session-scoped todo items in one call"
238    }
239
240    fn parameters(&self) -> Value {
241        tool_parameters!(TodoUpdateBatchInput)
242    }
243
244    async fn execute(&self, input: Value) -> ToolResult {
245        let input: TodoUpdateBatchInput = match serde_json::from_value(input) {
246            Ok(input) => input,
247            Err(err) => {
248                return ToolResult::error(format!("Invalid todo_update_batch input: {err}"));
249            }
250        };
251        if input.items.is_empty() {
252            return ToolResult::error("todo_update_batch requires at least one item");
253        }
254        let repo = match open_tool_repo(&self.db_path) {
255            Ok(repo) => repo,
256            Err(err) => return ToolResult::error(err.to_string()),
257        };
258        let mut updated_count = 0usize;
259        for item in input.items {
260            let id = match parse_tool_uuid("id", &item.id) {
261                Ok(id) => id,
262                Err(err) => return ToolResult::error(err),
263            };
264            let update = TodoUpdate {
265                title: item.title,
266                description: if item.clear_description {
267                    Some(None)
268                } else {
269                    item.description.map(Some)
270                },
271                priority: item.priority,
272                assigned_to_turn: if item.clear_assigned_to_turn {
273                    Some(None)
274                } else {
275                    item.assigned_to_turn.map(Some)
276                },
277                tags: item.tags,
278            };
279            match repo.update(self.session_id, id, update) {
280                Ok(_) => updated_count += 1,
281                Err(err) => return ToolResult::error(err.to_string()),
282            }
283        }
284        let action = format!("Updated {updated_count} todo(s)");
285        let all = repo.list_all(self.session_id).unwrap_or_default();
286        ToolResult::success(format_mutation_result(&action, &all))
287    }
288
289    fn family(&self) -> ToolFamily {
290        ToolFamily::Extension
291    }
292
293    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
294        vec![todo_permission_facet(self.session_id)]
295    }
296
297    fn summary_fields(&self) -> &'static [&'static str] {
298        &["items"]
299    }
300}
301
302/// Agent tool that updates a session todo status.
303#[derive(Debug, Clone)]
304pub struct TodoUpdateStatusTool {
305    db_path: PathBuf,
306    session_id: Uuid,
307}
308
309impl TodoUpdateStatusTool {
310    /// Create a todo status update tool bound to one session's SQLite database path.
311    #[must_use]
312    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
313        Self {
314            db_path,
315            session_id,
316        }
317    }
318
319    /// Create a todo status update tool bound to one session, using the
320    /// standard database path under a sessions directory.
321    #[must_use]
322    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
323        Self::new(sessions_dir.join("todos.sqlite"), session_id)
324    }
325}
326
327#[async_trait]
328impl AgentTool for TodoUpdateStatusTool {
329    fn name(&self) -> &str {
330        "todo_update_status"
331    }
332
333    fn description(&self) -> &str {
334        "Update the status of a session-scoped todo item"
335    }
336
337    fn parameters(&self) -> Value {
338        tool_parameters!(TodoUpdateStatusInput)
339    }
340
341    async fn execute(&self, input: Value) -> ToolResult {
342        let input: TodoUpdateStatusInput = match serde_json::from_value(input) {
343            Ok(input) => input,
344            Err(err) => {
345                return ToolResult::error(format!("Invalid todo_update_status input: {err}"));
346            }
347        };
348        let repo = match open_tool_repo(&self.db_path) {
349            Ok(repo) => repo,
350            Err(err) => return ToolResult::error(err.to_string()),
351        };
352        let id = match parse_tool_uuid("id", &input.id) {
353            Ok(id) => id,
354            Err(err) => return ToolResult::error(err),
355        };
356        match repo.update_status(self.session_id, id, input.status) {
357            Ok(item) => {
358                let all = repo.list_all(self.session_id).unwrap_or_default();
359                ToolResult::success(format_mutation_result(&format_updated(&item), &all))
360            }
361            Err(err) => ToolResult::error(err.to_string()),
362        }
363    }
364
365    fn family(&self) -> ToolFamily {
366        ToolFamily::Extension
367    }
368
369    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
370        vec![todo_permission_facet(self.session_id)]
371    }
372
373    fn summary_fields(&self) -> &'static [&'static str] {
374        &["id", "status"]
375    }
376}
377
378/// Agent tool that updates mutable fields on a session todo item.
379#[derive(Debug, Clone)]
380pub struct TodoUpdateTool {
381    db_path: PathBuf,
382    session_id: Uuid,
383}
384
385impl TodoUpdateTool {
386    /// Create a todo update tool bound to one session's SQLite database path.
387    #[must_use]
388    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
389        Self {
390            db_path,
391            session_id,
392        }
393    }
394
395    /// Create a todo update tool bound to one session, using the standard
396    /// database path under a sessions directory.
397    #[must_use]
398    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
399        Self::new(sessions_dir.join("todos.sqlite"), session_id)
400    }
401}
402
403#[async_trait]
404impl AgentTool for TodoUpdateTool {
405    fn name(&self) -> &str {
406        "todo_update"
407    }
408
409    fn description(&self) -> &str {
410        "Update mutable fields on a session-scoped todo item"
411    }
412
413    fn parameters(&self) -> Value {
414        tool_parameters!(TodoUpdateInput)
415    }
416
417    async fn execute(&self, input: Value) -> ToolResult {
418        let input: TodoUpdateInput = match serde_json::from_value(input) {
419            Ok(input) => input,
420            Err(err) => return ToolResult::error(format!("Invalid todo_update input: {err}")),
421        };
422        let repo = match open_tool_repo(&self.db_path) {
423            Ok(repo) => repo,
424            Err(err) => return ToolResult::error(err.to_string()),
425        };
426        let id = match parse_tool_uuid("id", &input.id) {
427            Ok(id) => id,
428            Err(err) => return ToolResult::error(err),
429        };
430        let update = TodoUpdate {
431            title: input.title,
432            description: if input.clear_description {
433                Some(None)
434            } else {
435                input.description.map(Some)
436            },
437            priority: input.priority,
438            assigned_to_turn: if input.clear_assigned_to_turn {
439                Some(None)
440            } else {
441                input.assigned_to_turn.map(Some)
442            },
443            tags: input.tags,
444        };
445        match repo.update(self.session_id, id, update) {
446            Ok(item) => {
447                let all = repo.list_all(self.session_id).unwrap_or_default();
448                ToolResult::success(format_mutation_result(&format_updated(&item), &all))
449            }
450            Err(err) => ToolResult::error(err.to_string()),
451        }
452    }
453
454    fn family(&self) -> ToolFamily {
455        ToolFamily::Extension
456    }
457
458    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
459        vec![todo_permission_facet(self.session_id)]
460    }
461
462    fn summary_fields(&self) -> &'static [&'static str] {
463        &["id", "title", "priority"]
464    }
465}
466
467/// Agent tool that deletes a session todo item.
468#[derive(Debug, Clone)]
469pub struct TodoDeleteTool {
470    db_path: PathBuf,
471    session_id: Uuid,
472}
473
474impl TodoDeleteTool {
475    /// Create a todo delete tool bound to one session's SQLite database path.
476    #[must_use]
477    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
478        Self {
479            db_path,
480            session_id,
481        }
482    }
483
484    /// Create a todo delete tool bound to one session, using the standard
485    /// database path under a sessions directory.
486    #[must_use]
487    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
488        Self::new(sessions_dir.join("todos.sqlite"), session_id)
489    }
490}
491
492#[async_trait]
493impl AgentTool for TodoDeleteTool {
494    fn name(&self) -> &str {
495        "todo_delete"
496    }
497
498    fn description(&self) -> &str {
499        "Delete a session-scoped todo item and its dependency edges"
500    }
501
502    fn parameters(&self) -> Value {
503        tool_parameters!(TodoDeleteInput)
504    }
505
506    async fn execute(&self, input: Value) -> ToolResult {
507        let input: TodoDeleteInput = match serde_json::from_value(input) {
508            Ok(input) => input,
509            Err(err) => return ToolResult::error(format!("Invalid todo_delete input: {err}")),
510        };
511        let mut repo = match open_tool_repo(&self.db_path) {
512            Ok(repo) => repo,
513            Err(err) => return ToolResult::error(err.to_string()),
514        };
515        let id = match parse_tool_uuid("id", &input.id) {
516            Ok(id) => id,
517            Err(err) => return ToolResult::error(err),
518        };
519        match repo.delete(self.session_id, id) {
520            Ok(deleted) => {
521                let action = if deleted {
522                    format!("Deleted todo item {id}")
523                } else {
524                    "Todo item not found (already deleted?)".to_string()
525                };
526                let all = repo.list_all(self.session_id).unwrap_or_default();
527                ToolResult::success(format_mutation_result(&action, &all))
528            }
529            Err(err) => ToolResult::error(err.to_string()),
530        }
531    }
532
533    fn family(&self) -> ToolFamily {
534        ToolFamily::Extension
535    }
536
537    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
538        vec![todo_permission_facet(self.session_id)]
539    }
540
541    fn summary_fields(&self) -> &'static [&'static str] {
542        &["id"]
543    }
544}
545
546/// Agent tool that adds a dependency edge between session todo items.
547#[derive(Debug, Clone)]
548pub struct TodoAddDependencyTool {
549    db_path: PathBuf,
550    session_id: Uuid,
551}
552
553impl TodoAddDependencyTool {
554    /// Create a todo dependency-add tool bound to one session's SQLite database path.
555    #[must_use]
556    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
557        Self {
558            db_path,
559            session_id,
560        }
561    }
562
563    /// Create a todo dependency-add tool bound to one session, using the
564    /// standard database path under a sessions directory.
565    #[must_use]
566    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
567        Self::new(sessions_dir.join("todos.sqlite"), session_id)
568    }
569}
570
571#[async_trait]
572impl AgentTool for TodoAddDependencyTool {
573    fn name(&self) -> &str {
574        "todo_add_dependency"
575    }
576
577    fn description(&self) -> &str {
578        "Add an acyclic dependency edge between two session-scoped todo items"
579    }
580
581    fn parameters(&self) -> Value {
582        tool_parameters!(TodoDependencyInput)
583    }
584
585    async fn execute(&self, input: Value) -> ToolResult {
586        let input: TodoDependencyInput = match serde_json::from_value(input) {
587            Ok(input) => input,
588            Err(err) => {
589                return ToolResult::error(format!("Invalid todo_add_dependency input: {err}"));
590            }
591        };
592        let repo = match open_tool_repo(&self.db_path) {
593            Ok(repo) => repo,
594            Err(err) => return ToolResult::error(err.to_string()),
595        };
596        let ids = match parse_dependency_input(&input) {
597            Ok(ids) => ids,
598            Err(err) => return ToolResult::error(err),
599        };
600        match repo.add_dependency(self.session_id, ids.parent_id, ids.child_id) {
601            Ok(_dep) => {
602                let action = format!("Added dependency: {} → {}", ids.parent_id, ids.child_id);
603                let all = repo.list_all(self.session_id).unwrap_or_default();
604                ToolResult::success(format_mutation_result(&action, &all))
605            }
606            Err(err) => ToolResult::error(err.to_string()),
607        }
608    }
609
610    fn family(&self) -> ToolFamily {
611        ToolFamily::Extension
612    }
613
614    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
615        vec![todo_permission_facet(self.session_id)]
616    }
617
618    fn summary_fields(&self) -> &'static [&'static str] {
619        &["parent_id", "child_id"]
620    }
621}
622
623/// Agent tool that removes a dependency edge between session todo items.
624#[derive(Debug, Clone)]
625pub struct TodoRemoveDependencyTool {
626    db_path: PathBuf,
627    session_id: Uuid,
628}
629
630impl TodoRemoveDependencyTool {
631    /// Create a todo dependency-remove tool bound to one session's SQLite database path.
632    #[must_use]
633    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
634        Self {
635            db_path,
636            session_id,
637        }
638    }
639
640    /// Create a todo dependency-remove tool bound to one session, using the
641    /// standard database path under a sessions directory.
642    #[must_use]
643    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
644        Self::new(sessions_dir.join("todos.sqlite"), session_id)
645    }
646}
647
648#[async_trait]
649impl AgentTool for TodoRemoveDependencyTool {
650    fn name(&self) -> &str {
651        "todo_remove_dependency"
652    }
653
654    fn description(&self) -> &str {
655        "Remove a dependency edge between two session-scoped todo items"
656    }
657
658    fn parameters(&self) -> Value {
659        tool_parameters!(TodoDependencyInput)
660    }
661
662    async fn execute(&self, input: Value) -> ToolResult {
663        let input: TodoDependencyInput = match serde_json::from_value(input) {
664            Ok(input) => input,
665            Err(err) => {
666                return ToolResult::error(format!("Invalid todo_remove_dependency input: {err}"));
667            }
668        };
669        let repo = match open_tool_repo(&self.db_path) {
670            Ok(repo) => repo,
671            Err(err) => return ToolResult::error(err.to_string()),
672        };
673        let ids = match parse_dependency_input(&input) {
674            Ok(ids) => ids,
675            Err(err) => return ToolResult::error(err),
676        };
677        match repo.remove_dependency(self.session_id, ids.parent_id, ids.child_id) {
678            Ok(removed) => {
679                let action = if removed {
680                    format!("Removed dependency: {} → {}", ids.parent_id, ids.child_id)
681                } else {
682                    "Dependency edge not found (already removed?)".to_string()
683                };
684                let all = repo.list_all(self.session_id).unwrap_or_default();
685                ToolResult::success(format_mutation_result(&action, &all))
686            }
687            Err(err) => ToolResult::error(err.to_string()),
688        }
689    }
690
691    fn family(&self) -> ToolFamily {
692        ToolFamily::Extension
693    }
694
695    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
696        vec![todo_permission_facet(self.session_id)]
697    }
698
699    fn summary_fields(&self) -> &'static [&'static str] {
700        &["parent_id", "child_id"]
701    }
702}
703
704/// Agent tool that queries session todo items.
705#[derive(Debug, Clone)]
706pub struct TodoQueryTool {
707    db_path: PathBuf,
708    session_id: Uuid,
709}
710
711impl TodoQueryTool {
712    /// Create a todo query tool bound to one session's SQLite database path.
713    #[must_use]
714    pub fn new(db_path: PathBuf, session_id: Uuid) -> Self {
715        Self {
716            db_path,
717            session_id,
718        }
719    }
720
721    /// Create a todo query tool bound to one session, using the standard
722    /// database path under a sessions directory.
723    #[must_use]
724    pub fn from_sessions_dir(sessions_dir: &Path, session_id: Uuid) -> Self {
725        Self::new(sessions_dir.join("todos.sqlite"), session_id)
726    }
727}
728
729#[async_trait]
730impl AgentTool for TodoQueryTool {
731    fn name(&self) -> &str {
732        "todo_query"
733    }
734
735    fn description(&self) -> &str {
736        "Query session-scoped todo items without modifying them"
737    }
738
739    fn parameters(&self) -> Value {
740        tool_parameters!(TodoQueryInput)
741    }
742
743    async fn execute(&self, input: Value) -> ToolResult {
744        let input: TodoQueryInput = match serde_json::from_value(input) {
745            Ok(input) => input,
746            Err(err) => return ToolResult::error(format!("Invalid todo_query input: {err}")),
747        };
748        let repo = match open_tool_repo(&self.db_path) {
749            Ok(repo) => repo,
750            Err(err) => return ToolResult::error(err.to_string()),
751        };
752        match repo.list(
753            self.session_id,
754            TodoQuery {
755                status: input.status,
756                priority: input.priority,
757                tag: input.tag,
758            },
759        ) {
760            Ok(items) => ToolResult::success(format_query_result(&items)),
761            Err(err) => ToolResult::error(err.to_string()),
762        }
763    }
764
765    fn is_read_only(&self) -> bool {
766        true
767    }
768
769    fn family(&self) -> ToolFamily {
770        ToolFamily::Extension
771    }
772
773    fn permission_profile(&self, _input: &Value) -> Vec<ToolPermissionFacet> {
774        vec![todo_permission_facet(self.session_id)]
775    }
776
777    fn summary_fields(&self) -> &'static [&'static str] {
778        &["status", "priority", "tag"]
779    }
780}
781
782fn open_tool_repo(db_path: &Path) -> Result<TodoRepository, TodoError> {
783    let repo = TodoRepository::new(db_path)?;
784    repo.init_schema()?;
785    Ok(repo)
786}
787
788fn parse_tool_uuid(field: &str, value: &str) -> Result<Uuid, String> {
789    Uuid::parse_str(value).map_err(|err| format!("Invalid {field} UUID: {err}"))
790}
791
792struct ParsedDependencyInput {
793    parent_id: Uuid,
794    child_id: Uuid,
795}
796
797fn parse_dependency_input(input: &TodoDependencyInput) -> Result<ParsedDependencyInput, String> {
798    Ok(ParsedDependencyInput {
799        parent_id: parse_tool_uuid("parent_id", &input.parent_id)?,
800        child_id: parse_tool_uuid("child_id", &input.child_id)?,
801    })
802}
803
804fn todo_permission_facet(session_id: Uuid) -> ToolPermissionFacet {
805    ToolPermissionFacet::with_resource(
806        ToolNature::Internal,
807        format!("session:{session_id}:todos"),
808        ToolResourceKind::Remote,
809    )
810    .with_description("session todo list")
811}