1use crate::canonical::ToolActionId;
4use crate::id::{ExchangeId, SessionKey, ToolId, ToolName, TransactionId};
5use crate::limits::ToolLimits;
6use serde::{Deserialize, Serialize};
7use std::time::{Duration, Instant};
8use thiserror::Error;
9
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct JsonSchema {
13 schema: serde_json::Value,
14}
15
16impl JsonSchema {
17 pub fn try_new(schema: serde_json::Value) -> Result<Self, ToolContractError> {
19 if !schema.is_object() {
20 return Err(ToolContractError::SchemaNotObject);
21 }
22 Ok(Self { schema })
23 }
24
25 pub fn as_value(&self) -> &serde_json::Value {
27 &self.schema
28 }
29}
30
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33pub enum ToolSuccessContract {
34 Json {
36 schema: JsonSchema,
38 },
39 Text {
41 media_type: String,
43 },
44}
45
46impl ToolSuccessContract {
47 pub fn json(schema: JsonSchema) -> Self {
49 Self::Json { schema }
50 }
51
52 pub fn text(media_type: impl Into<String>) -> Result<Self, ToolContractError> {
54 let media_type = media_type.into();
55 if media_type.is_empty()
56 || media_type.len() > 128
57 || media_type.chars().any(|c| c.is_control())
58 {
59 return Err(ToolContractError::InvalidMediaType);
60 }
61 Ok(Self::Text { media_type })
62 }
63}
64
65#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67pub struct ToolOutputContract {
68 pub success: ToolSuccessContract,
70 pub error_data_schema: Option<JsonSchema>,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub enum ToolCancellationPolicy {
77 Cooperative {
79 grace: Duration,
81 },
82 Abortable,
84 IsolatedKillable {
86 grace: Duration,
88 },
89}
90
91#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
93pub struct ToolSpec {
94 pub id: ToolId,
96 pub name: ToolName,
98 pub description: String,
100 pub input_schema: JsonSchema,
102 pub output_contract: ToolOutputContract,
104 pub limits: ToolLimits,
106 pub cancellation: ToolCancellationPolicy,
108}
109
110impl ToolSpec {
111 pub const MAX_DESCRIPTION_BYTES: usize = 4 * 1024;
113
114 pub fn try_new(
116 id: ToolId,
117 name: ToolName,
118 description: impl Into<String>,
119 input_schema: JsonSchema,
120 output_contract: ToolOutputContract,
121 limits: ToolLimits,
122 cancellation: ToolCancellationPolicy,
123 ) -> Result<Self, ToolContractError> {
124 let description = description.into();
125 if description.len() > Self::MAX_DESCRIPTION_BYTES {
126 return Err(ToolContractError::DescriptionTooLong);
127 }
128 if description.chars().any(|c| c.is_control()) {
129 return Err(ToolContractError::ControlCharacter);
130 }
131 if limits.max_concurrent == 0
132 || limits.max_input_bytes == 0
133 || limits.max_output_bytes == 0
134 || limits.execution_deadline.is_zero()
135 {
136 return Err(ToolContractError::InvalidLimits);
137 }
138 match &cancellation {
139 ToolCancellationPolicy::Cooperative { grace }
140 | ToolCancellationPolicy::IsolatedKillable { grace } => {
141 if grace.is_zero() {
142 return Err(ToolContractError::InvalidCancellationGrace);
143 }
144 }
145 ToolCancellationPolicy::Abortable => {}
146 }
147 Ok(Self {
148 id,
149 name,
150 description,
151 input_schema,
152 output_contract,
153 limits,
154 cancellation,
155 })
156 }
157}
158
159#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
161pub struct ToolCall {
162 pub tool_name: ToolName,
164 pub tool_id: ToolId,
166 pub provider_tool_call_id: String,
168 pub arguments: serde_json::Value,
170 pub request_ordinal: u32,
172}
173
174#[derive(Clone, Debug)]
176pub struct ToolCallContext {
177 pub transaction_id: TransactionId,
179 pub session_key: SessionKey,
181 pub exchange_id: Option<ExchangeId>,
183 pub tool_action_id: ToolActionId,
185 pub tool_id: ToolId,
187 pub deadline: Instant,
189}
190
191#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
193pub enum CanonicalToolOutput {
194 Json(serde_json::Value),
196 Text(String),
198}
199
200#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
202pub struct CanonicalToolError {
203 pub code: String,
205 pub message: String,
207 pub data: Option<serde_json::Value>,
209}
210
211impl CanonicalToolError {
212 pub fn try_new(
214 code: impl Into<String>,
215 message: impl Into<String>,
216 data: Option<serde_json::Value>,
217 max_message_bytes: usize,
218 ) -> Result<Self, ToolContractError> {
219 let code = code.into();
220 let message = message.into();
221 if code.is_empty() || code.len() > 64 || code.chars().any(|c| c.is_control()) {
222 return Err(ToolContractError::InvalidErrorCode);
223 }
224 if message.is_empty()
225 || message.len() > max_message_bytes
226 || message.chars().any(|c| c.is_control())
227 {
228 return Err(ToolContractError::InvalidErrorMessage);
229 }
230 Ok(Self {
231 code,
232 message,
233 data,
234 })
235 }
236}
237
238#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
240pub enum CanonicalToolResultOutcome {
241 Succeeded(CanonicalToolOutput),
243 DomainFailed(CanonicalToolError),
245}
246
247#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
249pub struct CanonicalToolResult {
250 pub transaction_id: TransactionId,
252 pub session_key: SessionKey,
254 pub exchange_id: ExchangeId,
256 pub tool_action_id: ToolActionId,
258 pub tool_id: ToolId,
260 pub provider_tool_call_id: String,
262 pub request_ordinal: u32,
264 pub outcome: CanonicalToolResultOutcome,
266}
267
268#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
270pub enum ToolLifecycleEvent {
271 Started {
273 tool_action_id: ToolActionId,
275 tool_id: ToolId,
277 tool_name: ToolName,
279 provider_tool_call_id: String,
281 request_ordinal: u32,
283 },
284 Completed {
286 result: CanonicalToolResult,
288 },
289 RuntimeFailed {
291 tool_action_id: ToolActionId,
293 tool_id: ToolId,
295 code: String,
297 },
298}
299
300#[derive(Clone, Debug, Error, PartialEq, Eq)]
302pub enum ToolContractError {
303 #[error("JSON schema must be an object")]
305 SchemaNotObject,
306 #[error("tool description exceeds maximum length")]
308 DescriptionTooLong,
309 #[error("tool string must not contain control characters")]
311 ControlCharacter,
312 #[error("tool limits must be non-zero")]
314 InvalidLimits,
315 #[error("cancellation grace must be non-zero")]
317 InvalidCancellationGrace,
318 #[error("invalid media type")]
320 InvalidMediaType,
321 #[error("invalid tool error code")]
323 InvalidErrorCode,
324 #[error("invalid tool error message")]
326 InvalidErrorMessage,
327}
328
329#[derive(Clone, Debug, Error, PartialEq, Eq)]
331pub enum ToolStartError {
332 #[error("tool capacity exceeded")]
334 CapacityExceeded,
335 #[error("tool start rejected: {0}")]
337 Rejected(&'static str),
338}
339
340#[derive(Clone, Debug, Error, PartialEq, Eq)]
342pub enum ToolRuntimeError {
343 #[error("tool panicked")]
345 Panicked,
346 #[error("tool completion lost")]
348 CompletionLost,
349 #[error("tool output contract violated")]
351 OutputContractViolated,
352 #[error("tool termination failed")]
354 TerminationFailed,
355 #[error("tool deadline exceeded")]
357 DeadlineExceeded,
358}
359
360#[derive(Clone, Debug, PartialEq)]
362pub enum ToolCompletion {
363 Succeeded(CanonicalToolOutput),
365 DomainFailed(CanonicalToolError),
367 RuntimeFailed(ToolRuntimeError),
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::id::{ChannelId, SessionId};
375
376 #[test]
377 fn tool_spec_construction() {
378 let schema = JsonSchema::try_new(serde_json::json!({
379 "type": "object",
380 "properties": { "q": { "type": "string" } }
381 }))
382 .unwrap();
383 let out = ToolOutputContract {
384 success: ToolSuccessContract::json(schema.clone()),
385 error_data_schema: None,
386 };
387 let spec = ToolSpec::try_new(
388 ToolId::try_new("search").unwrap(),
389 ToolName::try_new("search").unwrap(),
390 "Search the workspace",
391 schema,
392 out,
393 ToolLimits::default(),
394 ToolCancellationPolicy::Abortable,
395 )
396 .unwrap();
397 assert_eq!(spec.id.as_str(), "search");
398 }
399
400 #[test]
401 fn schema_must_be_object() {
402 assert!(JsonSchema::try_new(serde_json::json!([])).is_err());
403 }
404
405 #[test]
406 fn lifecycle_result_serializes() {
407 let tid = TransactionId::generate();
408 let sk = SessionKey::new(
409 ChannelId::try_new("ch").unwrap(),
410 SessionId::try_new("s").unwrap(),
411 );
412 let result = CanonicalToolResult {
413 transaction_id: tid,
414 session_key: sk,
415 exchange_id: ExchangeId::generate(),
416 tool_action_id: ToolActionId::new("a1"),
417 tool_id: ToolId::try_new("t").unwrap(),
418 provider_tool_call_id: "p1".into(),
419 request_ordinal: 0,
420 outcome: CanonicalToolResultOutcome::Succeeded(CanonicalToolOutput::Text("ok".into())),
421 };
422 let ev = ToolLifecycleEvent::Completed { result };
423 let json = serde_json::to_string(&ev).unwrap();
424 let _back: ToolLifecycleEvent = serde_json::from_str(&json).unwrap();
425 }
426}