1use std::collections::BTreeSet;
15
16use punch_types::{Capability, Message, ToolCategory, ToolDefinition};
17use tracing::info;
18
19pub fn tools_for_capabilities(capabilities: &[Capability]) -> Vec<ToolDefinition> {
24 let mut tools = Vec::new();
25
26 for cap in capabilities {
27 match cap {
28 Capability::FileRead(_) => {
29 push_unique(&mut tools, file_read());
30 push_unique(&mut tools, file_list());
31 push_unique(&mut tools, file_search());
32 push_unique(&mut tools, file_info());
33 }
34 Capability::FileWrite(_) => {
35 push_unique(&mut tools, file_write());
36 push_unique(&mut tools, patch_apply());
37 }
38 Capability::ShellExec(_) => {
39 push_unique(&mut tools, shell_exec());
40 push_unique(&mut tools, process_list());
41 push_unique(&mut tools, process_kill());
42 push_unique(&mut tools, env_get());
43 push_unique(&mut tools, env_list());
44 }
45 Capability::Network(_) => {
46 push_unique(&mut tools, web_fetch());
47 push_unique(&mut tools, web_search());
48 push_unique(&mut tools, http_request());
49 push_unique(&mut tools, http_post());
50 }
51 Capability::Memory => {
52 push_unique(&mut tools, memory_store());
53 push_unique(&mut tools, memory_recall());
54 }
55 Capability::KnowledgeGraph => {
56 push_unique(&mut tools, knowledge_add_entity());
57 push_unique(&mut tools, knowledge_add_relation());
58 push_unique(&mut tools, knowledge_query());
59 }
60 Capability::AgentSpawn => {
61 push_unique(&mut tools, agent_spawn());
62 }
63 Capability::AgentMessage => {
64 push_unique(&mut tools, agent_message());
65 push_unique(&mut tools, agent_list());
66 }
67 Capability::BrowserControl => {
68 push_unique(&mut tools, browser_navigate());
69 push_unique(&mut tools, browser_screenshot());
70 push_unique(&mut tools, browser_click());
71 push_unique(&mut tools, browser_type());
72 push_unique(&mut tools, browser_content());
73 }
74 Capability::SourceControl => {
75 push_unique(&mut tools, git_status());
76 push_unique(&mut tools, git_diff());
77 push_unique(&mut tools, git_log());
78 push_unique(&mut tools, git_commit());
79 push_unique(&mut tools, git_branch());
80 }
81 Capability::Container => {
82 push_unique(&mut tools, docker_ps());
83 push_unique(&mut tools, docker_run());
84 push_unique(&mut tools, docker_build());
85 push_unique(&mut tools, docker_logs());
86 }
87 Capability::DataManipulation => {
88 push_unique(&mut tools, json_query());
89 push_unique(&mut tools, json_transform());
90 push_unique(&mut tools, yaml_parse());
91 push_unique(&mut tools, regex_match());
92 push_unique(&mut tools, regex_replace());
93 push_unique(&mut tools, text_diff());
94 push_unique(&mut tools, text_count());
95 }
96 Capability::Schedule => {
97 push_unique(&mut tools, schedule_task());
98 push_unique(&mut tools, schedule_list());
99 push_unique(&mut tools, schedule_cancel());
100 }
101 Capability::CodeAnalysis => {
102 push_unique(&mut tools, code_search());
103 push_unique(&mut tools, code_symbols());
104 }
105 Capability::Archive => {
106 push_unique(&mut tools, archive_create());
107 push_unique(&mut tools, archive_extract());
108 push_unique(&mut tools, archive_list());
109 }
110 Capability::Template => {
111 push_unique(&mut tools, template_render());
112 }
113 Capability::Crypto => {
114 push_unique(&mut tools, hash_compute());
115 push_unique(&mut tools, hash_verify());
116 }
117 Capability::A2ADelegate => {
118 push_unique(&mut tools, a2a_delegate());
119 }
120 Capability::PluginInvoke => {
121 push_unique(&mut tools, wasm_invoke());
122 }
123 Capability::ChannelNotify => {
124 push_unique(&mut tools, channel_notify());
125 }
126 Capability::SelfConfig => {
127 push_unique(&mut tools, heartbeat_add());
128 push_unique(&mut tools, heartbeat_list());
129 push_unique(&mut tools, heartbeat_remove());
130 push_unique(&mut tools, creed_view());
131 push_unique(&mut tools, skill_list());
132 push_unique(&mut tools, skill_recommend());
133 }
134 Capability::SystemAutomation => {
135 push_unique(&mut tools, sys_screenshot());
136 }
137 Capability::UiAutomation(_) => {
138 push_unique(&mut tools, ui_screenshot());
139 push_unique(&mut tools, ui_find_elements());
140 push_unique(&mut tools, ui_click());
141 push_unique(&mut tools, ui_type_text());
142 push_unique(&mut tools, ui_list_windows());
143 push_unique(&mut tools, ui_read_attribute());
144 }
145 Capability::AppIntegration(_) => {
146 push_unique(&mut tools, app_ocr());
147 }
148 _ => {}
149 }
150 }
151
152 tools
153}
154
155pub fn all_tools() -> Vec<ToolDefinition> {
157 vec![
158 file_read(),
159 file_write(),
160 file_list(),
161 patch_apply(),
162 shell_exec(),
163 web_fetch(),
164 web_search(),
165 memory_store(),
166 memory_recall(),
167 knowledge_add_entity(),
168 knowledge_add_relation(),
169 knowledge_query(),
170 agent_spawn(),
171 agent_message(),
172 agent_list(),
173 browser_navigate(),
174 browser_screenshot(),
175 browser_click(),
176 browser_type(),
177 browser_content(),
178 git_status(),
180 git_diff(),
181 git_log(),
182 git_commit(),
183 git_branch(),
184 docker_ps(),
186 docker_run(),
187 docker_build(),
188 docker_logs(),
189 http_request(),
191 http_post(),
192 json_query(),
194 json_transform(),
195 yaml_parse(),
196 regex_match(),
197 regex_replace(),
198 process_list(),
200 process_kill(),
201 schedule_task(),
203 schedule_list(),
204 schedule_cancel(),
205 code_search(),
207 code_symbols(),
208 archive_create(),
210 archive_extract(),
211 archive_list(),
212 template_render(),
214 hash_compute(),
216 hash_verify(),
217 env_get(),
219 env_list(),
220 text_diff(),
222 text_count(),
223 file_search(),
225 file_info(),
226 a2a_delegate(),
228 wasm_invoke(),
230 channel_notify(),
232 heartbeat_add(),
234 heartbeat_list(),
235 heartbeat_remove(),
236 creed_view(),
237 skill_list(),
238 skill_recommend(),
239 sys_screenshot(),
241 ui_screenshot(),
242 app_ocr(),
243 ui_find_elements(),
244 ui_click(),
245 ui_type_text(),
246 ui_list_windows(),
247 ui_read_attribute(),
248 ]
249}
250
251fn push_unique(tools: &mut Vec<ToolDefinition>, tool: ToolDefinition) {
252 if !tools.iter().any(|t| t.name == tool.name) {
253 tools.push(tool);
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
267pub enum ToolGroup {
268 SourceControl,
269 Container,
270 DataManipulation,
271 Schedule,
272 BrowserControl,
273 AgentCoordination,
274 Archive,
275 Template,
276 Crypto,
277 UiAutomation,
278 AppIntegration,
279 PluginInvoke,
280 A2ADelegate,
281 KnowledgeGraph,
282 CodeAnalysis,
283 ProcessManagement,
284 HttpAdvanced,
285}
286
287impl ToolGroup {
288 const ALL: &[ToolGroup] = &[
290 ToolGroup::SourceControl,
291 ToolGroup::Container,
292 ToolGroup::DataManipulation,
293 ToolGroup::Schedule,
294 ToolGroup::BrowserControl,
295 ToolGroup::AgentCoordination,
296 ToolGroup::Archive,
297 ToolGroup::Template,
298 ToolGroup::Crypto,
299 ToolGroup::UiAutomation,
300 ToolGroup::AppIntegration,
301 ToolGroup::PluginInvoke,
302 ToolGroup::A2ADelegate,
303 ToolGroup::KnowledgeGraph,
304 ToolGroup::CodeAnalysis,
305 ToolGroup::ProcessManagement,
306 ToolGroup::HttpAdvanced,
307 ];
308
309 fn keywords(&self) -> &'static [&'static str] {
312 match self {
313 ToolGroup::SourceControl => &[
314 "git ",
315 "git commit",
316 "git branch",
317 "git diff",
318 "git merge",
319 "git rebase",
320 "git push",
321 "git pull",
322 "git clone",
323 "git stash",
324 "git checkout",
325 "git log",
326 "commit changes",
327 "commit my",
328 "repo",
329 "repository",
330 ],
331 ToolGroup::Container => &[
332 "docker",
333 "container",
334 "dockerfile",
335 "docker image",
336 "compose",
337 "pod",
338 "kubernetes",
339 "k8s",
340 "helm",
341 ],
342 ToolGroup::DataManipulation => &[
343 "json",
344 "yaml",
345 "toml",
346 "regex",
347 "parse json",
348 "parse yaml",
349 "transform json",
350 "csv",
351 "xml",
352 "jq",
353 ],
354 ToolGroup::Schedule => &[
355 "schedule",
356 "cron",
357 "timer",
358 "periodic",
359 "interval",
360 "recurring",
361 "every hour",
362 "every day",
363 ],
364 ToolGroup::BrowserControl => &[
365 "browser",
366 "webpage",
367 "click button",
368 "click element",
369 "click link",
370 "navigate to",
371 "selenium",
372 "puppeteer",
373 "scrape",
374 "open website",
375 "web page",
376 ],
377 ToolGroup::AgentCoordination => &[
378 "spawn fighter",
379 "spawn agent",
380 "spawn worker",
381 "new fighter",
382 "troop",
383 "multi-agent",
384 "delegate to",
385 "coordinate agents",
386 ],
387 ToolGroup::Archive => &[
388 "archive",
389 "zip file",
390 "tar file",
391 "gzip",
392 "extract archive",
393 "compress file",
394 "unzip",
395 "decompress",
396 ],
397 ToolGroup::Template => &[
398 "template",
399 "render template",
400 "handlebars",
401 "mustache",
402 "jinja",
403 ],
404 ToolGroup::Crypto => &[
405 "sha256",
406 "sha512",
407 "crypto",
408 "md5",
409 "hmac",
410 "checksum",
411 "digest",
412 "encrypt",
413 "decrypt",
414 "hash file",
415 "hash password",
416 ],
417 ToolGroup::UiAutomation => &[
418 "ui automation",
419 "accessibility",
420 "ui element",
421 "click button",
422 "menu bar",
423 "window list",
424 "applescript",
425 "osascript",
426 ],
427 ToolGroup::AppIntegration => &[
428 "ocr",
429 "text recognition",
430 "screen read",
431 "extract text from image",
432 ],
433 ToolGroup::PluginInvoke => &["plugin", "wasm", "extension", "webassembly"],
434 ToolGroup::A2ADelegate => &["a2a", "remote agent", "delegate task", "external agent"],
435 ToolGroup::KnowledgeGraph => &[
436 "knowledge graph",
437 "add entity",
438 "add relation",
439 "knowledge base",
440 "ontology",
441 "triple store",
442 ],
443 ToolGroup::CodeAnalysis => &[
444 "code symbols",
445 "code search",
446 "find definition",
447 "find references",
448 "ast",
449 "code analysis",
450 "go to definition",
451 ],
452 ToolGroup::ProcessManagement => &[
453 "list processes",
454 "kill process",
455 "pid",
456 "send signal",
457 "background process",
458 "process list",
459 ],
460 ToolGroup::HttpAdvanced => &[
461 "http post",
462 "api call",
463 "rest api",
464 "api endpoint",
465 "webhook",
466 "curl",
467 "http request",
468 "http put",
469 "http delete",
470 ],
471 }
472 }
473
474 fn is_permitted(&self, capabilities: &[Capability]) -> bool {
476 capabilities.iter().any(|c| match self {
477 ToolGroup::SourceControl => matches!(c, Capability::SourceControl),
478 ToolGroup::Container => matches!(c, Capability::Container),
479 ToolGroup::DataManipulation => matches!(c, Capability::DataManipulation),
480 ToolGroup::Schedule => matches!(c, Capability::Schedule),
481 ToolGroup::BrowserControl => matches!(c, Capability::BrowserControl),
482 ToolGroup::AgentCoordination => {
483 matches!(c, Capability::AgentSpawn | Capability::AgentMessage)
484 }
485 ToolGroup::Archive => matches!(c, Capability::Archive),
486 ToolGroup::Template => matches!(c, Capability::Template),
487 ToolGroup::Crypto => matches!(c, Capability::Crypto),
488 ToolGroup::UiAutomation => matches!(c, Capability::UiAutomation(_)),
489 ToolGroup::AppIntegration => matches!(c, Capability::AppIntegration(_)),
490 ToolGroup::PluginInvoke => matches!(c, Capability::PluginInvoke),
491 ToolGroup::A2ADelegate => matches!(c, Capability::A2ADelegate),
492 ToolGroup::KnowledgeGraph => matches!(c, Capability::KnowledgeGraph),
493 ToolGroup::CodeAnalysis => matches!(c, Capability::CodeAnalysis),
494 ToolGroup::ProcessManagement => matches!(c, Capability::ShellExec(_)),
495 ToolGroup::HttpAdvanced => matches!(c, Capability::Network(_)),
496 })
497 }
498
499 fn tools(&self) -> Vec<ToolDefinition> {
501 match self {
502 ToolGroup::SourceControl => {
503 vec![
504 git_status(),
505 git_diff(),
506 git_log(),
507 git_commit(),
508 git_branch(),
509 ]
510 }
511 ToolGroup::Container => {
512 vec![docker_ps(), docker_run(), docker_build(), docker_logs()]
513 }
514 ToolGroup::DataManipulation => vec![
515 json_query(),
516 json_transform(),
517 yaml_parse(),
518 regex_match(),
519 regex_replace(),
520 text_diff(),
521 text_count(),
522 ],
523 ToolGroup::Schedule => {
524 vec![schedule_task(), schedule_list(), schedule_cancel()]
525 }
526 ToolGroup::BrowserControl => vec![
527 browser_navigate(),
528 browser_screenshot(),
529 browser_click(),
530 browser_type(),
531 browser_content(),
532 ],
533 ToolGroup::AgentCoordination => {
534 vec![agent_spawn(), agent_message(), agent_list()]
535 }
536 ToolGroup::Archive => {
537 vec![archive_create(), archive_extract(), archive_list()]
538 }
539 ToolGroup::Template => vec![template_render()],
540 ToolGroup::Crypto => vec![hash_compute(), hash_verify()],
541 ToolGroup::UiAutomation => vec![
542 ui_screenshot(),
543 ui_find_elements(),
544 ui_click(),
545 ui_type_text(),
546 ui_list_windows(),
547 ui_read_attribute(),
548 ],
549 ToolGroup::AppIntegration => vec![app_ocr()],
550 ToolGroup::PluginInvoke => vec![wasm_invoke()],
551 ToolGroup::A2ADelegate => vec![a2a_delegate()],
552 ToolGroup::KnowledgeGraph => vec![
553 knowledge_add_entity(),
554 knowledge_add_relation(),
555 knowledge_query(),
556 ],
557 ToolGroup::CodeAnalysis => vec![code_search(), code_symbols()],
558 ToolGroup::ProcessManagement => vec![process_list(), process_kill()],
559 ToolGroup::HttpAdvanced => vec![http_request(), http_post()],
560 }
561 }
562}
563
564pub struct ToolSelector {
575 capabilities: Vec<Capability>,
577 active_groups: BTreeSet<ToolGroup>,
579 last_tool_hash: u64,
581}
582
583impl ToolSelector {
584 pub fn new(capabilities: &[Capability]) -> Self {
591 let mut active_groups = BTreeSet::new();
592
593 for group in ToolGroup::ALL {
597 if group.is_permitted(capabilities) && Self::group_needs_auto_activate(group) {
598 active_groups.insert(*group);
599 }
600 }
601
602 if !active_groups.is_empty() {
603 info!(
604 auto_activated = ?active_groups,
605 "tool selector: auto-activated groups for granted capabilities"
606 );
607 }
608
609 Self {
610 capabilities: capabilities.to_vec(),
611 active_groups,
612 last_tool_hash: 0,
613 }
614 }
615
616 fn group_needs_auto_activate(group: &ToolGroup) -> bool {
619 matches!(
620 group,
621 ToolGroup::SourceControl
622 | ToolGroup::Container
623 | ToolGroup::DataManipulation
624 | ToolGroup::Schedule
625 | ToolGroup::BrowserControl
626 | ToolGroup::AgentCoordination
627 | ToolGroup::ProcessManagement
628 )
629 }
630
631 pub fn select_tools(&mut self, messages: &[Message]) -> (Vec<ToolDefinition>, bool) {
640 let scan_text = Self::build_scan_text(messages);
641
642 let mut newly_activated = Vec::new();
643 for group in ToolGroup::ALL {
644 if self.active_groups.contains(group) {
645 continue;
646 }
647 if !group.is_permitted(&self.capabilities) {
648 continue;
649 }
650 if Self::keywords_match(&scan_text, group.keywords()) {
651 self.active_groups.insert(*group);
652 newly_activated.push(*group);
653 }
654 }
655
656 if !newly_activated.is_empty() {
657 info!(
658 groups = ?newly_activated,
659 total_active = self.active_groups.len(),
660 "tool selector: activated new groups"
661 );
662 }
663
664 let mut tools = self.core_tools();
665 for group in &self.active_groups {
666 for tool in group.tools() {
667 push_unique(&mut tools, tool);
668 }
669 }
670
671 let hash = Self::compute_hash(&tools);
672 let changed = hash != self.last_tool_hash;
673 self.last_tool_hash = hash;
674
675 (tools, changed)
676 }
677
678 pub fn active_group_count(&self) -> usize {
680 self.active_groups.len()
681 }
682
683 fn core_tools(&self) -> Vec<ToolDefinition> {
685 let mut tools = Vec::with_capacity(20);
686
687 for cap in &self.capabilities {
688 match cap {
689 Capability::FileRead(_) => {
690 push_unique(&mut tools, file_read());
691 push_unique(&mut tools, file_list());
692 push_unique(&mut tools, file_search());
693 push_unique(&mut tools, file_info());
694 }
695 Capability::FileWrite(_) => {
696 push_unique(&mut tools, file_write());
697 push_unique(&mut tools, patch_apply());
698 }
699 Capability::ShellExec(_) => {
700 push_unique(&mut tools, shell_exec());
701 push_unique(&mut tools, env_get());
702 push_unique(&mut tools, env_list());
703 }
704 Capability::Network(_) => {
705 push_unique(&mut tools, web_fetch());
706 push_unique(&mut tools, web_search());
707 }
708 Capability::Memory => {
709 push_unique(&mut tools, memory_store());
710 push_unique(&mut tools, memory_recall());
711 }
712 Capability::SystemAutomation => {
713 push_unique(&mut tools, sys_screenshot());
714 }
715 Capability::ChannelNotify => {
716 push_unique(&mut tools, channel_notify());
717 }
718 Capability::SelfConfig => {
719 push_unique(&mut tools, heartbeat_add());
720 push_unique(&mut tools, heartbeat_list());
721 push_unique(&mut tools, heartbeat_remove());
722 push_unique(&mut tools, creed_view());
723 push_unique(&mut tools, skill_list());
724 push_unique(&mut tools, skill_recommend());
725 }
726 _ => {}
727 }
728 }
729
730 tools
731 }
732
733 fn build_scan_text(messages: &[Message]) -> String {
738 let window_size = 4.min(messages.len());
739 let start = messages.len().saturating_sub(window_size);
740 let mut text = String::with_capacity(4000);
741 for msg in &messages[start..] {
742 text.push_str(&msg.content);
743 text.push(' ');
744 for tc in &msg.tool_calls {
745 text.push_str(&tc.name);
746 text.push(' ');
747 if let Some(obj) = tc.input.as_object() {
749 for val in obj.values() {
750 if let Some(s) = val.as_str() {
751 if s.len() <= 200 {
753 text.push_str(s);
754 text.push(' ');
755 }
756 }
757 }
758 }
759 }
760 for tr in &msg.tool_results {
761 let end = if tr.content.len() > 200 {
764 tr.content.floor_char_boundary(200)
765 } else {
766 tr.content.len()
767 };
768 text.push_str(&tr.content[..end]);
769 text.push(' ');
770 }
771 }
772 text.to_lowercase()
773 }
774
775 fn keywords_match(scan_text: &str, keywords: &[&str]) -> bool {
777 keywords.iter().any(|kw| scan_text.contains(kw))
778 }
779
780 fn compute_hash(tools: &[ToolDefinition]) -> u64 {
785 let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for tool in tools {
787 for byte in tool.name.as_bytes() {
788 hash ^= u64::from(*byte);
789 hash = hash.wrapping_mul(0x0100_0000_01b3); }
791 hash ^= 0xff;
793 hash = hash.wrapping_mul(0x0100_0000_01b3);
794 }
795 hash
796 }
797}
798
799fn file_read() -> ToolDefinition {
804 ToolDefinition {
805 name: "file_read".into(),
806 description: "Read a file by path (absolute or relative). Supports text, code, config, logs, CSV, JSON, etc.".into(),
807 input_schema: serde_json::json!({
808 "type": "object",
809 "properties": {
810 "path": {
811 "type": "string",
812 "description": "The file path to read (relative to working directory or absolute)."
813 }
814 },
815 "required": ["path"]
816 }),
817 category: ToolCategory::FileSystem,
818 }
819}
820
821fn file_write() -> ToolDefinition {
822 ToolDefinition {
823 name: "file_write".into(),
824 description: "Write or create a file. Creates parent directories if needed.".into(),
825 input_schema: serde_json::json!({
826 "type": "object",
827 "properties": {
828 "path": {
829 "type": "string",
830 "description": "The file path to write to."
831 },
832 "content": {
833 "type": "string",
834 "description": "The content to write to the file."
835 }
836 },
837 "required": ["path", "content"]
838 }),
839 category: ToolCategory::FileSystem,
840 }
841}
842
843fn file_list() -> ToolDefinition {
844 ToolDefinition {
845 name: "file_list".into(),
846 description: "List files and directories in a folder.".into(),
847 input_schema: serde_json::json!({
848 "type": "object",
849 "properties": {
850 "path": {
851 "type": "string",
852 "description": "The directory path to list (defaults to working directory)."
853 }
854 }
855 }),
856 category: ToolCategory::FileSystem,
857 }
858}
859
860fn shell_exec() -> ToolDefinition {
861 ToolDefinition {
862 name: "shell_exec".into(),
863 description: "Execute a shell command and return stdout, stderr, and exit code. Universal fallback for any task doable from a terminal.".into(),
864 input_schema: serde_json::json!({
865 "type": "object",
866 "properties": {
867 "command": {
868 "type": "string",
869 "description": "The shell command to execute."
870 }
871 },
872 "required": ["command"]
873 }),
874 category: ToolCategory::Shell,
875 }
876}
877
878fn web_fetch() -> ToolDefinition {
879 ToolDefinition {
880 name: "web_fetch".into(),
881 description: "Fetch the content of a URL via HTTP GET.".into(),
882 input_schema: serde_json::json!({
883 "type": "object",
884 "properties": {
885 "url": {
886 "type": "string",
887 "description": "The URL to fetch."
888 }
889 },
890 "required": ["url"]
891 }),
892 category: ToolCategory::Web,
893 }
894}
895
896fn web_search() -> ToolDefinition {
897 ToolDefinition {
898 name: "web_search".into(),
899 description: "Search the web and return top results with titles and URLs.".into(),
900 input_schema: serde_json::json!({
901 "type": "object",
902 "properties": {
903 "query": {
904 "type": "string",
905 "description": "The search query."
906 }
907 },
908 "required": ["query"]
909 }),
910 category: ToolCategory::Web,
911 }
912}
913
914fn memory_store() -> ToolDefinition {
915 ToolDefinition {
916 name: "memory_store".into(),
917 description: "Store a key-value pair in persistent memory. Use to remember facts, preferences, or context across conversations.".into(),
918 input_schema: serde_json::json!({
919 "type": "object",
920 "properties": {
921 "key": {
922 "type": "string",
923 "description": "A short descriptive key for the memory."
924 },
925 "value": {
926 "type": "string",
927 "description": "The value to remember."
928 },
929 "confidence": {
930 "type": "number",
931 "description": "Confidence level from 0.0 to 1.0 (default: 0.9)."
932 }
933 },
934 "required": ["key", "value"]
935 }),
936 category: ToolCategory::Memory,
937 }
938}
939
940fn memory_recall() -> ToolDefinition {
941 ToolDefinition {
942 name: "memory_recall".into(),
943 description: "Search persistent memory for previously stored information.".into(),
944 input_schema: serde_json::json!({
945 "type": "object",
946 "properties": {
947 "query": {
948 "type": "string",
949 "description": "Search query to find relevant memories."
950 },
951 "limit": {
952 "type": "integer",
953 "description": "Maximum number of results (default: 10)."
954 }
955 },
956 "required": ["query"]
957 }),
958 category: ToolCategory::Memory,
959 }
960}
961
962fn knowledge_add_entity() -> ToolDefinition {
963 ToolDefinition {
964 name: "knowledge_add_entity".into(),
965 description: "Add an entity to your knowledge graph.".into(),
966 input_schema: serde_json::json!({
967 "type": "object",
968 "properties": {
969 "name": {
970 "type": "string",
971 "description": "Name of the entity."
972 },
973 "entity_type": {
974 "type": "string",
975 "description": "Type of entity (e.g. 'person', 'company', 'concept')."
976 },
977 "properties": {
978 "type": "object",
979 "description": "Additional properties as key-value pairs."
980 }
981 },
982 "required": ["name", "entity_type"]
983 }),
984 category: ToolCategory::Knowledge,
985 }
986}
987
988fn knowledge_add_relation() -> ToolDefinition {
989 ToolDefinition {
990 name: "knowledge_add_relation".into(),
991 description: "Add a relation between two entities in your knowledge graph.".into(),
992 input_schema: serde_json::json!({
993 "type": "object",
994 "properties": {
995 "from": {
996 "type": "string",
997 "description": "Source entity name."
998 },
999 "relation": {
1000 "type": "string",
1001 "description": "The relation type (e.g. 'works_at', 'depends_on')."
1002 },
1003 "to": {
1004 "type": "string",
1005 "description": "Target entity name."
1006 },
1007 "properties": {
1008 "type": "object",
1009 "description": "Additional properties."
1010 }
1011 },
1012 "required": ["from", "relation", "to"]
1013 }),
1014 category: ToolCategory::Knowledge,
1015 }
1016}
1017
1018fn knowledge_query() -> ToolDefinition {
1019 ToolDefinition {
1020 name: "knowledge_query".into(),
1021 description: "Search your knowledge graph for entities and their relations.".into(),
1022 input_schema: serde_json::json!({
1023 "type": "object",
1024 "properties": {
1025 "query": {
1026 "type": "string",
1027 "description": "Search query to find entities."
1028 }
1029 },
1030 "required": ["query"]
1031 }),
1032 category: ToolCategory::Knowledge,
1033 }
1034}
1035
1036fn agent_spawn() -> ToolDefinition {
1041 ToolDefinition {
1042 name: "agent_spawn".into(),
1043 description: "Spawn a new fighter (AI agent) and return its ID.".into(),
1044 input_schema: serde_json::json!({
1045 "type": "object",
1046 "properties": {
1047 "name": {
1048 "type": "string",
1049 "description": "A human-readable name for the new fighter."
1050 },
1051 "system_prompt": {
1052 "type": "string",
1053 "description": "The system prompt that shapes the new fighter's behavior and specialization."
1054 },
1055 "description": {
1056 "type": "string",
1057 "description": "A short description of the fighter's purpose (optional)."
1058 },
1059 "capabilities": {
1060 "type": "array",
1061 "description": "Capabilities to grant the new fighter (optional). Each item is a capability object.",
1062 "items": {
1063 "type": "object"
1064 }
1065 }
1066 },
1067 "required": ["name", "system_prompt"]
1068 }),
1069 category: ToolCategory::Agent,
1070 }
1071}
1072
1073fn agent_message() -> ToolDefinition {
1074 ToolDefinition {
1075 name: "agent_message".into(),
1076 description: "Send a message to another fighter by ID or name and get its response.".into(),
1077 input_schema: serde_json::json!({
1078 "type": "object",
1079 "properties": {
1080 "fighter_id": {
1081 "type": "string",
1082 "description": "The UUID of the target fighter (provide either this or 'name')."
1083 },
1084 "name": {
1085 "type": "string",
1086 "description": "The name of the target fighter (provide either this or 'fighter_id')."
1087 },
1088 "message": {
1089 "type": "string",
1090 "description": "The message to send to the target fighter."
1091 }
1092 },
1093 "required": ["message"]
1094 }),
1095 category: ToolCategory::Agent,
1096 }
1097}
1098
1099fn agent_list() -> ToolDefinition {
1100 ToolDefinition {
1101 name: "agent_list".into(),
1102 description: "List all active fighters (AI agents) with their IDs, names, and status."
1103 .into(),
1104 input_schema: serde_json::json!({
1105 "type": "object",
1106 "properties": {}
1107 }),
1108 category: ToolCategory::Agent,
1109 }
1110}
1111
1112fn patch_apply() -> ToolDefinition {
1117 ToolDefinition {
1118 name: "patch_apply".into(),
1119 description: "Apply a unified diff patch to a file.".into(),
1120 input_schema: serde_json::json!({
1121 "type": "object",
1122 "properties": {
1123 "path": {
1124 "type": "string",
1125 "description": "The file path to patch (relative to working directory or absolute)."
1126 },
1127 "diff": {
1128 "type": "string",
1129 "description": "The unified diff text to apply to the file."
1130 }
1131 },
1132 "required": ["path", "diff"]
1133 }),
1134 category: ToolCategory::FileSystem,
1135 }
1136}
1137
1138fn browser_navigate() -> ToolDefinition {
1143 ToolDefinition {
1144 name: "browser_navigate".into(),
1145 description: "Navigate the browser to a URL. Opens the page and waits for it to load."
1146 .into(),
1147 input_schema: serde_json::json!({
1148 "type": "object",
1149 "properties": {
1150 "url": {
1151 "type": "string",
1152 "description": "The URL to navigate to."
1153 }
1154 },
1155 "required": ["url"]
1156 }),
1157 category: ToolCategory::Browser,
1158 }
1159}
1160
1161fn browser_screenshot() -> ToolDefinition {
1162 ToolDefinition {
1163 name: "browser_screenshot".into(),
1164 description: "Take a screenshot of the current page. Returns a base64-encoded PNG image."
1165 .into(),
1166 input_schema: serde_json::json!({
1167 "type": "object",
1168 "properties": {
1169 "full_page": {
1170 "type": "boolean",
1171 "description": "Capture the full scrollable page (true) or just the viewport (false). Default: false."
1172 }
1173 }
1174 }),
1175 category: ToolCategory::Browser,
1176 }
1177}
1178
1179fn browser_click() -> ToolDefinition {
1180 ToolDefinition {
1181 name: "browser_click".into(),
1182 description: "Click an element on the page matching the given CSS selector.".into(),
1183 input_schema: serde_json::json!({
1184 "type": "object",
1185 "properties": {
1186 "selector": {
1187 "type": "string",
1188 "description": "CSS selector of the element to click."
1189 }
1190 },
1191 "required": ["selector"]
1192 }),
1193 category: ToolCategory::Browser,
1194 }
1195}
1196
1197fn browser_type() -> ToolDefinition {
1198 ToolDefinition {
1199 name: "browser_type".into(),
1200 description: "Type text into an input element matching the given CSS selector.".into(),
1201 input_schema: serde_json::json!({
1202 "type": "object",
1203 "properties": {
1204 "selector": {
1205 "type": "string",
1206 "description": "CSS selector of the input element."
1207 },
1208 "text": {
1209 "type": "string",
1210 "description": "The text to type into the element."
1211 }
1212 },
1213 "required": ["selector", "text"]
1214 }),
1215 category: ToolCategory::Browser,
1216 }
1217}
1218
1219fn browser_content() -> ToolDefinition {
1220 ToolDefinition {
1221 name: "browser_content".into(),
1222 description: "Get the text content of the page or a specific element by CSS selector."
1223 .into(),
1224 input_schema: serde_json::json!({
1225 "type": "object",
1226 "properties": {
1227 "selector": {
1228 "type": "string",
1229 "description": "Optional CSS selector. If omitted, returns the full page text content."
1230 }
1231 }
1232 }),
1233 category: ToolCategory::Browser,
1234 }
1235}
1236
1237fn git_status() -> ToolDefinition {
1242 ToolDefinition {
1243 name: "git_status".into(),
1244 description: "Run `git status --porcelain` in the working directory to show changed files."
1245 .into(),
1246 input_schema: serde_json::json!({
1247 "type": "object",
1248 "properties": {}
1249 }),
1250 category: ToolCategory::SourceControl,
1251 }
1252}
1253
1254fn git_diff() -> ToolDefinition {
1255 ToolDefinition {
1256 name: "git_diff".into(),
1257 description:
1258 "Run `git diff` to show unstaged changes. Use `staged: true` to see staged changes."
1259 .into(),
1260 input_schema: serde_json::json!({
1261 "type": "object",
1262 "properties": {
1263 "staged": {
1264 "type": "boolean",
1265 "description": "If true, show staged changes (--staged). Default: false."
1266 },
1267 "path": {
1268 "type": "string",
1269 "description": "Optional file path to restrict the diff to."
1270 }
1271 }
1272 }),
1273 category: ToolCategory::SourceControl,
1274 }
1275}
1276
1277fn git_log() -> ToolDefinition {
1278 ToolDefinition {
1279 name: "git_log".into(),
1280 description: "Show recent git commits with `git log --oneline`.".into(),
1281 input_schema: serde_json::json!({
1282 "type": "object",
1283 "properties": {
1284 "count": {
1285 "type": "integer",
1286 "description": "Number of commits to show (default: 10)."
1287 }
1288 }
1289 }),
1290 category: ToolCategory::SourceControl,
1291 }
1292}
1293
1294fn git_commit() -> ToolDefinition {
1295 ToolDefinition {
1296 name: "git_commit".into(),
1297 description: "Stage files and create a git commit with the given message.".into(),
1298 input_schema: serde_json::json!({
1299 "type": "object",
1300 "properties": {
1301 "message": {
1302 "type": "string",
1303 "description": "The commit message."
1304 },
1305 "files": {
1306 "type": "array",
1307 "items": { "type": "string" },
1308 "description": "Files to stage before committing. If empty, commits all staged changes."
1309 }
1310 },
1311 "required": ["message"]
1312 }),
1313 category: ToolCategory::SourceControl,
1314 }
1315}
1316
1317fn git_branch() -> ToolDefinition {
1318 ToolDefinition {
1319 name: "git_branch".into(),
1320 description: "List, create, or switch git branches.".into(),
1321 input_schema: serde_json::json!({
1322 "type": "object",
1323 "properties": {
1324 "action": {
1325 "type": "string",
1326 "enum": ["list", "create", "switch"],
1327 "description": "Action to perform: list, create, or switch. Default: list."
1328 },
1329 "name": {
1330 "type": "string",
1331 "description": "Branch name (required for create and switch)."
1332 }
1333 }
1334 }),
1335 category: ToolCategory::SourceControl,
1336 }
1337}
1338
1339fn docker_ps() -> ToolDefinition {
1344 ToolDefinition {
1345 name: "docker_ps".into(),
1346 description: "List running Docker containers.".into(),
1347 input_schema: serde_json::json!({
1348 "type": "object",
1349 "properties": {
1350 "all": {
1351 "type": "boolean",
1352 "description": "Show all containers, not just running ones. Default: false."
1353 }
1354 }
1355 }),
1356 category: ToolCategory::Container,
1357 }
1358}
1359
1360fn docker_run() -> ToolDefinition {
1361 ToolDefinition {
1362 name: "docker_run".into(),
1363 description: "Run a Docker container from an image.".into(),
1364 input_schema: serde_json::json!({
1365 "type": "object",
1366 "properties": {
1367 "image": {
1368 "type": "string",
1369 "description": "The Docker image to run."
1370 },
1371 "command": {
1372 "type": "string",
1373 "description": "Optional command to run inside the container."
1374 },
1375 "env": {
1376 "type": "object",
1377 "description": "Environment variables as key-value pairs."
1378 },
1379 "ports": {
1380 "type": "array",
1381 "items": { "type": "string" },
1382 "description": "Port mappings (e.g. '8080:80')."
1383 },
1384 "detach": {
1385 "type": "boolean",
1386 "description": "Run in detached mode. Default: false."
1387 },
1388 "name": {
1389 "type": "string",
1390 "description": "Optional container name."
1391 }
1392 },
1393 "required": ["image"]
1394 }),
1395 category: ToolCategory::Container,
1396 }
1397}
1398
1399fn docker_build() -> ToolDefinition {
1400 ToolDefinition {
1401 name: "docker_build".into(),
1402 description: "Build a Docker image from a Dockerfile.".into(),
1403 input_schema: serde_json::json!({
1404 "type": "object",
1405 "properties": {
1406 "path": {
1407 "type": "string",
1408 "description": "Path to the build context directory (default: '.')."
1409 },
1410 "tag": {
1411 "type": "string",
1412 "description": "Tag for the built image (e.g. 'myapp:latest')."
1413 },
1414 "dockerfile": {
1415 "type": "string",
1416 "description": "Path to the Dockerfile (default: 'Dockerfile')."
1417 }
1418 }
1419 }),
1420 category: ToolCategory::Container,
1421 }
1422}
1423
1424fn docker_logs() -> ToolDefinition {
1425 ToolDefinition {
1426 name: "docker_logs".into(),
1427 description: "Get logs from a Docker container.".into(),
1428 input_schema: serde_json::json!({
1429 "type": "object",
1430 "properties": {
1431 "container": {
1432 "type": "string",
1433 "description": "Container ID or name."
1434 },
1435 "tail": {
1436 "type": "integer",
1437 "description": "Number of lines to show from the end (default: 100)."
1438 }
1439 },
1440 "required": ["container"]
1441 }),
1442 category: ToolCategory::Container,
1443 }
1444}
1445
1446fn http_request() -> ToolDefinition {
1451 ToolDefinition {
1452 name: "http_request".into(),
1453 description: "Send a full HTTP request with custom method, headers, body, and timeout."
1454 .into(),
1455 input_schema: serde_json::json!({
1456 "type": "object",
1457 "properties": {
1458 "url": {
1459 "type": "string",
1460 "description": "The URL to send the request to."
1461 },
1462 "method": {
1463 "type": "string",
1464 "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"],
1465 "description": "HTTP method. Default: GET."
1466 },
1467 "headers": {
1468 "type": "object",
1469 "description": "Request headers as key-value pairs."
1470 },
1471 "body": {
1472 "type": "string",
1473 "description": "Request body."
1474 },
1475 "timeout_secs": {
1476 "type": "integer",
1477 "description": "Request timeout in seconds (default: 30)."
1478 }
1479 },
1480 "required": ["url"]
1481 }),
1482 category: ToolCategory::Web,
1483 }
1484}
1485
1486fn http_post() -> ToolDefinition {
1487 ToolDefinition {
1488 name: "http_post".into(),
1489 description: "Shorthand for an HTTP POST request with a JSON body.".into(),
1490 input_schema: serde_json::json!({
1491 "type": "object",
1492 "properties": {
1493 "url": {
1494 "type": "string",
1495 "description": "The URL to POST to."
1496 },
1497 "json": {
1498 "type": "object",
1499 "description": "JSON body to send."
1500 },
1501 "headers": {
1502 "type": "object",
1503 "description": "Additional headers."
1504 }
1505 },
1506 "required": ["url", "json"]
1507 }),
1508 category: ToolCategory::Web,
1509 }
1510}
1511
1512fn json_query() -> ToolDefinition {
1517 ToolDefinition {
1518 name: "json_query".into(),
1519 description: "Query a JSON value using a dot-separated path (e.g. 'users.0.name'). Array indices are numeric.".into(),
1520 input_schema: serde_json::json!({
1521 "type": "object",
1522 "properties": {
1523 "data": {
1524 "description": "The JSON data to query (object, array, or string to parse)."
1525 },
1526 "path": {
1527 "type": "string",
1528 "description": "Dot-separated path to query (e.g. 'a.b.0.c')."
1529 }
1530 },
1531 "required": ["data", "path"]
1532 }),
1533 category: ToolCategory::Data,
1534 }
1535}
1536
1537fn json_transform() -> ToolDefinition {
1538 ToolDefinition {
1539 name: "json_transform".into(),
1540 description: "Transform JSON: extract keys, rename keys, or filter arrays.".into(),
1541 input_schema: serde_json::json!({
1542 "type": "object",
1543 "properties": {
1544 "data": {
1545 "description": "The JSON data to transform."
1546 },
1547 "extract": {
1548 "type": "array",
1549 "items": { "type": "string" },
1550 "description": "List of keys to extract from each object."
1551 },
1552 "rename": {
1553 "type": "object",
1554 "description": "Key rename mapping (old_name -> new_name)."
1555 },
1556 "filter_key": {
1557 "type": "string",
1558 "description": "Key to filter array items by."
1559 },
1560 "filter_value": {
1561 "type": "string",
1562 "description": "Value the filter_key must match."
1563 }
1564 },
1565 "required": ["data"]
1566 }),
1567 category: ToolCategory::Data,
1568 }
1569}
1570
1571fn yaml_parse() -> ToolDefinition {
1572 ToolDefinition {
1573 name: "yaml_parse".into(),
1574 description: "Parse a YAML string and return it as JSON.".into(),
1575 input_schema: serde_json::json!({
1576 "type": "object",
1577 "properties": {
1578 "content": {
1579 "type": "string",
1580 "description": "The YAML string to parse."
1581 }
1582 },
1583 "required": ["content"]
1584 }),
1585 category: ToolCategory::Data,
1586 }
1587}
1588
1589fn regex_match() -> ToolDefinition {
1590 ToolDefinition {
1591 name: "regex_match".into(),
1592 description: "Match a regex pattern against text and return all captures.".into(),
1593 input_schema: serde_json::json!({
1594 "type": "object",
1595 "properties": {
1596 "pattern": {
1597 "type": "string",
1598 "description": "The regex pattern."
1599 },
1600 "text": {
1601 "type": "string",
1602 "description": "The text to match against."
1603 },
1604 "global": {
1605 "type": "boolean",
1606 "description": "Find all matches (true) or just the first (false). Default: false."
1607 }
1608 },
1609 "required": ["pattern", "text"]
1610 }),
1611 category: ToolCategory::Data,
1612 }
1613}
1614
1615fn regex_replace() -> ToolDefinition {
1616 ToolDefinition {
1617 name: "regex_replace".into(),
1618 description: "Find and replace text using a regex pattern with capture group support."
1619 .into(),
1620 input_schema: serde_json::json!({
1621 "type": "object",
1622 "properties": {
1623 "pattern": {
1624 "type": "string",
1625 "description": "The regex pattern to find."
1626 },
1627 "replacement": {
1628 "type": "string",
1629 "description": "The replacement string (supports $1, $2, etc. for captures)."
1630 },
1631 "text": {
1632 "type": "string",
1633 "description": "The text to perform replacement on."
1634 }
1635 },
1636 "required": ["pattern", "replacement", "text"]
1637 }),
1638 category: ToolCategory::Data,
1639 }
1640}
1641
1642fn process_list() -> ToolDefinition {
1647 ToolDefinition {
1648 name: "process_list".into(),
1649 description: "List running processes with PID, name, and CPU/memory usage.".into(),
1650 input_schema: serde_json::json!({
1651 "type": "object",
1652 "properties": {
1653 "filter": {
1654 "type": "string",
1655 "description": "Optional filter string to match process names."
1656 }
1657 }
1658 }),
1659 category: ToolCategory::Shell,
1660 }
1661}
1662
1663fn process_kill() -> ToolDefinition {
1664 ToolDefinition {
1665 name: "process_kill".into(),
1666 description: "Kill a process by PID.".into(),
1667 input_schema: serde_json::json!({
1668 "type": "object",
1669 "properties": {
1670 "pid": {
1671 "type": "integer",
1672 "description": "The process ID to kill."
1673 },
1674 "signal": {
1675 "type": "string",
1676 "description": "Signal to send (e.g. 'TERM', 'KILL'). Default: 'TERM'."
1677 }
1678 },
1679 "required": ["pid"]
1680 }),
1681 category: ToolCategory::Shell,
1682 }
1683}
1684
1685fn schedule_task() -> ToolDefinition {
1690 ToolDefinition {
1691 name: "schedule_task".into(),
1692 description: "Schedule a one-shot or recurring task. Returns a task ID.".into(),
1693 input_schema: serde_json::json!({
1694 "type": "object",
1695 "properties": {
1696 "name": {
1697 "type": "string",
1698 "description": "Human-readable name for the task."
1699 },
1700 "command": {
1701 "type": "string",
1702 "description": "Shell command to execute when the task fires."
1703 },
1704 "delay_secs": {
1705 "type": "integer",
1706 "description": "Delay in seconds before first execution."
1707 },
1708 "interval_secs": {
1709 "type": "integer",
1710 "description": "Interval in seconds for recurring execution. If omitted, the task runs once."
1711 }
1712 },
1713 "required": ["name", "command", "delay_secs"]
1714 }),
1715 category: ToolCategory::Schedule,
1716 }
1717}
1718
1719fn schedule_list() -> ToolDefinition {
1720 ToolDefinition {
1721 name: "schedule_list".into(),
1722 description: "List all scheduled tasks with their IDs, names, and status.".into(),
1723 input_schema: serde_json::json!({
1724 "type": "object",
1725 "properties": {}
1726 }),
1727 category: ToolCategory::Schedule,
1728 }
1729}
1730
1731fn schedule_cancel() -> ToolDefinition {
1732 ToolDefinition {
1733 name: "schedule_cancel".into(),
1734 description: "Cancel a scheduled task by its ID.".into(),
1735 input_schema: serde_json::json!({
1736 "type": "object",
1737 "properties": {
1738 "task_id": {
1739 "type": "string",
1740 "description": "The UUID of the task to cancel."
1741 }
1742 },
1743 "required": ["task_id"]
1744 }),
1745 category: ToolCategory::Schedule,
1746 }
1747}
1748
1749fn code_search() -> ToolDefinition {
1754 ToolDefinition {
1755 name: "code_search".into(),
1756 description: "Search for a regex pattern in files recursively. Returns matching lines with paths and line numbers.".into(),
1757 input_schema: serde_json::json!({
1758 "type": "object",
1759 "properties": {
1760 "pattern": {
1761 "type": "string",
1762 "description": "The regex pattern to search for."
1763 },
1764 "path": {
1765 "type": "string",
1766 "description": "Root directory to search in (default: working directory)."
1767 },
1768 "file_pattern": {
1769 "type": "string",
1770 "description": "Glob pattern to filter files (e.g. '*.rs', '*.py')."
1771 },
1772 "max_results": {
1773 "type": "integer",
1774 "description": "Maximum number of matches to return (default: 50)."
1775 }
1776 },
1777 "required": ["pattern"]
1778 }),
1779 category: ToolCategory::CodeAnalysis,
1780 }
1781}
1782
1783fn code_symbols() -> ToolDefinition {
1784 ToolDefinition {
1785 name: "code_symbols".into(),
1786 description: "Extract function, struct, class, and method definitions from a source file."
1787 .into(),
1788 input_schema: serde_json::json!({
1789 "type": "object",
1790 "properties": {
1791 "path": {
1792 "type": "string",
1793 "description": "Path to the source file to analyze."
1794 }
1795 },
1796 "required": ["path"]
1797 }),
1798 category: ToolCategory::CodeAnalysis,
1799 }
1800}
1801
1802fn archive_create() -> ToolDefinition {
1807 ToolDefinition {
1808 name: "archive_create".into(),
1809 description: "Create a tar.gz archive from a list of file or directory paths.".into(),
1810 input_schema: serde_json::json!({
1811 "type": "object",
1812 "properties": {
1813 "output_path": {
1814 "type": "string",
1815 "description": "Path for the output .tar.gz archive file."
1816 },
1817 "paths": {
1818 "type": "array",
1819 "items": { "type": "string" },
1820 "description": "List of file or directory paths to include in the archive."
1821 }
1822 },
1823 "required": ["output_path", "paths"]
1824 }),
1825 category: ToolCategory::Archive,
1826 }
1827}
1828
1829fn archive_extract() -> ToolDefinition {
1830 ToolDefinition {
1831 name: "archive_extract".into(),
1832 description: "Extract a tar.gz archive to a destination directory.".into(),
1833 input_schema: serde_json::json!({
1834 "type": "object",
1835 "properties": {
1836 "archive_path": {
1837 "type": "string",
1838 "description": "Path to the .tar.gz archive to extract."
1839 },
1840 "destination": {
1841 "type": "string",
1842 "description": "Directory to extract the archive into."
1843 }
1844 },
1845 "required": ["archive_path", "destination"]
1846 }),
1847 category: ToolCategory::Archive,
1848 }
1849}
1850
1851fn archive_list() -> ToolDefinition {
1852 ToolDefinition {
1853 name: "archive_list".into(),
1854 description: "List the contents of a tar.gz archive without extracting.".into(),
1855 input_schema: serde_json::json!({
1856 "type": "object",
1857 "properties": {
1858 "archive_path": {
1859 "type": "string",
1860 "description": "Path to the .tar.gz archive to list."
1861 }
1862 },
1863 "required": ["archive_path"]
1864 }),
1865 category: ToolCategory::Archive,
1866 }
1867}
1868
1869fn template_render() -> ToolDefinition {
1874 ToolDefinition {
1875 name: "template_render".into(),
1876 description: "Render a Handlebars-style template by substituting {{variable}} placeholders with provided values.".into(),
1877 input_schema: serde_json::json!({
1878 "type": "object",
1879 "properties": {
1880 "template": {
1881 "type": "string",
1882 "description": "The template string containing {{variable}} placeholders."
1883 },
1884 "variables": {
1885 "type": "object",
1886 "description": "Key-value pairs mapping variable names to their values."
1887 }
1888 },
1889 "required": ["template", "variables"]
1890 }),
1891 category: ToolCategory::Template,
1892 }
1893}
1894
1895fn hash_compute() -> ToolDefinition {
1900 ToolDefinition {
1901 name: "hash_compute".into(),
1902 description: "Compute a cryptographic hash (SHA-256, SHA-512, or MD5) of a string or file."
1903 .into(),
1904 input_schema: serde_json::json!({
1905 "type": "object",
1906 "properties": {
1907 "algorithm": {
1908 "type": "string",
1909 "enum": ["sha256", "sha512", "md5"],
1910 "description": "Hash algorithm to use. Default: sha256."
1911 },
1912 "input": {
1913 "type": "string",
1914 "description": "The string to hash (provide either this or 'file')."
1915 },
1916 "file": {
1917 "type": "string",
1918 "description": "Path to a file to hash (provide either this or 'input')."
1919 }
1920 }
1921 }),
1922 category: ToolCategory::Crypto,
1923 }
1924}
1925
1926fn hash_verify() -> ToolDefinition {
1927 ToolDefinition {
1928 name: "hash_verify".into(),
1929 description: "Verify that a hash matches an expected value.".into(),
1930 input_schema: serde_json::json!({
1931 "type": "object",
1932 "properties": {
1933 "algorithm": {
1934 "type": "string",
1935 "enum": ["sha256", "sha512", "md5"],
1936 "description": "Hash algorithm to use. Default: sha256."
1937 },
1938 "input": {
1939 "type": "string",
1940 "description": "The string to hash (provide either this or 'file')."
1941 },
1942 "file": {
1943 "type": "string",
1944 "description": "Path to a file to hash (provide either this or 'input')."
1945 },
1946 "expected": {
1947 "type": "string",
1948 "description": "The expected hex-encoded hash value to compare against."
1949 }
1950 },
1951 "required": ["expected"]
1952 }),
1953 category: ToolCategory::Crypto,
1954 }
1955}
1956
1957fn env_get() -> ToolDefinition {
1962 ToolDefinition {
1963 name: "env_get".into(),
1964 description: "Get the value of an environment variable.".into(),
1965 input_schema: serde_json::json!({
1966 "type": "object",
1967 "properties": {
1968 "name": {
1969 "type": "string",
1970 "description": "The environment variable name."
1971 }
1972 },
1973 "required": ["name"]
1974 }),
1975 category: ToolCategory::Shell,
1976 }
1977}
1978
1979fn env_list() -> ToolDefinition {
1980 ToolDefinition {
1981 name: "env_list".into(),
1982 description: "List all environment variables, with optional prefix filter.".into(),
1983 input_schema: serde_json::json!({
1984 "type": "object",
1985 "properties": {
1986 "prefix": {
1987 "type": "string",
1988 "description": "Optional prefix to filter environment variable names by."
1989 }
1990 }
1991 }),
1992 category: ToolCategory::Shell,
1993 }
1994}
1995
1996fn text_diff() -> ToolDefinition {
2001 ToolDefinition {
2002 name: "text_diff".into(),
2003 description: "Compute a unified diff between two text strings.".into(),
2004 input_schema: serde_json::json!({
2005 "type": "object",
2006 "properties": {
2007 "old_text": {
2008 "type": "string",
2009 "description": "The original text."
2010 },
2011 "new_text": {
2012 "type": "string",
2013 "description": "The modified text."
2014 },
2015 "label": {
2016 "type": "string",
2017 "description": "Optional label for the diff output (default: 'a' / 'b')."
2018 }
2019 },
2020 "required": ["old_text", "new_text"]
2021 }),
2022 category: ToolCategory::Data,
2023 }
2024}
2025
2026fn text_count() -> ToolDefinition {
2027 ToolDefinition {
2028 name: "text_count".into(),
2029 description: "Count lines, words, and characters in text.".into(),
2030 input_schema: serde_json::json!({
2031 "type": "object",
2032 "properties": {
2033 "text": {
2034 "type": "string",
2035 "description": "The text to count."
2036 }
2037 },
2038 "required": ["text"]
2039 }),
2040 category: ToolCategory::Data,
2041 }
2042}
2043
2044fn file_search() -> ToolDefinition {
2049 ToolDefinition {
2050 name: "file_search".into(),
2051 description: "Search for files by glob pattern recursively.".into(),
2052 input_schema: serde_json::json!({
2053 "type": "object",
2054 "properties": {
2055 "pattern": {
2056 "type": "string",
2057 "description": "Glob pattern to match file names (e.g. '*.rs', 'Cargo.*')."
2058 },
2059 "path": {
2060 "type": "string",
2061 "description": "Root directory to search in (default: working directory)."
2062 },
2063 "max_results": {
2064 "type": "integer",
2065 "description": "Maximum number of results to return (default: 100)."
2066 }
2067 },
2068 "required": ["pattern"]
2069 }),
2070 category: ToolCategory::FileSystem,
2071 }
2072}
2073
2074fn file_info() -> ToolDefinition {
2075 ToolDefinition {
2076 name: "file_info".into(),
2077 description: "Get file metadata: size, modified time, permissions, and type.".into(),
2078 input_schema: serde_json::json!({
2079 "type": "object",
2080 "properties": {
2081 "path": {
2082 "type": "string",
2083 "description": "Path to the file or directory to inspect."
2084 }
2085 },
2086 "required": ["path"]
2087 }),
2088 category: ToolCategory::FileSystem,
2089 }
2090}
2091
2092fn a2a_delegate() -> ToolDefinition {
2093 ToolDefinition {
2094 name: "a2a_delegate".into(),
2095 description: "Delegate a task to a remote A2A agent and return the result.".into(),
2096 input_schema: serde_json::json!({
2097 "type": "object",
2098 "properties": {
2099 "agent_url": {
2100 "type": "string",
2101 "description": "Base URL of the remote A2A agent (e.g. 'https://agent.example.com')."
2102 },
2103 "prompt": {
2104 "type": "string",
2105 "description": "The task description / prompt to send to the remote agent."
2106 },
2107 "context": {
2108 "type": "object",
2109 "description": "Optional additional context as key-value pairs."
2110 },
2111 "timeout_secs": {
2112 "type": "integer",
2113 "description": "Maximum time to wait for the task to complete (default: 60)."
2114 }
2115 },
2116 "required": ["agent_url", "prompt"]
2117 }),
2118 category: ToolCategory::Agent,
2119 }
2120}
2121
2122fn wasm_invoke() -> ToolDefinition {
2123 ToolDefinition {
2124 name: "wasm_invoke".into(),
2125 description: "Invoke a function on a loaded WASM plugin and return the result.".into(),
2126 input_schema: serde_json::json!({
2127 "type": "object",
2128 "properties": {
2129 "plugin": {
2130 "type": "string",
2131 "description": "Name of the loaded WASM plugin to invoke."
2132 },
2133 "function": {
2134 "type": "string",
2135 "description": "Name of the exported function to call within the plugin."
2136 },
2137 "input": {
2138 "type": "object",
2139 "description": "Input arguments to pass to the plugin function (optional)."
2140 }
2141 },
2142 "required": ["plugin", "function"]
2143 }),
2144 category: ToolCategory::Plugin,
2145 }
2146}
2147
2148fn channel_notify() -> ToolDefinition {
2149 ToolDefinition {
2150 name: "channel_notify".into(),
2151 description: "Send a message to an external channel (Telegram, Slack, Discord, etc.)."
2152 .into(),
2153 input_schema: serde_json::json!({
2154 "type": "object",
2155 "properties": {
2156 "channel": {
2157 "type": "string",
2158 "description": "The channel adapter name (e.g., \"telegram\", \"discord\", \"slack\")."
2159 },
2160 "chat_id": {
2161 "type": "string",
2162 "description": "The channel/conversation ID to send the message to."
2163 },
2164 "message": {
2165 "type": "string",
2166 "description": "The text message to send. Keep it concise and actionable."
2167 }
2168 },
2169 "required": ["channel", "chat_id", "message"]
2170 }),
2171 category: ToolCategory::Channel,
2172 }
2173}
2174
2175fn heartbeat_add() -> ToolDefinition {
2180 ToolDefinition {
2181 name: "heartbeat_add".into(),
2182 description: "Add a recurring heartbeat task to your creed. Cadences: every_bout, on_wake, hourly, daily, weekly, cron.".into(),
2183 input_schema: serde_json::json!({
2184 "type": "object",
2185 "properties": {
2186 "task": {
2187 "type": "string",
2188 "description": "What to do when the heartbeat fires (e.g., \"Check email for important messages and notify user\")."
2189 },
2190 "cadence": {
2191 "type": "string",
2192 "description": "How often: every_bout, on_wake, hourly, daily, weekly, 'every 30m', 'every 2h', or cron like '*/10 * * * *'."
2193 }
2194 },
2195 "required": ["task", "cadence"]
2196 }),
2197 category: ToolCategory::Agent,
2198 }
2199}
2200
2201fn heartbeat_list() -> ToolDefinition {
2202 ToolDefinition {
2203 name: "heartbeat_list".into(),
2204 description: "List all heartbeat tasks in your creed with cadence and execution counts."
2205 .into(),
2206 input_schema: serde_json::json!({
2207 "type": "object",
2208 "properties": {},
2209 "required": []
2210 }),
2211 category: ToolCategory::Agent,
2212 }
2213}
2214
2215fn heartbeat_remove() -> ToolDefinition {
2216 ToolDefinition {
2217 name: "heartbeat_remove".into(),
2218 description: "Remove a heartbeat task from your creed by its 0-based index.".into(),
2219 input_schema: serde_json::json!({
2220 "type": "object",
2221 "properties": {
2222 "index": {
2223 "type": "integer",
2224 "description": "The 0-based index of the heartbeat task to remove."
2225 }
2226 },
2227 "required": ["index"]
2228 }),
2229 category: ToolCategory::Agent,
2230 }
2231}
2232
2233fn creed_view() -> ToolDefinition {
2234 ToolDefinition {
2235 name: "creed_view".into(),
2236 description: "View your current creed: identity, traits, directives, behaviors, relationships, and stats.".into(),
2237 input_schema: serde_json::json!({
2238 "type": "object",
2239 "properties": {},
2240 "required": []
2241 }),
2242 category: ToolCategory::Agent,
2243 }
2244}
2245
2246fn skill_list() -> ToolDefinition {
2247 ToolDefinition {
2248 name: "skill_list".into(),
2249 description: "List available skill packs: productivity, developer, research, files.".into(),
2250 input_schema: serde_json::json!({
2251 "type": "object",
2252 "properties": {},
2253 "required": []
2254 }),
2255 category: ToolCategory::Agent,
2256 }
2257}
2258
2259fn skill_recommend() -> ToolDefinition {
2260 ToolDefinition {
2261 name: "skill_recommend".into(),
2262 description: "Recommend a skill pack when the user needs capabilities you don't have (calendar, email, GitHub). Shows install command.".into(),
2263 input_schema: serde_json::json!({
2264 "type": "object",
2265 "properties": {
2266 "pack_name": {
2267 "type": "string",
2268 "description": "The skill pack name to recommend (e.g., \"productivity\", \"developer\", \"research\", \"files\")."
2269 }
2270 },
2271 "required": ["pack_name"]
2272 }),
2273 category: ToolCategory::Agent,
2274 }
2275}
2276
2277fn sys_screenshot() -> ToolDefinition {
2282 ToolDefinition {
2283 name: "sys_screenshot".into(),
2284 description:
2285 "Capture a screenshot of the full screen or a specific window. Returns base64 PNG."
2286 .into(),
2287 input_schema: serde_json::json!({
2288 "type": "object",
2289 "properties": {
2290 "window": {
2291 "type": "string",
2292 "description": "Optional window title to capture. If omitted, captures the full screen."
2293 }
2294 }
2295 }),
2296 category: ToolCategory::SystemAutomation,
2297 }
2298}
2299
2300fn ui_screenshot() -> ToolDefinition {
2301 ToolDefinition {
2302 name: "ui_screenshot".into(),
2303 description: "Capture a screenshot of a specific UI region by element ID or bounds.".into(),
2304 input_schema: serde_json::json!({
2305 "type": "object",
2306 "properties": {
2307 "element_id": {
2308 "type": "string",
2309 "description": "Element ID from ui_find_elements (e.g. \"Safari:3\"). Captures the region of that element."
2310 },
2311 "bounds": {
2312 "type": "object",
2313 "description": "Explicit bounds to capture: {x, y, width, height} in pixels.",
2314 "properties": {
2315 "x": {"type": "integer"},
2316 "y": {"type": "integer"},
2317 "width": {"type": "integer"},
2318 "height": {"type": "integer"}
2319 },
2320 "required": ["x", "y", "width", "height"]
2321 }
2322 }
2323 }),
2324 category: ToolCategory::UiAutomation,
2325 }
2326}
2327
2328fn app_ocr() -> ToolDefinition {
2329 ToolDefinition {
2330 name: "app_ocr".into(),
2331 description: "Extract text from an app window using OCR. Returns plain text. Prefer over sys_screenshot for text extraction.".into(),
2332 input_schema: serde_json::json!({
2333 "type": "object",
2334 "properties": {
2335 "app": {
2336 "type": "string",
2337 "description": "Name of the application to OCR (e.g. \"Messages\", \"Safari\")."
2338 }
2339 },
2340 "required": ["app"]
2341 }),
2342 category: ToolCategory::AppIntegration,
2343 }
2344}
2345
2346fn ui_find_elements() -> ToolDefinition {
2347 ToolDefinition {
2348 name: "ui_find_elements".into(),
2349 description: "Query the accessibility tree of an app to find UI elements. Returns element IDs for ui_click/ui_type_text. Re-query after state changes — IDs are ephemeral.".into(),
2350 input_schema: serde_json::json!({
2351 "type": "object",
2352 "properties": {
2353 "app": {
2354 "type": "string",
2355 "description": "Name of the application to query (e.g. \"Messages\", \"Safari\")."
2356 },
2357 "role": {
2358 "type": "string",
2359 "description": "Optional: filter by accessibility role (e.g. \"button\", \"text field\", \"row\", \"menu item\")."
2360 },
2361 "label": {
2362 "type": "string",
2363 "description": "Optional: filter by accessibility label (substring match)."
2364 },
2365 "value": {
2366 "type": "string",
2367 "description": "Optional: filter by current value (substring match)."
2368 }
2369 },
2370 "required": ["app"]
2371 }),
2372 category: ToolCategory::UiAutomation,
2373 }
2374}
2375
2376fn ui_click() -> ToolDefinition {
2377 ToolDefinition {
2378 name: "ui_click".into(),
2379 description: "Click a UI element by its element ID from ui_find_elements.".into(),
2380 input_schema: serde_json::json!({
2381 "type": "object",
2382 "properties": {
2383 "element_id": {
2384 "type": "string",
2385 "description": "Element ID from ui_find_elements (e.g. \"Messages:0\")."
2386 }
2387 },
2388 "required": ["element_id"]
2389 }),
2390 category: ToolCategory::UiAutomation,
2391 }
2392}
2393
2394fn ui_type_text() -> ToolDefinition {
2395 ToolDefinition {
2396 name: "ui_type_text".into(),
2397 description: "Type text into a UI element by its element ID from ui_find_elements.".into(),
2398 input_schema: serde_json::json!({
2399 "type": "object",
2400 "properties": {
2401 "element_id": {
2402 "type": "string",
2403 "description": "Element ID of the text field (e.g. \"Messages:2\")."
2404 },
2405 "text": {
2406 "type": "string",
2407 "description": "The text to type into the element."
2408 }
2409 },
2410 "required": ["element_id", "text"]
2411 }),
2412 category: ToolCategory::UiAutomation,
2413 }
2414}
2415
2416fn ui_list_windows() -> ToolDefinition {
2417 ToolDefinition {
2418 name: "ui_list_windows".into(),
2419 description: "List all visible windows with their titles and owning apps.".into(),
2420 input_schema: serde_json::json!({
2421 "type": "object",
2422 "properties": {}
2423 }),
2424 category: ToolCategory::UiAutomation,
2425 }
2426}
2427
2428fn ui_read_attribute() -> ToolDefinition {
2429 ToolDefinition {
2430 name: "ui_read_attribute".into(),
2431 description:
2432 "Read an accessibility attribute (value, enabled, focused, etc.) from a UI element."
2433 .into(),
2434 input_schema: serde_json::json!({
2435 "type": "object",
2436 "properties": {
2437 "element_id": {
2438 "type": "string",
2439 "description": "Element ID from ui_find_elements (e.g. \"Safari:3\")."
2440 },
2441 "attribute": {
2442 "type": "string",
2443 "description": "The attribute to read. Allowed: value, name, role, role description, title, description, enabled, focused, position, size, selected, help, subrole, identifier, minimum value, maximum value, orientation, placeholder value."
2444 }
2445 },
2446 "required": ["element_id", "attribute"]
2447 }),
2448 category: ToolCategory::UiAutomation,
2449 }
2450}
2451
2452#[cfg(test)]
2457mod tests {
2458 use super::*;
2459
2460 #[test]
2461 fn test_browser_tool_definitions_correct() {
2462 let nav = browser_navigate();
2463 assert_eq!(nav.name, "browser_navigate");
2464 assert_eq!(nav.category, ToolCategory::Browser);
2465 assert!(
2466 nav.input_schema["required"]
2467 .as_array()
2468 .expect("required should be array")
2469 .iter()
2470 .any(|v| v == "url")
2471 );
2472
2473 let ss = browser_screenshot();
2474 assert_eq!(ss.name, "browser_screenshot");
2475 assert_eq!(ss.category, ToolCategory::Browser);
2476
2477 let click = browser_click();
2478 assert_eq!(click.name, "browser_click");
2479 assert!(
2480 click.input_schema["required"]
2481 .as_array()
2482 .expect("required should be array")
2483 .iter()
2484 .any(|v| v == "selector")
2485 );
2486
2487 let typ = browser_type();
2488 assert_eq!(typ.name, "browser_type");
2489 let required = typ.input_schema["required"]
2490 .as_array()
2491 .expect("required should be array");
2492 assert!(required.iter().any(|v| v == "selector"));
2493 assert!(required.iter().any(|v| v == "text"));
2494
2495 let content = browser_content();
2496 assert_eq!(content.name, "browser_content");
2497 assert_eq!(content.category, ToolCategory::Browser);
2498 }
2499
2500 #[test]
2501 fn test_browser_tools_require_browser_control_capability() {
2502 let caps = vec![Capability::BrowserControl];
2503 let tools = tools_for_capabilities(&caps);
2504
2505 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2506 assert!(
2507 names.contains(&"browser_navigate"),
2508 "missing browser_navigate"
2509 );
2510 assert!(
2511 names.contains(&"browser_screenshot"),
2512 "missing browser_screenshot"
2513 );
2514 assert!(names.contains(&"browser_click"), "missing browser_click");
2515 assert!(names.contains(&"browser_type"), "missing browser_type");
2516 assert!(
2517 names.contains(&"browser_content"),
2518 "missing browser_content"
2519 );
2520 }
2521
2522 #[test]
2523 fn test_browser_tools_absent_without_capability() {
2524 let caps = vec![Capability::Memory];
2525 let tools = tools_for_capabilities(&caps);
2526
2527 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2528 assert!(!names.iter().any(|n| n.starts_with("browser_")));
2529 }
2530
2531 #[test]
2532 fn test_all_tools_includes_browser() {
2533 let tools = all_tools();
2534 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2535 assert!(names.contains(&"browser_navigate"));
2536 assert!(names.contains(&"browser_screenshot"));
2537 assert!(names.contains(&"browser_click"));
2538 assert!(names.contains(&"browser_type"));
2539 assert!(names.contains(&"browser_content"));
2540 }
2541
2542 #[test]
2547 fn test_file_read_definition() {
2548 let t = file_read();
2549 assert_eq!(t.name, "file_read");
2550 assert_eq!(t.category, ToolCategory::FileSystem);
2551 assert_eq!(t.input_schema["type"], "object");
2552 let required = t.input_schema["required"].as_array().unwrap();
2553 assert!(required.iter().any(|v| v == "path"));
2554 }
2555
2556 #[test]
2557 fn test_file_write_definition() {
2558 let t = file_write();
2559 assert_eq!(t.name, "file_write");
2560 assert_eq!(t.category, ToolCategory::FileSystem);
2561 let required = t.input_schema["required"].as_array().unwrap();
2562 assert!(required.iter().any(|v| v == "path"));
2563 assert!(required.iter().any(|v| v == "content"));
2564 }
2565
2566 #[test]
2567 fn test_file_list_definition() {
2568 let t = file_list();
2569 assert_eq!(t.name, "file_list");
2570 assert_eq!(t.category, ToolCategory::FileSystem);
2571 assert_eq!(t.input_schema["type"], "object");
2572 }
2573
2574 #[test]
2575 fn test_file_search_definition() {
2576 let t = file_search();
2577 assert_eq!(t.name, "file_search");
2578 assert_eq!(t.category, ToolCategory::FileSystem);
2579 let required = t.input_schema["required"].as_array().unwrap();
2580 assert!(required.iter().any(|v| v == "pattern"));
2581 }
2582
2583 #[test]
2584 fn test_file_info_definition() {
2585 let t = file_info();
2586 assert_eq!(t.name, "file_info");
2587 assert_eq!(t.category, ToolCategory::FileSystem);
2588 let required = t.input_schema["required"].as_array().unwrap();
2589 assert!(required.iter().any(|v| v == "path"));
2590 }
2591
2592 #[test]
2593 fn test_patch_apply_definition() {
2594 let t = patch_apply();
2595 assert_eq!(t.name, "patch_apply");
2596 assert_eq!(t.category, ToolCategory::FileSystem);
2597 let required = t.input_schema["required"].as_array().unwrap();
2598 assert!(required.iter().any(|v| v == "path"));
2599 assert!(required.iter().any(|v| v == "diff"));
2600 }
2601
2602 #[test]
2603 fn test_shell_exec_definition() {
2604 let t = shell_exec();
2605 assert_eq!(t.name, "shell_exec");
2606 assert_eq!(t.category, ToolCategory::Shell);
2607 let required = t.input_schema["required"].as_array().unwrap();
2608 assert!(required.iter().any(|v| v == "command"));
2609 }
2610
2611 #[test]
2612 fn test_web_fetch_definition() {
2613 let t = web_fetch();
2614 assert_eq!(t.name, "web_fetch");
2615 assert_eq!(t.category, ToolCategory::Web);
2616 let required = t.input_schema["required"].as_array().unwrap();
2617 assert!(required.iter().any(|v| v == "url"));
2618 }
2619
2620 #[test]
2621 fn test_web_search_definition() {
2622 let t = web_search();
2623 assert_eq!(t.name, "web_search");
2624 assert_eq!(t.category, ToolCategory::Web);
2625 let required = t.input_schema["required"].as_array().unwrap();
2626 assert!(required.iter().any(|v| v == "query"));
2627 }
2628
2629 #[test]
2630 fn test_memory_store_definition() {
2631 let t = memory_store();
2632 assert_eq!(t.name, "memory_store");
2633 assert_eq!(t.category, ToolCategory::Memory);
2634 let required = t.input_schema["required"].as_array().unwrap();
2635 assert!(required.iter().any(|v| v == "key"));
2636 assert!(required.iter().any(|v| v == "value"));
2637 }
2638
2639 #[test]
2640 fn test_memory_recall_definition() {
2641 let t = memory_recall();
2642 assert_eq!(t.name, "memory_recall");
2643 assert_eq!(t.category, ToolCategory::Memory);
2644 let required = t.input_schema["required"].as_array().unwrap();
2645 assert!(required.iter().any(|v| v == "query"));
2646 }
2647
2648 #[test]
2649 fn test_knowledge_tools_definitions() {
2650 let ae = knowledge_add_entity();
2651 assert_eq!(ae.name, "knowledge_add_entity");
2652 assert_eq!(ae.category, ToolCategory::Knowledge);
2653 let required = ae.input_schema["required"].as_array().unwrap();
2654 assert!(required.iter().any(|v| v == "name"));
2655 assert!(required.iter().any(|v| v == "entity_type"));
2656
2657 let ar = knowledge_add_relation();
2658 assert_eq!(ar.name, "knowledge_add_relation");
2659 let required = ar.input_schema["required"].as_array().unwrap();
2660 assert!(required.iter().any(|v| v == "from"));
2661 assert!(required.iter().any(|v| v == "relation"));
2662 assert!(required.iter().any(|v| v == "to"));
2663
2664 let kq = knowledge_query();
2665 assert_eq!(kq.name, "knowledge_query");
2666 let required = kq.input_schema["required"].as_array().unwrap();
2667 assert!(required.iter().any(|v| v == "query"));
2668 }
2669
2670 #[test]
2671 fn test_agent_tools_definitions() {
2672 let spawn = agent_spawn();
2673 assert_eq!(spawn.name, "agent_spawn");
2674 assert_eq!(spawn.category, ToolCategory::Agent);
2675 let required = spawn.input_schema["required"].as_array().unwrap();
2676 assert!(required.iter().any(|v| v == "name"));
2677 assert!(required.iter().any(|v| v == "system_prompt"));
2678
2679 let msg = agent_message();
2680 assert_eq!(msg.name, "agent_message");
2681 assert_eq!(msg.category, ToolCategory::Agent);
2682 let required = msg.input_schema["required"].as_array().unwrap();
2683 assert!(required.iter().any(|v| v == "message"));
2684
2685 let list = agent_list();
2686 assert_eq!(list.name, "agent_list");
2687 assert_eq!(list.category, ToolCategory::Agent);
2688 }
2689
2690 #[test]
2691 fn test_git_tools_definitions() {
2692 let status = git_status();
2693 assert_eq!(status.name, "git_status");
2694 assert_eq!(status.category, ToolCategory::SourceControl);
2695
2696 let diff = git_diff();
2697 assert_eq!(diff.name, "git_diff");
2698
2699 let log = git_log();
2700 assert_eq!(log.name, "git_log");
2701
2702 let commit = git_commit();
2703 assert_eq!(commit.name, "git_commit");
2704 let required = commit.input_schema["required"].as_array().unwrap();
2705 assert!(required.iter().any(|v| v == "message"));
2706
2707 let branch = git_branch();
2708 assert_eq!(branch.name, "git_branch");
2709 }
2710
2711 #[test]
2712 fn test_docker_tools_definitions() {
2713 let ps = docker_ps();
2714 assert_eq!(ps.name, "docker_ps");
2715 assert_eq!(ps.category, ToolCategory::Container);
2716
2717 let run = docker_run();
2718 assert_eq!(run.name, "docker_run");
2719 let required = run.input_schema["required"].as_array().unwrap();
2720 assert!(required.iter().any(|v| v == "image"));
2721
2722 let build = docker_build();
2723 assert_eq!(build.name, "docker_build");
2724
2725 let logs = docker_logs();
2726 assert_eq!(logs.name, "docker_logs");
2727 let required = logs.input_schema["required"].as_array().unwrap();
2728 assert!(required.iter().any(|v| v == "container"));
2729 }
2730
2731 #[test]
2732 fn test_http_tools_definitions() {
2733 let req = http_request();
2734 assert_eq!(req.name, "http_request");
2735 assert_eq!(req.category, ToolCategory::Web);
2736 let required = req.input_schema["required"].as_array().unwrap();
2737 assert!(required.iter().any(|v| v == "url"));
2738
2739 let post = http_post();
2740 assert_eq!(post.name, "http_post");
2741 let required = post.input_schema["required"].as_array().unwrap();
2742 assert!(required.iter().any(|v| v == "url"));
2743 assert!(required.iter().any(|v| v == "json"));
2744 }
2745
2746 #[test]
2747 fn test_data_tools_definitions() {
2748 let jq = json_query();
2749 assert_eq!(jq.name, "json_query");
2750 assert_eq!(jq.category, ToolCategory::Data);
2751 let required = jq.input_schema["required"].as_array().unwrap();
2752 assert!(required.iter().any(|v| v == "data"));
2753 assert!(required.iter().any(|v| v == "path"));
2754
2755 let jt = json_transform();
2756 assert_eq!(jt.name, "json_transform");
2757 let required = jt.input_schema["required"].as_array().unwrap();
2758 assert!(required.iter().any(|v| v == "data"));
2759
2760 let yp = yaml_parse();
2761 assert_eq!(yp.name, "yaml_parse");
2762 let required = yp.input_schema["required"].as_array().unwrap();
2763 assert!(required.iter().any(|v| v == "content"));
2764
2765 let rm = regex_match();
2766 assert_eq!(rm.name, "regex_match");
2767 let required = rm.input_schema["required"].as_array().unwrap();
2768 assert!(required.iter().any(|v| v == "pattern"));
2769 assert!(required.iter().any(|v| v == "text"));
2770
2771 let rr = regex_replace();
2772 assert_eq!(rr.name, "regex_replace");
2773 let required = rr.input_schema["required"].as_array().unwrap();
2774 assert!(required.iter().any(|v| v == "pattern"));
2775 assert!(required.iter().any(|v| v == "replacement"));
2776 assert!(required.iter().any(|v| v == "text"));
2777 }
2778
2779 #[test]
2780 fn test_process_tools_definitions() {
2781 let pl = process_list();
2782 assert_eq!(pl.name, "process_list");
2783 assert_eq!(pl.category, ToolCategory::Shell);
2784
2785 let pk = process_kill();
2786 assert_eq!(pk.name, "process_kill");
2787 let required = pk.input_schema["required"].as_array().unwrap();
2788 assert!(required.iter().any(|v| v == "pid"));
2789 }
2790
2791 #[test]
2792 fn test_schedule_tools_definitions() {
2793 let st = schedule_task();
2794 assert_eq!(st.name, "schedule_task");
2795 assert_eq!(st.category, ToolCategory::Schedule);
2796 let required = st.input_schema["required"].as_array().unwrap();
2797 assert!(required.iter().any(|v| v == "name"));
2798 assert!(required.iter().any(|v| v == "command"));
2799 assert!(required.iter().any(|v| v == "delay_secs"));
2800
2801 let sl = schedule_list();
2802 assert_eq!(sl.name, "schedule_list");
2803
2804 let sc = schedule_cancel();
2805 assert_eq!(sc.name, "schedule_cancel");
2806 let required = sc.input_schema["required"].as_array().unwrap();
2807 assert!(required.iter().any(|v| v == "task_id"));
2808 }
2809
2810 #[test]
2811 fn test_code_analysis_tools_definitions() {
2812 let cs = code_search();
2813 assert_eq!(cs.name, "code_search");
2814 assert_eq!(cs.category, ToolCategory::CodeAnalysis);
2815 let required = cs.input_schema["required"].as_array().unwrap();
2816 assert!(required.iter().any(|v| v == "pattern"));
2817
2818 let sym = code_symbols();
2819 assert_eq!(sym.name, "code_symbols");
2820 let required = sym.input_schema["required"].as_array().unwrap();
2821 assert!(required.iter().any(|v| v == "path"));
2822 }
2823
2824 #[test]
2825 fn test_archive_tools_definitions() {
2826 let ac = archive_create();
2827 assert_eq!(ac.name, "archive_create");
2828 assert_eq!(ac.category, ToolCategory::Archive);
2829 let required = ac.input_schema["required"].as_array().unwrap();
2830 assert!(required.iter().any(|v| v == "output_path"));
2831 assert!(required.iter().any(|v| v == "paths"));
2832
2833 let ae = archive_extract();
2834 assert_eq!(ae.name, "archive_extract");
2835 let required = ae.input_schema["required"].as_array().unwrap();
2836 assert!(required.iter().any(|v| v == "archive_path"));
2837 assert!(required.iter().any(|v| v == "destination"));
2838
2839 let al = archive_list();
2840 assert_eq!(al.name, "archive_list");
2841 let required = al.input_schema["required"].as_array().unwrap();
2842 assert!(required.iter().any(|v| v == "archive_path"));
2843 }
2844
2845 #[test]
2846 fn test_template_render_definition() {
2847 let t = template_render();
2848 assert_eq!(t.name, "template_render");
2849 assert_eq!(t.category, ToolCategory::Template);
2850 let required = t.input_schema["required"].as_array().unwrap();
2851 assert!(required.iter().any(|v| v == "template"));
2852 assert!(required.iter().any(|v| v == "variables"));
2853 }
2854
2855 #[test]
2856 fn test_crypto_tools_definitions() {
2857 let hc = hash_compute();
2858 assert_eq!(hc.name, "hash_compute");
2859 assert_eq!(hc.category, ToolCategory::Crypto);
2860
2861 let hv = hash_verify();
2862 assert_eq!(hv.name, "hash_verify");
2863 let required = hv.input_schema["required"].as_array().unwrap();
2864 assert!(required.iter().any(|v| v == "expected"));
2865 }
2866
2867 #[test]
2868 fn test_env_tools_definitions() {
2869 let eg = env_get();
2870 assert_eq!(eg.name, "env_get");
2871 assert_eq!(eg.category, ToolCategory::Shell);
2872 let required = eg.input_schema["required"].as_array().unwrap();
2873 assert!(required.iter().any(|v| v == "name"));
2874
2875 let el = env_list();
2876 assert_eq!(el.name, "env_list");
2877 }
2878
2879 #[test]
2880 fn test_text_tools_definitions() {
2881 let td = text_diff();
2882 assert_eq!(td.name, "text_diff");
2883 assert_eq!(td.category, ToolCategory::Data);
2884 let required = td.input_schema["required"].as_array().unwrap();
2885 assert!(required.iter().any(|v| v == "old_text"));
2886 assert!(required.iter().any(|v| v == "new_text"));
2887
2888 let tc = text_count();
2889 assert_eq!(tc.name, "text_count");
2890 let required = tc.input_schema["required"].as_array().unwrap();
2891 assert!(required.iter().any(|v| v == "text"));
2892 }
2893
2894 #[test]
2899 fn test_tools_for_file_read_capability() {
2900 let caps = vec![Capability::FileRead("**".into())];
2901 let tools = tools_for_capabilities(&caps);
2902 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2903 assert!(names.contains(&"file_read"));
2904 assert!(names.contains(&"file_list"));
2905 assert!(names.contains(&"file_search"));
2906 assert!(names.contains(&"file_info"));
2907 assert!(!names.contains(&"file_write"));
2908 }
2909
2910 #[test]
2911 fn test_tools_for_file_write_capability() {
2912 let caps = vec![Capability::FileWrite("**".into())];
2913 let tools = tools_for_capabilities(&caps);
2914 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2915 assert!(names.contains(&"file_write"));
2916 assert!(names.contains(&"patch_apply"));
2917 assert!(!names.contains(&"file_read"));
2918 }
2919
2920 #[test]
2921 fn test_tools_for_shell_exec_capability() {
2922 let caps = vec![Capability::ShellExec("*".into())];
2923 let tools = tools_for_capabilities(&caps);
2924 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2925 assert!(names.contains(&"shell_exec"));
2926 assert!(names.contains(&"process_list"));
2927 assert!(names.contains(&"process_kill"));
2928 assert!(names.contains(&"env_get"));
2929 assert!(names.contains(&"env_list"));
2930 }
2931
2932 #[test]
2933 fn test_tools_for_network_capability() {
2934 let caps = vec![Capability::Network("*".into())];
2935 let tools = tools_for_capabilities(&caps);
2936 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2937 assert!(names.contains(&"web_fetch"));
2938 assert!(names.contains(&"web_search"));
2939 assert!(names.contains(&"http_request"));
2940 assert!(names.contains(&"http_post"));
2941 }
2942
2943 #[test]
2944 fn test_tools_for_memory_capability() {
2945 let caps = vec![Capability::Memory];
2946 let tools = tools_for_capabilities(&caps);
2947 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2948 assert!(names.contains(&"memory_store"));
2949 assert!(names.contains(&"memory_recall"));
2950 assert_eq!(names.len(), 2);
2951 }
2952
2953 #[test]
2954 fn test_tools_for_knowledge_graph_capability() {
2955 let caps = vec![Capability::KnowledgeGraph];
2956 let tools = tools_for_capabilities(&caps);
2957 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2958 assert!(names.contains(&"knowledge_add_entity"));
2959 assert!(names.contains(&"knowledge_add_relation"));
2960 assert!(names.contains(&"knowledge_query"));
2961 assert_eq!(names.len(), 3);
2962 }
2963
2964 #[test]
2965 fn test_tools_for_agent_spawn_capability() {
2966 let caps = vec![Capability::AgentSpawn];
2967 let tools = tools_for_capabilities(&caps);
2968 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2969 assert!(names.contains(&"agent_spawn"));
2970 assert_eq!(names.len(), 1);
2971 }
2972
2973 #[test]
2974 fn test_tools_for_agent_message_capability() {
2975 let caps = vec![Capability::AgentMessage];
2976 let tools = tools_for_capabilities(&caps);
2977 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2978 assert!(names.contains(&"agent_message"));
2979 assert!(names.contains(&"agent_list"));
2980 assert_eq!(names.len(), 2);
2981 }
2982
2983 #[test]
2984 fn test_tools_for_source_control_capability() {
2985 let caps = vec![Capability::SourceControl];
2986 let tools = tools_for_capabilities(&caps);
2987 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2988 assert!(names.contains(&"git_status"));
2989 assert!(names.contains(&"git_diff"));
2990 assert!(names.contains(&"git_log"));
2991 assert!(names.contains(&"git_commit"));
2992 assert!(names.contains(&"git_branch"));
2993 assert_eq!(names.len(), 5);
2994 }
2995
2996 #[test]
2997 fn test_tools_for_container_capability() {
2998 let caps = vec![Capability::Container];
2999 let tools = tools_for_capabilities(&caps);
3000 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3001 assert!(names.contains(&"docker_ps"));
3002 assert!(names.contains(&"docker_run"));
3003 assert!(names.contains(&"docker_build"));
3004 assert!(names.contains(&"docker_logs"));
3005 assert_eq!(names.len(), 4);
3006 }
3007
3008 #[test]
3009 fn test_tools_for_data_manipulation_capability() {
3010 let caps = vec![Capability::DataManipulation];
3011 let tools = tools_for_capabilities(&caps);
3012 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3013 assert!(names.contains(&"json_query"));
3014 assert!(names.contains(&"json_transform"));
3015 assert!(names.contains(&"yaml_parse"));
3016 assert!(names.contains(&"regex_match"));
3017 assert!(names.contains(&"regex_replace"));
3018 assert!(names.contains(&"text_diff"));
3019 assert!(names.contains(&"text_count"));
3020 assert_eq!(names.len(), 7);
3021 }
3022
3023 #[test]
3024 fn test_tools_for_schedule_capability() {
3025 let caps = vec![Capability::Schedule];
3026 let tools = tools_for_capabilities(&caps);
3027 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3028 assert!(names.contains(&"schedule_task"));
3029 assert!(names.contains(&"schedule_list"));
3030 assert!(names.contains(&"schedule_cancel"));
3031 assert_eq!(names.len(), 3);
3032 }
3033
3034 #[test]
3035 fn test_tools_for_code_analysis_capability() {
3036 let caps = vec![Capability::CodeAnalysis];
3037 let tools = tools_for_capabilities(&caps);
3038 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3039 assert!(names.contains(&"code_search"));
3040 assert!(names.contains(&"code_symbols"));
3041 assert_eq!(names.len(), 2);
3042 }
3043
3044 #[test]
3045 fn test_tools_for_archive_capability() {
3046 let caps = vec![Capability::Archive];
3047 let tools = tools_for_capabilities(&caps);
3048 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3049 assert!(names.contains(&"archive_create"));
3050 assert!(names.contains(&"archive_extract"));
3051 assert!(names.contains(&"archive_list"));
3052 assert_eq!(names.len(), 3);
3053 }
3054
3055 #[test]
3056 fn test_tools_for_template_capability() {
3057 let caps = vec![Capability::Template];
3058 let tools = tools_for_capabilities(&caps);
3059 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3060 assert!(names.contains(&"template_render"));
3061 assert_eq!(names.len(), 1);
3062 }
3063
3064 #[test]
3065 fn test_tools_for_crypto_capability() {
3066 let caps = vec![Capability::Crypto];
3067 let tools = tools_for_capabilities(&caps);
3068 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3069 assert!(names.contains(&"hash_compute"));
3070 assert!(names.contains(&"hash_verify"));
3071 assert_eq!(names.len(), 2);
3072 }
3073
3074 #[test]
3075 fn test_tools_for_empty_capabilities() {
3076 let caps: Vec<Capability> = vec![];
3077 let tools = tools_for_capabilities(&caps);
3078 assert!(tools.is_empty());
3079 }
3080
3081 #[test]
3082 fn test_tools_for_event_publish_returns_empty() {
3083 let caps = vec![Capability::EventPublish];
3085 let tools = tools_for_capabilities(&caps);
3086 assert!(tools.is_empty());
3087 }
3088
3089 #[test]
3094 fn test_push_unique_dedup() {
3095 let mut tools = Vec::new();
3096 push_unique(&mut tools, file_read());
3097 push_unique(&mut tools, file_read());
3098 push_unique(&mut tools, file_read());
3099 assert_eq!(tools.len(), 1);
3100 }
3101
3102 #[test]
3103 fn test_push_unique_different_tools() {
3104 let mut tools = Vec::new();
3105 push_unique(&mut tools, file_read());
3106 push_unique(&mut tools, file_write());
3107 push_unique(&mut tools, shell_exec());
3108 assert_eq!(tools.len(), 3);
3109 }
3110
3111 #[test]
3112 fn test_tools_for_multiple_capabilities_dedup() {
3113 let caps = vec![
3115 Capability::FileRead("src/**".into()),
3116 Capability::FileRead("tests/**".into()),
3117 ];
3118 let tools = tools_for_capabilities(&caps);
3119 let file_read_count = tools.iter().filter(|t| t.name == "file_read").count();
3120 assert_eq!(file_read_count, 1);
3121 }
3122
3123 #[test]
3128 fn test_all_tools_count() {
3129 let tools = all_tools();
3130 assert!(
3132 tools.len() >= 50,
3133 "expected at least 50 tools, got {}",
3134 tools.len()
3135 );
3136 }
3137
3138 #[test]
3139 fn test_all_tools_unique_names() {
3140 let tools = all_tools();
3141 let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3142 let original_len = names.len();
3143 names.sort();
3144 names.dedup();
3145 assert_eq!(names.len(), original_len, "all_tools has duplicate names");
3146 }
3147
3148 #[test]
3149 fn test_all_tools_valid_schemas() {
3150 let tools = all_tools();
3151 for tool in &tools {
3152 assert_eq!(
3153 tool.input_schema["type"], "object",
3154 "tool {} has non-object schema",
3155 tool.name
3156 );
3157 assert!(!tool.name.is_empty(), "tool has empty name");
3158 assert!(
3159 !tool.description.is_empty(),
3160 "tool {} has empty description",
3161 tool.name
3162 );
3163 }
3164 }
3165
3166 #[test]
3167 fn test_a2a_delegate_tool_definition() {
3168 let tool = a2a_delegate();
3169 assert_eq!(tool.name, "a2a_delegate");
3170 assert_eq!(tool.category, ToolCategory::Agent);
3171 let required = tool.input_schema["required"]
3172 .as_array()
3173 .expect("required should be array");
3174 assert!(required.iter().any(|v| v == "agent_url"));
3175 assert!(required.iter().any(|v| v == "prompt"));
3176 }
3177
3178 #[test]
3179 fn test_tools_for_a2a_delegate_capability() {
3180 let caps = vec![Capability::A2ADelegate];
3181 let tools = tools_for_capabilities(&caps);
3182 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3183 assert!(names.contains(&"a2a_delegate"));
3184 assert_eq!(names.len(), 1);
3185 }
3186
3187 #[test]
3188 fn test_all_tools_includes_a2a_delegate() {
3189 let tools = all_tools();
3190 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3191 assert!(names.contains(&"a2a_delegate"));
3192 }
3193
3194 #[test]
3195 fn test_wasm_invoke_tool_definition() {
3196 let tool = wasm_invoke();
3197 assert_eq!(tool.name, "wasm_invoke");
3198 assert_eq!(tool.category, ToolCategory::Plugin);
3199 let required = tool.input_schema["required"]
3200 .as_array()
3201 .expect("required should be array");
3202 assert!(required.iter().any(|v| v == "plugin"));
3203 assert!(required.iter().any(|v| v == "function"));
3204 }
3205
3206 #[test]
3207 fn test_tools_for_plugin_invoke_capability() {
3208 let caps = vec![Capability::PluginInvoke];
3209 let tools = tools_for_capabilities(&caps);
3210 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3211 assert!(names.contains(&"wasm_invoke"));
3212 assert_eq!(names.len(), 1);
3213 }
3214
3215 #[test]
3216 fn test_all_tools_includes_wasm_invoke() {
3217 let tools = all_tools();
3218 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3219 assert!(names.contains(&"wasm_invoke"));
3220 }
3221
3222 #[test]
3227 fn test_self_config_tools_registered() {
3228 let caps = vec![Capability::SelfConfig];
3229 let tools = tools_for_capabilities(&caps);
3230 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3231 assert!(names.contains(&"heartbeat_add"), "missing heartbeat_add");
3232 assert!(names.contains(&"heartbeat_list"), "missing heartbeat_list");
3233 assert!(
3234 names.contains(&"heartbeat_remove"),
3235 "missing heartbeat_remove"
3236 );
3237 assert!(names.contains(&"creed_view"), "missing creed_view");
3238 assert!(names.contains(&"skill_list"), "missing skill_list");
3239 assert!(
3240 names.contains(&"skill_recommend"),
3241 "missing skill_recommend"
3242 );
3243 assert_eq!(names.len(), 6);
3244 }
3245
3246 #[test]
3247 fn test_self_config_tools_absent_without_capability() {
3248 let caps = vec![Capability::Memory];
3249 let tools = tools_for_capabilities(&caps);
3250 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3251 assert!(!names.contains(&"heartbeat_add"));
3252 assert!(!names.contains(&"creed_view"));
3253 assert!(!names.contains(&"skill_list"));
3254 }
3255
3256 #[test]
3257 fn test_heartbeat_add_definition() {
3258 let t = heartbeat_add();
3259 assert_eq!(t.name, "heartbeat_add");
3260 assert_eq!(t.category, ToolCategory::Agent);
3261 let required = t.input_schema["required"].as_array().unwrap();
3262 assert!(required.iter().any(|v| v == "task"));
3263 assert!(required.iter().any(|v| v == "cadence"));
3264 }
3265
3266 #[test]
3267 fn test_heartbeat_remove_definition() {
3268 let t = heartbeat_remove();
3269 assert_eq!(t.name, "heartbeat_remove");
3270 let required = t.input_schema["required"].as_array().unwrap();
3271 assert!(required.iter().any(|v| v == "index"));
3272 }
3273
3274 #[test]
3275 fn test_skill_recommend_definition() {
3276 let t = skill_recommend();
3277 assert_eq!(t.name, "skill_recommend");
3278 let required = t.input_schema["required"].as_array().unwrap();
3279 assert!(required.iter().any(|v| v == "pack_name"));
3280 }
3281
3282 #[test]
3283 fn test_all_tools_includes_self_config() {
3284 let tools = all_tools();
3285 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3286 assert!(names.contains(&"heartbeat_add"));
3287 assert!(names.contains(&"heartbeat_list"));
3288 assert!(names.contains(&"heartbeat_remove"));
3289 assert!(names.contains(&"creed_view"));
3290 assert!(names.contains(&"skill_list"));
3291 assert!(names.contains(&"skill_recommend"));
3292 }
3293
3294 #[test]
3297 fn test_sys_screenshot_definition() {
3298 let t = sys_screenshot();
3299 assert_eq!(t.name, "sys_screenshot");
3300 assert_eq!(t.category, ToolCategory::SystemAutomation);
3301 assert!(t.input_schema.get("required").is_none());
3303 }
3304
3305 #[test]
3306 fn test_ui_screenshot_definition() {
3307 let t = ui_screenshot();
3308 assert_eq!(t.name, "ui_screenshot");
3309 assert_eq!(t.category, ToolCategory::UiAutomation);
3310 }
3311
3312 #[test]
3313 fn test_app_ocr_definition() {
3314 let t = app_ocr();
3315 assert_eq!(t.name, "app_ocr");
3316 assert_eq!(t.category, ToolCategory::AppIntegration);
3317 let required = t.input_schema["required"].as_array().unwrap();
3318 assert!(required.iter().any(|v| v == "app"));
3319 }
3320
3321 #[test]
3322 fn test_ui_find_elements_definition() {
3323 let t = ui_find_elements();
3324 assert_eq!(t.name, "ui_find_elements");
3325 assert_eq!(t.category, ToolCategory::UiAutomation);
3326 let required = t.input_schema["required"].as_array().unwrap();
3327 assert!(required.iter().any(|v| v == "app"));
3328 }
3329
3330 #[test]
3331 fn test_ui_click_definition() {
3332 let t = ui_click();
3333 assert_eq!(t.name, "ui_click");
3334 assert_eq!(t.category, ToolCategory::UiAutomation);
3335 let required = t.input_schema["required"].as_array().unwrap();
3336 assert!(required.iter().any(|v| v == "element_id"));
3337 }
3338
3339 #[test]
3340 fn test_ui_type_text_definition() {
3341 let t = ui_type_text();
3342 assert_eq!(t.name, "ui_type_text");
3343 assert_eq!(t.category, ToolCategory::UiAutomation);
3344 let required = t.input_schema["required"].as_array().unwrap();
3345 assert!(required.iter().any(|v| v == "element_id"));
3346 assert!(required.iter().any(|v| v == "text"));
3347 }
3348
3349 #[test]
3350 fn test_ui_list_windows_definition() {
3351 let t = ui_list_windows();
3352 assert_eq!(t.name, "ui_list_windows");
3353 assert_eq!(t.category, ToolCategory::UiAutomation);
3354 }
3355
3356 #[test]
3357 fn test_ui_read_attribute_definition() {
3358 let t = ui_read_attribute();
3359 assert_eq!(t.name, "ui_read_attribute");
3360 assert_eq!(t.category, ToolCategory::UiAutomation);
3361 let required = t.input_schema["required"].as_array().unwrap();
3362 assert!(required.iter().any(|v| v == "element_id"));
3363 assert!(required.iter().any(|v| v == "attribute"));
3364 }
3365
3366 #[test]
3367 fn test_all_tools_includes_automation() {
3368 let tools = all_tools();
3369 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3370 assert!(names.contains(&"sys_screenshot"));
3371 assert!(names.contains(&"ui_screenshot"));
3372 assert!(names.contains(&"app_ocr"));
3373 assert!(names.contains(&"ui_find_elements"));
3374 assert!(names.contains(&"ui_click"));
3375 assert!(names.contains(&"ui_type_text"));
3376 assert!(names.contains(&"ui_list_windows"));
3377 assert!(names.contains(&"ui_read_attribute"));
3378 }
3379
3380 #[test]
3381 fn test_tools_for_system_automation_capability() {
3382 let tools = tools_for_capabilities(&[Capability::SystemAutomation]);
3383 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3384 assert!(names.contains(&"sys_screenshot"));
3385 assert!(!names.contains(&"ui_click")); }
3387
3388 #[test]
3389 fn test_tools_for_ui_automation_capability() {
3390 let tools = tools_for_capabilities(&[Capability::UiAutomation("*".to_string())]);
3391 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3392 assert!(names.contains(&"ui_find_elements"));
3393 assert!(names.contains(&"ui_click"));
3394 assert!(names.contains(&"ui_type_text"));
3395 assert!(names.contains(&"ui_list_windows"));
3396 assert!(names.contains(&"ui_read_attribute"));
3397 assert!(names.contains(&"ui_screenshot"));
3398 assert!(!names.contains(&"sys_screenshot")); assert!(!names.contains(&"app_ocr")); }
3401
3402 #[test]
3403 fn test_tools_for_app_integration_capability() {
3404 let tools = tools_for_capabilities(&[Capability::AppIntegration("*".to_string())]);
3405 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3406 assert!(names.contains(&"app_ocr"));
3407 assert!(!names.contains(&"ui_click")); }
3409
3410 #[test]
3411 fn test_automation_tool_no_duplicates() {
3412 let tools = tools_for_capabilities(&[
3414 Capability::SystemAutomation,
3415 Capability::UiAutomation("*".to_string()),
3416 Capability::AppIntegration("*".to_string()),
3417 ]);
3418 let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3419 let before = names.len();
3420 names.sort();
3421 names.dedup();
3422 assert_eq!(before, names.len(), "duplicate tools found");
3423 }
3424
3425 use super::{ToolGroup, ToolSelector};
3430 use punch_types::{Message, Role};
3431
3432 fn full_caps() -> Vec<Capability> {
3433 Capability::full_access()
3434 }
3435
3436 fn msg(role: Role, text: &str) -> Message {
3437 Message::new(role, text)
3438 }
3439
3440 #[test]
3441 fn test_selector_core_tools_always_present() {
3442 let mut sel = ToolSelector::new(&full_caps());
3443 let messages = vec![msg(Role::User, "hello, how are you?")];
3444 let (tools, _) = sel.select_tools(&messages);
3445
3446 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3447 assert!(names.contains(&"file_read"), "file_read missing");
3449 assert!(names.contains(&"shell_exec"), "shell_exec missing");
3450 assert!(names.contains(&"web_fetch"), "web_fetch missing");
3451 assert!(names.contains(&"memory_store"), "memory_store missing");
3452 assert!(
3454 names.contains(&"git_status"),
3455 "git tools should auto-activate for SourceControl capability"
3456 );
3457 assert!(
3458 names.contains(&"docker_ps"),
3459 "docker tools should auto-activate for Container capability"
3460 );
3461 }
3462
3463 #[test]
3464 fn test_selector_no_auto_activate_without_capability() {
3465 let caps = vec![
3467 Capability::FileRead("**".to_string()),
3468 Capability::ShellExec("*".to_string()),
3469 ];
3470 let mut sel = ToolSelector::new(&caps);
3471 let messages = vec![msg(Role::User, "hello")];
3472 let (tools, _) = sel.select_tools(&messages);
3473
3474 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3475 assert!(
3476 !names.contains(&"git_status"),
3477 "git should not auto-activate without SourceControl"
3478 );
3479 assert!(
3480 !names.contains(&"docker_ps"),
3481 "docker should not auto-activate without Container"
3482 );
3483 }
3484
3485 #[test]
3486 fn test_selector_keyword_activates_group() {
3487 let mut sel = ToolSelector::new(&full_caps());
3488 let messages = vec![msg(Role::User, "please commit my changes to git")];
3489 let (tools, _) = sel.select_tools(&messages);
3490
3491 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3492 assert!(
3493 names.contains(&"git_status"),
3494 "git_status should be activated"
3495 );
3496 assert!(
3497 names.contains(&"git_commit"),
3498 "git_commit should be activated"
3499 );
3500 assert!(names.contains(&"git_diff"), "git_diff should be activated");
3501 }
3502
3503 #[test]
3504 fn test_selector_multiple_groups_activate() {
3505 let mut sel = ToolSelector::new(&full_caps());
3506 let messages = vec![msg(
3507 Role::User,
3508 "build the docker image and then commit it to git",
3509 )];
3510 let (tools, _) = sel.select_tools(&messages);
3511
3512 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3513 assert!(names.contains(&"git_status"), "git should activate");
3514 assert!(names.contains(&"docker_build"), "docker should activate");
3515 }
3516
3517 #[test]
3518 fn test_selector_capability_gating() {
3519 let mut sel = ToolSelector::new(&[]);
3521 let messages = vec![msg(Role::User, "git commit and docker build")];
3522 let (tools, _) = sel.select_tools(&messages);
3523
3524 assert!(tools.is_empty(), "no tools without capabilities");
3525 }
3526
3527 #[test]
3528 fn test_selector_monotonic_growth() {
3529 let mut sel = ToolSelector::new(&full_caps());
3530
3531 let msgs1 = vec![msg(Role::User, "commit my changes")];
3533 let (tools1, _) = sel.select_tools(&msgs1);
3534 let count1 = tools1.len();
3535
3536 let msgs2 = vec![
3538 msg(Role::User, "commit my changes"),
3539 msg(Role::Assistant, "done"),
3540 msg(Role::User, "now tell me a joke"),
3541 ];
3542 let (tools2, _) = sel.select_tools(&msgs2);
3543
3544 let names2: Vec<&str> = tools2.iter().map(|t| t.name.as_str()).collect();
3545 assert!(
3546 names2.contains(&"git_status"),
3547 "git tools should persist across turns"
3548 );
3549 assert!(tools2.len() >= count1, "tool count should not decrease");
3550 }
3551
3552 #[test]
3553 fn test_selector_tools_changed_detection() {
3554 let caps = vec![Capability::Archive, Capability::Crypto];
3556 let mut sel = ToolSelector::new(&caps);
3557
3558 let msgs1 = vec![msg(Role::User, "hello")];
3559 let (_, changed1) = sel.select_tools(&msgs1);
3560 assert!(changed1, "first call should report changed");
3562
3563 let (_, changed2) = sel.select_tools(&msgs1);
3565 assert!(!changed2, "no new groups means no change");
3566
3567 let msgs3 = vec![msg(Role::User, "compute sha256 checksum")];
3569 let (_, changed3) = sel.select_tools(&msgs3);
3570 assert!(changed3, "new group activation should report changed");
3571 }
3572
3573 #[test]
3574 fn test_selector_case_insensitive() {
3575 let mut sel = ToolSelector::new(&full_caps());
3576 let messages = vec![msg(Role::User, "GIT COMMIT DOCKER BUILD")];
3577 let (tools, _) = sel.select_tools(&messages);
3578
3579 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3580 assert!(names.contains(&"git_status"), "case insensitive match");
3581 assert!(names.contains(&"docker_ps"), "case insensitive match");
3582 }
3583
3584 #[test]
3585 fn test_selector_scan_window_limited() {
3586 let caps = vec![Capability::Crypto];
3588 let mut sel = ToolSelector::new(&caps);
3589 let messages = vec![
3591 msg(Role::User, "compute sha256 checksum"), msg(Role::Assistant, "done"), msg(Role::User, "ok"), msg(Role::Assistant, "ok"), msg(Role::User, "tell me a joke"), ];
3597 let (tools, _) = sel.select_tools(&messages);
3598
3599 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3600 assert!(
3601 !names.contains(&"hash_compute"),
3602 "old messages outside window should not activate groups"
3603 );
3604 }
3605
3606 #[test]
3607 fn test_selector_no_duplicates() {
3608 let mut sel = ToolSelector::new(&full_caps());
3609 let messages = vec![msg(
3611 Role::User,
3612 "git commit docker json unzip schedule cron browser sha256 plugin a2a knowledge graph code analysis",
3613 )];
3614 let (tools, _) = sel.select_tools(&messages);
3615
3616 let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3617 let before = names.len();
3618 names.sort();
3619 names.dedup();
3620 assert_eq!(before, names.len(), "duplicate tools found in selection");
3621 }
3622
3623 #[test]
3624 fn test_selector_core_count_reasonable() {
3625 let caps = vec![
3627 Capability::FileRead("**".to_string()),
3628 Capability::FileWrite("**".to_string()),
3629 Capability::ShellExec("*".to_string()),
3630 Capability::Network("*".to_string()),
3631 Capability::Memory,
3632 ];
3633 let mut sel = ToolSelector::new(&caps);
3634 let messages = vec![msg(Role::User, "hello")];
3635 let (tools, _) = sel.select_tools(&messages);
3636
3637 assert!(
3639 tools.len() < 25,
3640 "core tools should be under 25, got {}",
3641 tools.len()
3642 );
3643 assert!(
3644 tools.len() >= 10,
3645 "core tools should be at least 10, got {}",
3646 tools.len()
3647 );
3648 }
3649
3650 #[test]
3651 fn test_selector_full_caps_with_auto_activation() {
3652 let mut sel = ToolSelector::new(&full_caps());
3654 let messages = vec![msg(Role::User, "hello")];
3655 let (tools, _) = sel.select_tools(&messages);
3656
3657 assert!(
3659 tools.len() < 60,
3660 "full caps + auto-activation should be under 60, got {}",
3661 tools.len()
3662 );
3663 assert!(
3664 tools.len() >= 30,
3665 "full caps + auto-activation should be at least 30, got {}",
3666 tools.len()
3667 );
3668 }
3669
3670 #[test]
3671 fn test_tool_group_all_covers_every_variant() {
3672 assert_eq!(
3674 ToolGroup::ALL.len(),
3675 17,
3676 "ToolGroup::ALL must match the number of variants"
3677 );
3678 }
3679
3680 #[test]
3681 fn test_tool_group_tools_not_empty() {
3682 for group in ToolGroup::ALL {
3683 let tools = group.tools();
3684 assert!(!tools.is_empty(), "ToolGroup::{:?} has no tools", group);
3685 }
3686 }
3687}