elicitation/tool.rs
1//! Contract-based tool system for MCP.
2//!
3//! This module provides **formally verified tool composition** for building
4//! agent programs with compile-time guarantees. Tools are functions with
5//! explicit preconditions and postconditions, enabling type-safe chaining
6//! where the compiler prevents invalid compositions.
7//!
8//! # What Are Tool Contracts?
9//!
10//! Traditional MCP tools are "bag of functions" - you can call them in any order:
11//!
12//! ```text
13//! Traditional: With Contracts:
14//! =========== ==============
15//! validate(x) validate(x) → (x, Proof)
16//! send(x) // Unsafe! send(x, Proof) // Type-checked!
17//! ```
18//!
19//! Tool contracts **track dependencies through the type system**:
20//! - Cannot call `send_email()` without proof of validation
21//! - Cannot chain incompatible tools (compile error)
22//! - Proofs compile away to zero overhead
23//!
24//! # Why This Matters
25//!
26//! In production agent systems, tool chains can fail in subtle ways:
27//!
28//! ```text
29//! FAIL: Agent skips validation step
30//! FAIL: Tools called in wrong order
31//! FAIL: Preconditions not checked
32//! ```
33//!
34//! **With contracts**: All these bugs become compile errors. The type system
35//! enforces correct composition.
36//!
37//! # Quick Start
38//!
39//! ```rust,ignore
40//! use elicitation::tool::{Tool, True, then};
41//! use elicitation::contracts::{Established, Prop};
42//!
43//! // Define your domain propositions
44//! struct EmailValidated;
45//! impl Prop for EmailValidated {}
46//!
47//! // Tool 1: Validates email (no precondition, establishes validation)
48//! struct ValidateTool;
49//! impl Tool for ValidateTool {
50//! type Input = String;
51//! type Output = String;
52//! type Pre = True; // No precondition
53//! type Post = EmailValidated; // Establishes validation
54//!
55//! async fn execute(&self, email: String, _: Established<True>)
56//! -> Result<(String, Established<EmailValidated>), ToolError>
57//! {
58//! if email.contains('@') {
59//! Ok((email, Established::assert()))
60//! } else {
61//! Err(ToolError::validation("Invalid"))
62//! }
63//! }
64//! }
65//!
66//! // Tool 2: Sends email (REQUIRES validation)
67//! struct SendTool;
68//! impl Tool for SendTool {
69//! type Input = String;
70//! type Output = ();
71//! type Pre = EmailValidated; // Requires validated email!
72//! type Post = True;
73//!
74//! async fn execute(&self, email: String, _: Established<EmailValidated>)
75//! -> Result<((), Established<True>), ToolError>
76//! {
77//! println!("Sending to {}", email);
78//! Ok(((), True::axiom()))
79//! }
80//! }
81//!
82//! // Compose: Type system enforces validation happens first
83//! let pipeline = then(ValidateTool, SendTool);
84//! pipeline.execute("user@example.com", True::axiom()).await?;
85//! ```
86//!
87//! # Key Design Principles
88//!
89//! - **Explicit contracts**: Preconditions and postconditions in types
90//! - **Type-safe composition**: Cannot chain incompatible tools
91//! - **Zero runtime cost**: All proofs compile away
92//! - **Formally verified**: Kani proves composition soundness
93//!
94//! # API Overview
95//!
96//! ## Core Types
97//!
98//! - [`Tool`]: Trait for tools with contracts
99//! - [`True`]: The always-true proposition (for tools with no pre/postconditions)
100//!
101//! ## Composition Functions
102//!
103//! - [`then(t1, t2)`]: Sequential composition (output of t1 → input of t2)
104//! - [`both_tools(t1, t2)`]: Parallel composition (both tools execute)
105//!
106//! # Sequential Composition
107//!
108//! Use `then()` to chain tools where output of first becomes input to second:
109//!
110//! ```rust,ignore
111//! // Tool 1: String → ValidatedEmail
112//! // Tool 2: ValidatedEmail → SentConfirmation
113//!
114//! let pipeline = then(validate_tool, send_tool);
115//! // Type system ensures: ValidatedEmail connects the chain
116//! ```
117//!
118//! The compiler enforces:
119//! - Output type of T1 must match input type of T2
120//! - Postcondition of T1 must imply precondition of T2
121//!
122//! If these don't hold: **compile error**, not runtime failure!
123//!
124//! # Parallel Composition
125//!
126//! Use `both_tools()` to run independent tools:
127//!
128//! ```rust,ignore
129//! // Tool 1: Validate email
130//! // Tool 2: Check permissions
131//!
132//! let combined = both_tools(email_tool, permission_tool);
133//! // Result: Both validations complete, proofs combined
134//! ```
135//!
136//! # Real-World Example: Email Workflow
137//!
138//! ```rust,ignore
139//! use elicitation::tool::{Tool, True, then};
140//!
141//! // Step 1: Validate email format
142//! struct ValidateFormat;
143//! impl Tool for ValidateFormat {
144//! type Pre = True;
145//! type Post = FormatValid;
146//! // ...
147//! }
148//!
149//! // Step 2: Check email exists (requires format valid)
150//! struct CheckExists;
151//! impl Tool for CheckExists {
152//! type Pre = FormatValid; // Can't check invalid format!
153//! type Post = EmailExists;
154//! // ...
155//! }
156//!
157//! // Step 3: Send email (requires existence proof)
158//! struct SendEmail;
159//! impl Tool for SendEmail {
160//! type Pre = EmailExists;
161//! type Post = EmailSent;
162//! // ...
163//! }
164//!
165//! // Compose entire workflow - type-checked!
166//! let workflow = then(then(ValidateFormat, CheckExists), SendEmail);
167//! ```
168//!
169//! # Compared to Traditional MCP
170//!
171//! | Aspect | Traditional MCP | With Contracts |
172//! |--------|----------------|----------------|
173//! | **Preconditions** | Documentation only | Type-enforced |
174//! | **Composition** | Runtime validation | Compile-time proof |
175//! | **Tool chains** | Can fail at runtime | Cannot compile if invalid |
176//! | **Safety** | Testing required | Mathematically guaranteed |
177//! | **Cost** | Runtime checks | **Zero** (compile-time only) |
178//!
179//! # Migration Guide
180//!
181//! Existing MCP tools can adopt contracts incrementally:
182//!
183//! ```text
184//! Before:
185//! async fn execute(&self, input: Input) -> Result<Output>
186//!
187//! After (minimal):
188//! impl Tool for MyTool {
189//! type Pre = True; // No precondition (yet)
190//! type Post = True; // No postcondition (yet)
191//! async fn execute(&self, input: Input, _: Established<True>)
192//! -> Result<(Output, Established<True>)>
193//! }
194//!
195//! After (with contracts):
196//! impl Tool for MyTool {
197//! type Pre = InputValidated;
198//! type Post = OutputGenerated;
199//! async fn execute(&self, input: Input, _: Established<InputValidated>)
200//! -> Result<(Output, Established<OutputGenerated>)>
201//! }
202//! ```
203//!
204//! # Formal Verification
205//!
206//! All composition functions are **formally verified with Kani**:
207//!
208//! - ✅ `then()` preserves proofs correctly (183 symbolic checks)
209//! - ✅ `both_tools()` combines proofs soundly (183 checks)
210//! - ✅ Cannot skip validation steps (type system + Kani proof)
211//!
212//! See `src/kani_tests.rs` for complete verification harnesses.
213//!
214//! # When NOT to Use Tool Contracts
215//!
216//! - **Simple tools**: Single operation with no dependencies
217//! - **External integration**: Third-party systems without type support
218//! - **Rapid prototyping**: Add contracts after design stabilizes
219//!
220//! # Further Reading
221//!
222//! - [Core contracts](crate::contracts): Underlying proof system
223//! - [Examples](../../examples): Complete working examples
224//! - [Vision document](../../elicitation_composition_primitives.md): Design philosophy
225
226use crate::{
227 ElicitResult, Elicitation,
228 contracts::{Established, Prop},
229};
230
231/// MCP tool with explicit preconditions and postconditions.
232///
233/// Tools are contract-preserving functions that:
234/// - Require proof of preconditions before execution
235/// - Return proof of postconditions after success
236/// - Cannot be called without establishing prerequisites
237///
238/// # Type Parameters
239///
240/// - `Input`: The input type (must implement `Elicitation`)
241/// - `Output`: The output type
242/// - `Pre`: Precondition proposition (what must be true before)
243/// - `Post`: Postcondition proposition (what's true after success)
244///
245/// # Example
246///
247/// ```rust,ignore
248/// struct ValidateEmailTool;
249///
250/// impl Tool for ValidateEmailTool {
251/// type Input = String;
252/// type Output = String;
253/// type Pre = True; // No precondition
254/// type Post = EmailValidated; // Establishes validation
255///
256/// async fn execute(&self, email: String, _pre: Established<True>)
257/// -> Result<(String, Established<EmailValidated>), ToolError>
258/// {
259/// if email.contains('@') {
260/// Ok((email, Established::assert()))
261/// } else {
262/// Err(ToolError::validation("Invalid email"))
263/// }
264/// }
265/// }
266/// ```
267pub trait Tool {
268 /// Tool input type (must be elicitable).
269 type Input: Elicitation;
270
271 /// Tool output type.
272 type Output;
273
274 /// Precondition proposition (what must be true before calling).
275 type Pre: Prop;
276
277 /// Postcondition proposition (what's true after success).
278 type Post: Prop;
279
280 /// Execute tool with precondition proof, returns output and postcondition proof.
281 ///
282 /// # Arguments
283 ///
284 /// - `input`: The tool input
285 /// - `_pre`: Proof that precondition holds
286 ///
287 /// # Returns
288 ///
289 /// On success: `(output, proof_of_postcondition)`
290 /// On failure: `ToolError`
291 fn execute(
292 &self,
293 input: Self::Input,
294 _pre: Established<Self::Pre>,
295 ) -> impl std::future::Future<Output = ElicitResult<(Self::Output, Established<Self::Post>)>> + Send;
296}
297
298/// Trivially true proposition.
299///
300/// `True` is always established and can be used as a precondition for
301/// unconstrained tools or as a postcondition for tools with no guarantees.
302///
303/// # Example
304///
305/// ```rust
306/// use elicitation::tool::True;
307/// use elicitation::contracts::{Established, Prop};
308///
309/// // Tools with no preconditions use True
310/// let no_constraint: Established<True> = True::axiom();
311///
312/// // Can call anytime
313/// assert_eq!(std::mem::size_of_val(&no_constraint), 0);
314/// ```
315pub struct True;
316
317impl Prop for True {}
318
319impl True {
320 /// Axiom: truth is always established.
321 ///
322 /// This provides a proof of `True` without any preconditions.
323 /// Use this as the precondition proof for unconstrained tools.
324 ///
325 /// # Example
326 ///
327 /// ```rust
328 /// use elicitation::tool::True;
329 ///
330 /// let proof = True::axiom();
331 /// // Can pass this to any tool with Pre = True
332 /// ```
333 #[inline(always)]
334 pub fn axiom() -> Established<True> {
335 Established::assert()
336 }
337}
338
339/// Sequentially compose two tools where first's postcondition implies second's precondition.
340///
341/// Chains tools together: runs the first tool, then uses its output and
342/// postcondition proof to run the second tool. The postcondition of the
343/// first tool must imply the precondition of the second tool.
344///
345/// # Type Parameters
346///
347/// - `T1`: First tool type
348/// - `T2`: Second tool type (input must match T1's output)
349///
350/// # Arguments
351///
352/// - `tool1`: First tool to execute
353/// - `tool2`: Second tool to execute
354/// - `input1`: Input for first tool
355/// - `pre1`: Precondition proof for first tool
356///
357/// # Returns
358///
359/// Tuple of (final output, final postcondition proof)
360///
361/// # Example
362///
363/// ```rust,ignore
364/// use elicitation::tool::{Tool, True, then};
365///
366/// // Chain: validate email then send
367/// let validator = ValidateEmailTool;
368/// let sender = SendEmailTool;
369///
370/// let (result, proof) = then(
371/// &validator,
372/// &sender,
373/// "user@example.com".to_string(),
374/// True::axiom(),
375/// ).await?;
376/// ```
377pub async fn then<T1, T2>(
378 tool1: &T1,
379 tool2: &T2,
380 input1: T1::Input,
381 pre1: Established<T1::Pre>,
382) -> ElicitResult<(T2::Output, Established<T2::Post>)>
383where
384 T1: Tool,
385 T2: Tool<Input = T1::Output>,
386 T1::Post: crate::contracts::Implies<T2::Pre>,
387{
388 let (output1, post1) = tool1.execute(input1, pre1).await?;
389 let pre2 = post1.weaken();
390 let (output2, post2) = tool2.execute(output1, pre2).await?;
391 Ok((output2, post2))
392}
393
394/// Run two tools in parallel and combine their proofs.
395///
396/// Executes both tools concurrently (though this implementation runs
397/// sequentially for now). Requires a proof that both preconditions hold,
398/// and returns both outputs with a proof that both postconditions hold.
399///
400/// # Type Parameters
401///
402/// - `T1`: First tool type
403/// - `T2`: Second tool type
404///
405/// # Arguments
406///
407/// - `tool1`: First tool to execute
408/// - `tool2`: Second tool to execute
409/// - `input1`: Input for first tool
410/// - `input2`: Input for second tool
411/// - `pre`: Proof that both preconditions hold
412///
413/// # Returns
414///
415/// Tuple of ((output1, output2), proof of both postconditions)
416///
417/// # Example
418///
419/// ```rust,ignore
420/// use elicitation::tool::{Tool, both_tools};
421/// use elicitation::contracts::{And, both};
422///
423/// let (outputs, combined_proof) = both_tools(
424/// &tool1,
425/// &tool2,
426/// input1,
427/// input2,
428/// both(pre1, pre2),
429/// ).await?;
430/// ```
431pub async fn both_tools<T1, T2>(
432 tool1: &T1,
433 tool2: &T2,
434 input1: T1::Input,
435 input2: T2::Input,
436 pre: Established<crate::contracts::And<T1::Pre, T2::Pre>>,
437) -> ElicitResult<(
438 (T1::Output, T2::Output),
439 Established<crate::contracts::And<T1::Post, T2::Post>>,
440)>
441where
442 T1: Tool,
443 T2: Tool,
444{
445 use crate::contracts::{both, fst, snd};
446
447 let pre1 = fst(pre);
448 let pre2 = snd(pre);
449
450 let (out1, post1) = tool1.execute(input1, pre1).await?;
451 let (out2, post2) = tool2.execute(input2, pre2).await?;
452
453 Ok(((out1, out2), both(post1, post2)))
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_true_is_zero_sized() {
462 let proof = True::axiom();
463 assert_eq!(std::mem::size_of_val(&proof), 0);
464 }
465
466 #[test]
467 fn test_true_is_copy() {
468 let proof = True::axiom();
469 let proof2 = proof;
470 let _proof3 = proof; // Can still use original
471 let _proof4 = proof2;
472 }
473
474 #[test]
475 fn test_true_axiom_always_succeeds() {
476 // Can call anytime, anywhere
477 let _proof1 = True::axiom();
478 let _proof2 = True::axiom();
479 let _proof3 = True::axiom();
480 }
481}