elicitation 0.10.0

Conversational elicitation of strongly-typed Rust values via MCP
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! Communication abstraction for elicitation.
//!
//! This module provides the `ElicitCommunicator` trait which abstracts over
//! client-side and server-side elicitation contexts. Both `ElicitClient` and
//! `ElicitServer` implement this trait, allowing the `Elicitation` trait to
//! work with either context seamlessly.

use crate::{ElicitError, ElicitErrorKind, ElicitResult, Elicitation, StyleMarker, TypeMetadata};
use std::any::TypeId;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};

/// Abstraction for elicitation communication.
///
/// This trait provides a unified interface for both client-side and server-side
/// elicitation. Implementations handle the details of sending prompts and
/// receiving responses in their respective contexts.
///
/// # Implementors
///
/// - `ElicitClient` - Client-side communication via `Peer<RoleClient>`
/// - `ElicitServer` - Server-side communication via `Peer<RoleServer>`
pub trait ElicitCommunicator: Clone + Send + Sync {
    /// Send a prompt and receive a text response.
    ///
    /// The implementation handles the details of formatting the prompt,
    /// sending it via MCP, and extracting the text response.
    ///
    /// # Arguments
    ///
    /// * `prompt` - The prompt text to send
    ///
    /// # Returns
    ///
    /// Returns the response text on success, or an error if communication fails.
    fn send_prompt(
        &self,
        prompt: &str,
    ) -> impl std::future::Future<Output = ElicitResult<String>> + Send;

    /// Call an MCP tool directly with given parameters.
    ///
    /// This is a low-level method used by validation types that need specific
    /// tool interactions beyond generic text prompts.
    ///
    /// # Arguments
    ///
    /// * `params` - The tool call parameters
    ///
    /// # Returns
    ///
    /// Returns the tool call result or an error.
    fn call_tool(
        &self,
        params: rmcp::model::CallToolRequestParams,
    ) -> impl std::future::Future<
        Output = Result<rmcp::model::CallToolResult, rmcp::service::ServiceError>,
    > + Send;

    /// Get the style context for type-specific styles.
    ///
    /// The style context maintains custom style selections for different types,
    /// allowing each type to have its own style independently.
    fn style_context(&self) -> &StyleContext;

    /// Create a new communicator with a style added for a specific type.
    ///
    /// Returns a new communicator with the style in the context. The original
    /// communicator is unchanged.
    ///
    /// # Type Parameters
    ///
    /// * `T` - The type to set the style for
    /// * `S` - The style type (must implement [`StyleMarker`] and
    ///   [`style::ElicitationStyle`](crate::style::ElicitationStyle))
    fn with_style<T: 'static, S: StyleMarker + crate::style::ElicitationStyle + 'static>(
        &self,
        style: S,
    ) -> Self;

    /// Get the current style for a type, or use default if not set.
    ///
    /// This method checks if a custom style was set via `with_style()`.
    /// If found, returns that style. Otherwise, returns `T::Style::default()`.
    ///
    /// # Errors
    ///
    /// Returns an error if the style context lock is poisoned.
    fn style_or_default<T: Elicitation + 'static>(&self) -> ElicitResult<T::Style>
    where
        T::Style: StyleMarker,
    {
        Ok(self
            .style_context()
            .get_style::<T, T::Style>()?
            .unwrap_or_default())
    }

    /// Get the current style for a type, eliciting if not set.
    ///
    /// This method checks if a custom style was set via `with_style()`.
    /// If found, returns that style. Otherwise, elicits the style from
    /// the user/client.
    ///
    /// This enables "auto-selection": styles are only elicited when needed.
    fn style_or_elicit<T: Elicitation + 'static>(
        &self,
    ) -> impl std::future::Future<Output = ElicitResult<T::Style>> + Send
    where
        T::Style: StyleMarker,
    {
        async move {
            if let Some(style) = self.style_context().get_style::<T, T::Style>()? {
                Ok(style)
            } else {
                T::Style::elicit(self).await
            }
        }
    }

    /// Get the elicitation context for introspection.
    ///
    /// The elicitation context tracks the current chain of nested elicitations,
    /// enabling observability without storing full history.
    fn elicitation_context(&self) -> &ElicitationContext;

    /// Get the metadata for the currently elicited type.
    ///
    /// Returns `None` if no elicitation is in progress (e.g., at the top level
    /// before any elicitation starts).
    ///
    /// # Errors
    ///
    /// Returns an error if the elicitation context lock is poisoned.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// // In a traced function
    /// if let Some(meta) = communicator.current_type()? {
    ///     tracing::info!(
    ///         type_name = %meta.type_name,
    ///         pattern = ?meta.pattern(),
    ///         "Eliciting type"
    ///     );
    /// }
    /// ```
    fn current_type(&self) -> ElicitResult<Option<TypeMetadata>> {
        self.elicitation_context().current()
    }

    /// Get the current elicitation depth.
    ///
    /// Returns:
    /// - `0` if at the top level (before any elicitation)
    /// - `1` if eliciting a top-level type
    /// - `2` if eliciting a field of a struct, etc.
    ///
    /// # Errors
    ///
    /// Returns an error if the elicitation context lock is poisoned.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let depth = communicator.current_depth()?;
    /// tracing::debug!(depth, "Elicitation depth");
    /// ```
    fn current_depth(&self) -> ElicitResult<usize> {
        self.elicitation_context().depth()
    }

    /// Get a snapshot of the full elicitation stack.
    ///
    /// Returns the complete chain from root to current type.
    /// Useful for detailed logging or debugging.
    ///
    /// # Errors
    ///
    /// Returns an error if the elicitation context lock is poisoned.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// for meta in communicator.elicitation_stack()? {
    ///     println!("  {}", meta.type_name);
    ///     }
    /// ```
    fn elicitation_stack(&self) -> ElicitResult<Vec<TypeMetadata>> {
        self.elicitation_context().stack()
    }
}

/// Internal trait for type-erased style storage.
///
/// Combines `Any` (for concrete type downcasting) with
/// [`style::ElicitationStyle`](crate::style::ElicitationStyle)
/// (for dynamic prompt generation). This allows stored styles to be both
/// retrieved as concrete types via `get_style` and used dynamically via
/// `prompt_for_type` without knowing the concrete type.
trait StyleEntry: Send + Sync {
    /// Downcast support for concrete type recovery.
    fn as_any(&self) -> &dyn std::any::Any;

    /// Dynamic access to the style's prompt generation.
    fn as_style(&self) -> &dyn crate::style::ElicitationStyle;
}

impl<T: crate::style::ElicitationStyle + 'static> StyleEntry for T {
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }

    fn as_style(&self) -> &dyn crate::style::ElicitationStyle {
        self
    }
}

/// Storage for type-specific styles.
///
/// Uses `TypeId` to store different style enums for different types.
/// This allows each type to have its own style selection without interference.
/// Internally uses `Arc<RwLock<_>>` for efficient cloning.
#[derive(Clone, Default)]
pub struct StyleContext {
    styles: Arc<RwLock<HashMap<TypeId, Box<dyn StyleEntry>>>>,
}

impl StyleContext {
    /// Set a custom style for a specific type.
    ///
    /// Accepts any style type S that implements [`StyleMarker`] and
    /// [`style::ElicitationStyle`](crate::style::ElicitationStyle).
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    #[tracing::instrument(skip(self, style), level = "debug", fields(type_id = ?TypeId::of::<T>()))]
    pub fn set_style<T: 'static, S: StyleMarker + crate::style::ElicitationStyle + 'static>(
        &mut self,
        style: S,
    ) -> ElicitResult<()> {
        let type_id = TypeId::of::<T>();
        let mut styles = self.styles.write().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "StyleContext lock poisoned: {}",
                e
            )))
        })?;
        styles.insert(type_id, Box::new(style));
        Ok(())
    }

    /// Get the custom style for a specific type, if one was set.
    ///
    /// Returns None if no custom style was provided, allowing
    /// fallback to T::Style::default().
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    #[tracing::instrument(skip(self), level = "debug", fields(type_id = ?TypeId::of::<T>()))]
    pub fn get_style<T: 'static, S: StyleMarker>(&self) -> ElicitResult<Option<S>> {
        let type_id = TypeId::of::<T>();
        let styles = self.styles.read().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "StyleContext lock poisoned: {}",
                e
            )))
        })?;
        Ok(styles
            .get(&type_id)
            .and_then(|entry| entry.as_any().downcast_ref::<S>())
            .cloned())
    }

    /// Generate a prompt for a type using its stored custom style.
    ///
    /// Returns `None` if no custom style was set for this type, allowing
    /// callers to fall back to the default `Prompt::prompt()` string.
    /// This enables primitive types (u64, i32, etc.) to respect styles
    /// set via `with_style` without knowing the concrete style type.
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    #[tracing::instrument(skip(self), level = "debug", fields(type_id = ?TypeId::of::<T>()))]
    pub fn prompt_for_type<T: 'static>(
        &self,
        field_name: &str,
        field_type: &str,
        context: &crate::style::PromptContext,
    ) -> ElicitResult<Option<String>> {
        let type_id = TypeId::of::<T>();
        let styles = self.styles.read().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "StyleContext lock poisoned: {}",
                e
            )))
        })?;
        Ok(styles.get(&type_id).map(|entry| {
            entry
                .as_style()
                .prompt_for_field(field_name, field_type, context)
        }))
    }
}

/// Storage for current elicitation context (for observability).
///
/// Tracks the current "stack" of types being elicited, allowing introspection
/// of the elicitation state without storing full history. The stack only contains
/// the current chain of nested elicitations, providing O(1) memory per nesting level.
///
/// # Use Cases
///
/// - **Tracing**: Add type context to OpenTelemetry spans
/// - **Metrics**: Label Prometheus metrics with current type
/// - **Debugging**: Understand elicitation depth and current type
///
/// # Memory Efficiency
///
/// - **O(depth) memory**: Only stores current chain, not history
/// - **No accumulation**: Stack shrinks as elicitations complete
/// - **Stateless metadata**: TypeMetadata contains only static strings
#[derive(Clone, Default)]
pub struct ElicitationContext {
    stack: Arc<RwLock<Vec<TypeMetadata>>>,
}

impl ElicitationContext {
    /// Push a new type onto the elicitation stack.
    ///
    /// Call this when entering a new elicitation. Pair with `pop()` when done.
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    pub fn push(&self, metadata: TypeMetadata) -> ElicitResult<()> {
        let mut stack = self.stack.write().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "ElicitationContext lock poisoned: {}",
                e
            )))
        })?;
        stack.push(metadata.clone());
        tracing::debug!(
            type_name = metadata.type_name,
            depth = stack.len(),
            "Entering elicitation"
        );
        Ok(())
    }

    /// Pop the current type from the elicitation stack.
    ///
    /// Call this when exiting an elicitation.
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    pub fn pop(&self) -> ElicitResult<()> {
        let mut stack = self.stack.write().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "ElicitationContext lock poisoned: {}",
                e
            )))
        })?;
        if let Some(metadata) = stack.pop() {
            tracing::debug!(
                type_name = metadata.type_name,
                depth = stack.len(),
                "Exiting elicitation"
            );
        }
        Ok(())
    }

    /// Get the metadata for the currently elicited type.
    ///
    /// Returns `None` if no elicitation is currently in progress.
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    pub fn current(&self) -> ElicitResult<Option<TypeMetadata>> {
        let stack = self.stack.read().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "ElicitationContext lock poisoned: {}",
                e
            )))
        })?;
        Ok(stack.last().cloned())
    }

    /// Get the current elicitation depth.
    ///
    /// Returns 0 if at the top level, 1 if eliciting a field of a struct, etc.
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    pub fn depth(&self) -> ElicitResult<usize> {
        let stack = self.stack.read().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "ElicitationContext lock poisoned: {}",
                e
            )))
        })?;
        Ok(stack.len())
    }

    /// Get a snapshot of the full elicitation stack.
    ///
    /// Returns a vector of all types in the current chain, from root to current.
    /// Useful for debugging or detailed logging.
    ///
    /// # Errors
    ///
    /// Returns an error if the lock is poisoned.
    pub fn stack(&self) -> ElicitResult<Vec<TypeMetadata>> {
        let stack = self.stack.read().map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "ElicitationContext lock poisoned: {}",
                e
            )))
        })?;
        Ok(stack.clone())
    }
}