Skip to main content

plexus_core/plexus/bidirectional/
mod.rs

1//! Bidirectional streaming support for Plexus RPC
2//!
3//! This module enables **server-to-client requests** during streaming RPC execution,
4//! allowing for interactive workflows like confirmations, prompts, and multi-step wizards.
5//!
6//! # Overview
7//!
8//! Traditional RPC is unidirectional: clients send requests, servers respond. Bidirectional
9//! communication extends this by allowing the **server to request input from the client**
10//! during stream execution. This is essential for:
11//!
12//! - **User confirmations** before destructive operations
13//! - **Interactive prompts** for missing information
14//! - **Multi-step wizards** that guide users through complex workflows
15//! - **Dynamic selection menus** based on server-side state
16//!
17//! # Architecture
18//!
19//! The bidirectional system is built on generic types that can work with any
20//! serializable request/response types:
21//!
22//! ```text
23//! ┌─────────────────────────────────────────────────────────────────────┐
24//! │                        BidirChannel<Req, Resp>                      │
25//! │  Generic channel for type-safe server→client requests              │
26//! └─────────────────────────────────────────────────────────────────────┘
27//!                                    │
28//!                    ┌───────────────┴───────────────┐
29//!                    ▼                               ▼
30//! ┌──────────────────────────────┐   ┌──────────────────────────────┐
31//! │     StandardBidirChannel     │   │   Custom Request/Response    │
32//! │  (confirm/prompt/select)     │   │   (domain-specific types)    │
33//! └──────────────────────────────┘   └──────────────────────────────┘
34//! ```
35//!
36//! ## Wire Format
37//!
38//! Bidirectional requests are sent as `PlexusStreamItem::Request`:
39//!
40//! ```json
41//! {
42//!   "type": "request",
43//!   "requestId": "550e8400-e29b-41d4-a716-446655440000",
44//!   "requestData": { "type": "confirm", "message": "Delete file?" },
45//!   "timeoutMs": 30000
46//! }
47//! ```
48//!
49//! Clients respond via the `_plexus_respond` method or transport-specific mechanism.
50//!
51//! # Core Types
52//!
53//! - [`BidirChannel`] - Generic channel for any request/response types
54//! - [`StandardBidirChannel`] - Type alias for [`BidirChannel<StandardRequest, StandardResponse>`]
55//! - [`StandardRequest`] - Common UI patterns: `Confirm`, `Prompt`, `Select`
56//! - [`StandardResponse`] - Matching responses: `Confirmed`, `Text`, `Selected`, `Cancelled`
57//! - [`SelectOption`] - Option for selection menus
58//! - [`BidirError`] - Error types for bidirectional operations
59//!
60//! # Helper Functions
61//!
62//! - [`TimeoutConfig`] - Timeout presets (quick, normal, patient, extended)
63//! - [`auto_respond_channel`] - Create test channel with automatic responses
64//! - [`auto_confirm_channel`] - Create test channel that auto-confirms
65//! - [`bidir_error_message`] - Get user-friendly error messages
66//!
67//! # Examples
68//!
69//! ## Using StandardBidirChannel (Most Common)
70//!
71//! The `StandardBidirChannel` provides convenience methods for common UI patterns:
72//!
73//! ```rust,ignore
74//! use plexus_core::plexus::bidirectional::{StandardBidirChannel, BidirError, SelectOption};
75//!
76//! async fn my_method(ctx: &StandardBidirChannel) -> Result<(), BidirError> {
77//!     // Simple yes/no confirmation
78//!     if ctx.confirm("Delete this file?").await? {
79//!         // User confirmed - proceed with deletion
80//!     }
81//!
82//!     // Text input prompt
83//!     let name = ctx.prompt("Enter your name:").await?;
84//!     println!("Hello, {}!", name);
85//!
86//!     // Selection from options
87//!     let choices = vec![
88//!         SelectOption::new("dev", "Development")
89//!             .with_description("Local development environment"),
90//!         SelectOption::new("staging", "Staging")
91//!             .with_description("Pre-production testing"),
92//!         SelectOption::new("prod", "Production")
93//!             .with_description("Live environment (requires approval)"),
94//!     ];
95//!     let selected = ctx.select("Choose environment:", choices).await?;
96//!     println!("Selected: {:?}", selected);
97//!
98//!     Ok(())
99//! }
100//! ```
101//!
102//! ## Handling Errors Gracefully
103//!
104//! Always handle bidirectional errors to support non-interactive transports:
105//!
106//! ```rust,ignore
107//! use plexus_core::plexus::bidirectional::{StandardBidirChannel, BidirError, bidir_error_message};
108//!
109//! async fn safe_delete(ctx: &StandardBidirChannel, path: &str) -> Result<bool, String> {
110//!     match ctx.confirm(&format!("Delete '{}'?", path)).await {
111//!         Ok(true) => {
112//!             // User confirmed
113//!             Ok(true)
114//!         }
115//!         Ok(false) => {
116//!             // User declined
117//!             Ok(false)
118//!         }
119//!         Err(BidirError::NotSupported) => {
120//!             // Transport doesn't support bidirectional - skip deletion for safety
121//!             Err("Cannot delete without user confirmation".into())
122//!         }
123//!         Err(BidirError::Cancelled) => {
124//!             // User explicitly cancelled
125//!             Ok(false)
126//!         }
127//!         Err(e) => {
128//!             // Other error - log and return user-friendly message
129//!             Err(bidir_error_message(&e))
130//!         }
131//!     }
132//! }
133//! ```
134//!
135//! ## Using Custom Request/Response Types
136//!
137//! For domain-specific interactions, define custom types:
138//!
139//! ```rust,ignore
140//! use serde::{Deserialize, Serialize};
141//! use schemars::JsonSchema;
142//! use plexus_core::plexus::bidirectional::{BidirChannel, BidirError};
143//!
144//! #[derive(Serialize, Deserialize, JsonSchema)]
145//! #[serde(tag = "type", rename_all = "snake_case")]
146//! enum ImageRequest {
147//!     ConfirmOverwrite { path: String, size: u64 },
148//!     ChooseQuality { min: u8, max: u8, default: u8 },
149//!     SelectFormat { formats: Vec<String> },
150//! }
151//!
152//! #[derive(Serialize, Deserialize, JsonSchema)]
153//! #[serde(tag = "type", rename_all = "snake_case")]
154//! enum ImageResponse {
155//!     Confirmed { value: bool },
156//!     Quality { value: u8 },
157//!     Format { value: String },
158//!     Cancelled,
159//! }
160//!
161//! type ImageBidirChannel = BidirChannel<ImageRequest, ImageResponse>;
162//!
163//! async fn process_image(
164//!     ctx: &ImageBidirChannel,
165//!     path: &str,
166//! ) -> Result<(), BidirError> {
167//!     // Ask for quality
168//!     let quality = ctx.request(ImageRequest::ChooseQuality {
169//!         min: 50, max: 100, default: 85,
170//!     }).await?;
171//!
172//!     if let ImageResponse::Quality { value } = quality {
173//!         println!("Processing with quality: {}", value);
174//!     }
175//!
176//!     Ok(())
177//! }
178//! ```
179//!
180//! ## Testing with Auto-Response Channels
181//!
182//! Use test helpers for deterministic unit tests:
183//!
184//! ```rust,ignore
185//! use plexus_core::plexus::bidirectional::{
186//!     auto_respond_channel, StandardRequest, StandardResponse
187//! };
188//!
189//! #[tokio::test]
190//! async fn test_wizard_flow() {
191//!     let ctx = auto_respond_channel(|req: &StandardRequest| {
192//!         match req {
193//!             StandardRequest::Confirm { .. } => StandardResponse::Confirmed { value: true },
194//!             StandardRequest::Prompt { .. } => StandardResponse::Text { value: "test-value".into() },
195//!             StandardRequest::Select { options, .. } => {
196//!                 StandardResponse::Selected { values: vec![options[0].value.clone()] }
197//!             }
198//!         }
199//!     });
200//!
201//!     // Test your activation with deterministic responses
202//!     let result = ctx.confirm("Test?").await;
203//!     assert_eq!(result.unwrap(), true);
204//! }
205//! ```
206//!
207//! # Transport Support
208//!
209//! Bidirectional communication works differently across transports:
210//!
211//! | Transport  | Mechanism                                           |
212//! |------------|-----------------------------------------------------|
213//! | WebSocket  | Request sent as stream item, response via dedicated call |
214//! | MCP        | Request as logging notification, response via `_plexus_respond` tool |
215//! | HTTP       | Not supported (stateless)                           |
216//!
217//! The `BidirChannel` automatically detects transport capabilities and returns
218//! `BidirError::NotSupported` for transports that cannot handle bidirectional requests.
219
220pub mod channel;
221pub mod helpers;
222pub mod registry;
223pub mod types;
224
225pub use channel::{BidirChannel, BidirWithFallback, StandardBidirChannel};
226pub use helpers::{
227    TimeoutConfig, auto_confirm_channel, auto_respond_channel, bidir_error_message,
228    create_test_bidir_channel, create_test_standard_channel,
229};
230pub use registry::{
231    handle_pending_response, is_request_pending, pending_count, register_pending_request,
232    unregister_pending_request,
233};
234pub use types::{BidirError, SelectOption, StandardRequest, StandardResponse};