1use 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#[derive(Debug, Clone)]
25pub struct TodoCreateTool {
26 db_path: PathBuf,
27 session_id: Uuid,
28}
29
30impl TodoCreateTool {
31 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
102pub struct TodoCreateBatchInput {
103 #[serde(default)]
105 pub items: Vec<TodoCreateInput>,
106}
107
108#[derive(Debug, Clone)]
110pub struct TodoCreateBatchTool {
111 db_path: PathBuf,
112 session_id: Uuid,
113}
114
115impl TodoCreateBatchTool {
116 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
199pub struct TodoUpdateBatchInput {
200 #[serde(default)]
202 pub items: Vec<TodoUpdateInput>,
203}
204
205#[derive(Debug, Clone)]
207pub struct TodoUpdateBatchTool {
208 db_path: PathBuf,
209 session_id: Uuid,
210}
211
212impl TodoUpdateBatchTool {
213 #[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 #[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#[derive(Debug, Clone)]
304pub struct TodoUpdateStatusTool {
305 db_path: PathBuf,
306 session_id: Uuid,
307}
308
309impl TodoUpdateStatusTool {
310 #[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 #[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#[derive(Debug, Clone)]
380pub struct TodoUpdateTool {
381 db_path: PathBuf,
382 session_id: Uuid,
383}
384
385impl TodoUpdateTool {
386 #[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 #[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#[derive(Debug, Clone)]
469pub struct TodoDeleteTool {
470 db_path: PathBuf,
471 session_id: Uuid,
472}
473
474impl TodoDeleteTool {
475 #[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 #[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#[derive(Debug, Clone)]
548pub struct TodoAddDependencyTool {
549 db_path: PathBuf,
550 session_id: Uuid,
551}
552
553impl TodoAddDependencyTool {
554 #[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 #[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#[derive(Debug, Clone)]
625pub struct TodoRemoveDependencyTool {
626 db_path: PathBuf,
627 session_id: Uuid,
628}
629
630impl TodoRemoveDependencyTool {
631 #[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 #[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#[derive(Debug, Clone)]
706pub struct TodoQueryTool {
707 db_path: PathBuf,
708 session_id: Uuid,
709}
710
711impl TodoQueryTool {
712 #[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 #[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}