Skip to main content

lash_remote_protocol/protocol/
tools.rs

1#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2pub struct RemoteToolGrant {
3    pub protocol_version: u32,
4    #[serde(default, skip_serializing_if = "Option::is_none")]
5    pub id: Option<String>,
6    pub name: String,
7    #[serde(default, skip_serializing_if = "String::is_empty")]
8    pub description: String,
9    #[serde(default = "default_input_schema")]
10    pub input_schema: serde_json::Value,
11    #[serde(default)]
12    pub output_schema: serde_json::Value,
13    #[serde(default, skip_serializing_if = "Vec::is_empty")]
14    pub input_schema_projections: Vec<RemoteSchemaProjectionOverride>,
15    #[serde(default, skip_serializing_if = "Vec::is_empty")]
16    pub output_schema_projections: Vec<RemoteSchemaProjectionOverride>,
17    #[serde(default, skip_serializing_if = "RemoteToolOutputContract::is_static")]
18    pub output_contract: RemoteToolOutputContract,
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub examples: Vec<String>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub availability: Option<RemoteToolAvailability>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub activation: Option<RemoteToolActivation>,
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub argument_projection: Option<RemoteToolArgumentProjectionPolicy>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub scheduling: Option<RemoteToolScheduling>,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub retry_policy: Option<RemoteToolRetryPolicy>,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub lashlang_binding: Option<RemoteLashlangToolBinding>,
33}
34
35impl RemoteToolGrant {
36    pub fn call_path(&self) -> Result<String, RemoteProtocolError> {
37        let binding = self.required_lashlang_binding()?;
38        Ok(format!(
39            "{}.{}",
40            binding.module_path.join("."),
41            binding.operation
42        ))
43    }
44
45    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
46        ensure_protocol_version(self.protocol_version)?;
47        if self.name.trim().is_empty() {
48            return Err(RemoteProtocolError::InvalidToolGrant {
49                tool_name: self.name.clone(),
50                message: "tool grant name cannot be empty".to_string(),
51            });
52        }
53        self.required_lashlang_binding()?;
54        Ok(())
55    }
56
57    pub fn validate_all(grants: &[Self]) -> Result<(), RemoteProtocolError> {
58        let mut seen = HashSet::new();
59        for grant in grants {
60            grant.validate()?;
61            let call_path = grant.call_path()?;
62            if !seen.insert(call_path.clone()) {
63                return Err(RemoteProtocolError::DuplicateRemoteCallPath { call_path });
64            }
65        }
66        Ok(())
67    }
68
69    fn required_lashlang_binding(&self) -> Result<&RemoteLashlangToolBinding, RemoteProtocolError> {
70        let Some(binding) = &self.lashlang_binding else {
71            return Err(RemoteProtocolError::MissingLashlangToolBinding {
72                tool_name: self.name.clone(),
73            });
74        };
75        if binding.module_path.is_empty() {
76            return Err(RemoteProtocolError::InvalidToolGrant {
77                tool_name: self.name.clone(),
78                message: "remote tool grant requires an explicit module path".to_string(),
79            });
80        }
81        if binding
82            .module_path
83            .iter()
84            .any(|part| part.trim().is_empty())
85        {
86            return Err(RemoteProtocolError::InvalidToolGrant {
87                tool_name: self.name.clone(),
88                message: "remote tool grant module path cannot contain empty segments".to_string(),
89            });
90        }
91        if binding.operation.trim().is_empty() {
92            return Err(RemoteProtocolError::InvalidToolGrant {
93                tool_name: self.name.clone(),
94                message: "remote tool grant requires an explicit operation".to_string(),
95            });
96        }
97        Ok(binding)
98    }
99}
100
101fn default_input_schema() -> serde_json::Value {
102    serde_json::json!({
103        "type": "object",
104        "properties": {},
105        "additionalProperties": true
106    })
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
110pub struct RemoteLashlangToolBinding {
111    pub module_path: Vec<String>,
112    pub operation: String,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub authority_type: Option<String>,
115    #[serde(default, skip_serializing_if = "Vec::is_empty")]
116    pub aliases: Vec<String>,
117}
118
119impl RemoteLashlangToolBinding {
120    pub fn new(
121        module_path: impl IntoIterator<Item = impl Into<String>>,
122        operation: impl Into<String>,
123    ) -> Self {
124        Self {
125            module_path: module_path.into_iter().map(Into::into).collect(),
126            operation: operation.into(),
127            authority_type: None,
128            aliases: Vec::new(),
129        }
130    }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
134pub struct RemoteSchemaProjectionOverride {
135    pub profile: String,
136    pub schema: serde_json::Value,
137}
138
139#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
140#[serde(rename_all = "snake_case")]
141pub enum RemoteToolAvailability {
142    Off,
143    Searchable,
144    Callable,
145    #[default]
146    Showcased,
147}
148
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
150#[serde(rename_all = "snake_case")]
151pub enum RemoteToolActivation {
152    #[default]
153    Always,
154    Internal,
155}
156
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
158#[serde(rename_all = "snake_case")]
159pub enum RemoteToolScheduling {
160    #[default]
161    Parallel,
162    Serial,
163}
164
165#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
166#[serde(tag = "kind", rename_all = "snake_case")]
167pub enum RemoteToolOutputContract {
168    #[default]
169    Static,
170    FromInputSchema {
171        input_field: String,
172        #[serde(default, skip_serializing_if = "Option::is_none")]
173        default_schema: Option<serde_json::Value>,
174    },
175}
176
177impl RemoteToolOutputContract {
178    fn is_static(&self) -> bool {
179        matches!(self, Self::Static)
180    }
181}
182
183#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
184#[serde(tag = "kind", rename_all = "snake_case")]
185pub enum RemoteToolArgumentProjectionPolicy {
186    #[default]
187    MaterializeProjectedValues,
188    PreserveProjectedRefsInField {
189        field: String,
190    },
191}
192
193#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
194#[serde(tag = "type", rename_all = "snake_case")]
195pub enum RemoteToolRetryPolicy {
196    #[default]
197    Never,
198    Safe {
199        max_attempts: u32,
200        base_delay_ms: u64,
201        max_delay_ms: u64,
202    },
203    Idempotent {
204        max_attempts: u32,
205        base_delay_ms: u64,
206        max_delay_ms: u64,
207    },
208}