weavatrix_rust/operations/catalog/
mod.rs1use blazingly_json::{Map, Value, json};
2use serde::Serialize;
3
4mod definitions;
5mod profile;
6mod schema;
7mod validation;
8
9pub use profile::ToolProfile;
10pub(crate) use validation::{reject_unknown_arguments, require_valid_output_format};
11
12#[derive(Debug, Clone, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ToolDefinition {
15 pub name: &'static str,
16 pub description: &'static str,
17 pub input_schema: Value,
18}
19
20#[must_use]
21pub fn catalog() -> Vec<ToolDefinition> {
22 definitions::SPECS
23 .iter()
24 .filter(|spec| capability_is_compiled(spec.name))
25 .map(|spec| tool(spec.name, spec.description, spec.required))
26 .collect()
27}
28
29#[must_use]
30pub fn catalog_for_profile(profile: ToolProfile) -> Vec<ToolDefinition> {
31 catalog()
32 .into_iter()
33 .filter(|tool| profile.allows(tool.name))
34 .collect()
35}
36
37#[allow(clippy::needless_bool)]
38fn capability_is_compiled(tool: &str) -> bool {
39 if tool == "find_duplicates" {
40 cfg!(feature = "clone")
41 } else if matches!(
42 tool,
43 "change_impact"
44 | "git_history"
45 | "git_read_blob"
46 | "cross_repo_git"
47 | "verified_change"
48 | "graph_diff"
49 | "select_tests"
50 | "perf_attribution"
51 ) {
52 cfg!(feature = "git")
53 } else if tool == "search_code" {
54 cfg!(feature = "search")
55 } else if matches!(tool, "semantic_link" | "seo_link_suggestions") {
56 cfg!(feature = "semantic")
57 } else if tool == "vector_search" {
58 cfg!(feature = "vector")
59 } else if tool == "memory_context" {
60 cfg!(feature = "memory")
61 } else {
62 true
63 }
64}
65
66fn tool(
67 tool_name: &'static str,
68 description: &'static str,
69 required: &[&'static str],
70) -> ToolDefinition {
71 let mut properties = Map::from_iter([
72 (
73 "output_format".to_owned(),
74 json!({
75 "type": "string",
76 "enum": ["text", "json", "structured"],
77 "default": "json",
78 "description": "text returns the concise text block only; json returns \
79 structured output and mirrors it into text for clients that \
80 read only content; structured drops that mirror, which is the \
81 larger copy, and is safe only where the client reads \
82 structuredContent"
83 }),
84 ),
85 (
86 "expected_repository".to_owned(),
87 json!({
88 "type": "string",
89 "description": "Path or folder name of the repository this call is about; \
90 the call fails instead of answering when the active \
91 repository differs. Every answer also carries a \
92 repository_context block naming its root, revision and \
93 graph age."
94 }),
95 ),
96 ]);
97 for name in schema::optional_fields(tool_name) {
98 properties.insert((*name).to_owned(), schema::field_schema(tool_name, name));
99 }
100 for name in required {
101 let schema = schema::field_schema(tool_name, name);
102 properties.insert((*name).to_owned(), schema);
103 }
104 let required = required
105 .iter()
106 .map(|value| json!(value))
107 .collect::<Vec<_>>();
108 ToolDefinition {
109 name: tool_name,
110 description,
111 input_schema: json!({
112 "type": "object",
113 "additionalProperties": true,
114 "properties": properties,
115 "required": required
116 }),
117 }
118}