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};