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}
209
210#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
211pub struct RemoteToolCallRequest {
212 pub protocol_version: u32,
213 pub tool_name: String,
214 pub call_path: String,
215 pub args: serde_json::Value,
216 pub session_id: String,
217 pub completion_key: serde_json::Value,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub tool_call_id: Option<String>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub replay_key: Option<String>,
222 pub attempt_number: u32,
223 pub max_attempts: u32,
224 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
225 pub headers: HashMap<String, String>,
226}
227
228impl RemoteToolCallRequest {
229 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
230 ensure_protocol_version(self.protocol_version)?;
231 if self.tool_name.trim().is_empty() {
232 return Err(RemoteProtocolError::UnknownRemoteTool {
233 tool_name: self.tool_name.clone(),
234 });
235 }
236 if self.call_path.trim().is_empty() {
237 return Err(RemoteProtocolError::RemoteToolTransport(
238 "remote tool call request requires a non-empty call_path".to_string(),
239 ));
240 }
241 if self.session_id.trim().is_empty() {
242 return Err(RemoteProtocolError::RemoteToolTransport(
243 "remote tool call request requires a non-empty session_id".to_string(),
244 ));
245 }
246 if self.completion_key.is_null() {
247 return Err(RemoteProtocolError::RemoteToolTransport(
248 "remote tool call request requires completion_key".to_string(),
249 ));
250 }
251 Ok(())
252 }
253}
254
255#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
256#[serde(rename_all = "snake_case")]
257pub enum RemoteTimeoutBehavior {
258 #[default]
259 ErrorAsResult,
260 FailTurn,
261}
262
263#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
264#[serde(rename_all = "snake_case")]
265pub enum RemoteCancelHint {
266 Ignore,
267 #[default]
268 CancelExternalWork,
269}
270
271#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
272#[serde(tag = "status", rename_all = "snake_case")]
273pub enum RemoteToolCallResponse {
274 Success {
275 protocol_version: u32,
276 #[serde(default)]
277 value: serde_json::Value,
278 },
279 Failure {
280 protocol_version: u32,
281 #[serde(default = "default_failure_code")]
282 code: String,
283 message: String,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 raw: Option<serde_json::Value>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 retry_after_ms: Option<u64>,
288 },
289 Cancelled {
290 protocol_version: u32,
291 message: String,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 raw: Option<serde_json::Value>,
294 },
295 Pending {
296 protocol_version: u32,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 deadline_ms: Option<u64>,
299 #[serde(default)]
300 on_timeout: RemoteTimeoutBehavior,
301 #[serde(default)]
302 on_cancel: RemoteCancelHint,
303 },
304}
305
306impl RemoteToolCallResponse {
307 pub fn protocol_version(&self) -> u32 {
308 match self {
309 Self::Success {
310 protocol_version, ..
311 }
312 | Self::Failure {
313 protocol_version, ..
314 }
315 | Self::Cancelled {
316 protocol_version, ..
317 }
318 | Self::Pending {
319 protocol_version, ..
320 } => *protocol_version,
321 }
322 }
323
324 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
325 ensure_protocol_version(self.protocol_version())
326 }
327}
328
329fn default_failure_code() -> String {
330 "remote_tool_error".to_string()
331}