streamkit_api/
lib.rs

1// SPDX-FileCopyrightText: © 2025 StreamKit Contributors
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! api: Defines the WebSocket API contract for StreamKit.
6//!
7//! All API communication uses JSON for parameters and payloads.
8//! While pipeline YAML files are still supported internally, the WebSocket API
9//! contract exclusively uses JSON for consistency and TypeScript compatibility.
10
11use serde::{Deserialize, Serialize};
12use ts_rs::TS;
13
14// YAML pipeline format compilation
15pub mod yaml;
16
17// Re-export types so client crates can use them
18pub use streamkit_core::control::{ConnectionMode, NodeControlMessage};
19pub use streamkit_core::{NodeDefinition, NodeState, NodeStats};
20
21// --- Message Types ---
22
23/// The type of WebSocket message being sent or received.
24///
25/// StreamKit uses a request/response pattern with optional events:
26/// - **Request**: Client sends to server with correlation_id
27/// - **Response**: Server replies with matching correlation_id
28/// - **Event**: Server broadcasts to all clients (no correlation_id)
29#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)]
30#[ts(export)]
31#[serde(rename_all = "lowercase")]
32pub enum MessageType {
33    /// Client-initiated request that expects a response
34    Request,
35    /// Server response to a specific request (matched by correlation_id)
36    Response,
37    /// Server-initiated broadcast event (no correlation_id)
38    Event,
39}
40
41// --- Base Message ---
42
43/// Generic WebSocket message container for requests, responses, and events.
44///
45/// # Example (Request)
46/// ```json
47/// {
48///   "type": "request",
49///   "correlation_id": "abc123",
50///   "payload": {
51///     "action": "createsession",
52///     "name": "My Session"
53///   }
54/// }
55/// ```
56///
57/// # Example (Response)
58/// ```json
59/// {
60///   "type": "response",
61///   "correlation_id": "abc123",
62///   "payload": {
63///     "action": "sessioncreated",
64///     "session_id": "sess_xyz",
65///     "name": "My Session"
66///   }
67/// }
68/// ```
69///
70/// # Example (Event)
71/// ```json
72/// {
73///   "type": "event",
74///   "payload": {
75///     "event": "nodestatechanged",
76///     "session_id": "sess_xyz",
77///     "node_id": "gain1",
78///     "state": { "Running": null }
79///   }
80/// }
81/// ```
82#[derive(Serialize, Deserialize, Debug, Clone)]
83pub struct Message<T> {
84    /// The type of message (Request, Response, or Event)
85    #[serde(rename = "type")]
86    pub message_type: MessageType,
87    /// Optional correlation ID for matching requests with responses.
88    /// Present in Request and Response messages, absent in Event messages.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub correlation_id: Option<String>,
91    /// The message payload (RequestPayload, ResponsePayload, or EventPayload)
92    pub payload: T,
93}
94
95// --- Client-to-Server Payloads (Requests) ---
96
97/// Client-to-server request payload types.
98///
99/// All requests should include a correlation_id in the outer Message wrapper
100/// to match responses.
101///
102/// # Session Management
103/// - `CreateSession`: Create a new dynamic pipeline session
104/// - `DestroySession`: Destroy an existing session
105/// - `ListSessions`: List all sessions visible to the current role
106///
107/// # Pipeline Manipulation
108/// - `AddNode`: Add a node to a session's pipeline
109/// - `RemoveNode`: Remove a node from a session's pipeline
110/// - `Connect`: Connect two nodes in a session's pipeline
111/// - `Disconnect`: Disconnect two nodes in a session's pipeline
112/// - `TuneNode`: Send control message to a node (with response)
113/// - `TuneNodeAsync`: Send control message to a node (fire-and-forget)
114///
115/// # Batch Operations
116/// - `ValidateBatch`: Validate multiple operations without applying
117/// - `ApplyBatch`: Apply multiple operations atomically
118///
119/// # Discovery
120/// - `ListNodes`: List all available node types
121/// - `GetPipeline`: Get current pipeline state for a session
122/// - `GetPermissions`: Get current user's permissions
123#[derive(Serialize, Deserialize, Debug, TS)]
124#[ts(export)]
125#[serde(tag = "action")]
126#[serde(rename_all = "lowercase")]
127pub enum RequestPayload {
128    /// Create a new dynamic pipeline session
129    CreateSession {
130        /// Optional session name for identification
131        #[serde(skip_serializing_if = "Option::is_none")]
132        name: Option<String>,
133    },
134    /// Destroy an existing session and clean up resources
135    DestroySession {
136        /// The session ID to destroy
137        session_id: String,
138    },
139    /// List all sessions visible to the current user/role
140    ListSessions,
141    /// List all available node types and their schemas
142    ListNodes,
143    /// Add a node to a session's pipeline
144    AddNode {
145        /// The session ID to add the node to
146        session_id: String,
147        /// Unique identifier for this node instance
148        node_id: String,
149        /// Node type (e.g., "audio::gain", "plugin::native::whisper")
150        kind: String,
151        /// Optional JSON configuration parameters for the node
152        #[serde(skip_serializing_if = "Option::is_none")]
153        #[ts(type = "JsonValue")]
154        params: Option<serde_json::Value>,
155    },
156    /// Remove a node from a session's pipeline
157    RemoveNode {
158        /// The session ID containing the node
159        session_id: String,
160        /// The node ID to remove
161        node_id: String,
162    },
163    /// Connect two nodes in a session's pipeline
164    Connect {
165        /// The session ID containing the nodes
166        session_id: String,
167        /// Source node ID
168        from_node: String,
169        /// Source output pin name
170        from_pin: String,
171        /// Destination node ID
172        to_node: String,
173        /// Destination input pin name
174        to_pin: String,
175        /// Connection mode (reliable or best-effort). Defaults to Reliable.
176        #[serde(default)]
177        mode: ConnectionMode,
178    },
179    /// Disconnect two nodes in a session's pipeline
180    Disconnect {
181        /// The session ID containing the nodes
182        session_id: String,
183        /// Source node ID
184        from_node: String,
185        /// Source output pin name
186        from_pin: String,
187        /// Destination node ID
188        to_node: String,
189        /// Destination input pin name
190        to_pin: String,
191    },
192    /// Send a control message to a node and wait for response
193    TuneNode {
194        /// The session ID containing the node
195        session_id: String,
196        /// The node ID to send the message to
197        node_id: String,
198        /// The control message (UpdateParams, Start, or Shutdown)
199        message: NodeControlMessage,
200    },
201    /// Fire-and-forget version of TuneNode for frequent updates.
202    /// No response is sent, making it suitable for high-frequency parameter updates.
203    TuneNodeAsync {
204        /// The session ID containing the node
205        session_id: String,
206        /// The node ID to send the message to
207        node_id: String,
208        /// The control message (typically UpdateParams)
209        message: NodeControlMessage,
210    },
211    /// Get the current pipeline state for a session
212    GetPipeline {
213        /// The session ID to query
214        session_id: String,
215    },
216    /// Validate a batch of operations without applying them.
217    /// Returns validation errors if any operations would fail.
218    ValidateBatch {
219        /// The session ID to validate operations against
220        session_id: String,
221        /// List of operations to validate
222        operations: Vec<BatchOperation>,
223    },
224    /// Apply a batch of operations atomically.
225    /// All operations succeed or all fail together.
226    ApplyBatch {
227        /// The session ID to apply operations to
228        session_id: String,
229        /// List of operations to apply atomically
230        operations: Vec<BatchOperation>,
231    },
232    /// Get current user's permissions based on their role
233    GetPermissions,
234}
235
236#[derive(Serialize, Deserialize, Debug, Clone, TS)]
237#[ts(export)]
238#[serde(tag = "action")]
239#[serde(rename_all = "lowercase")]
240pub enum BatchOperation {
241    AddNode {
242        node_id: String,
243        kind: String,
244        #[serde(skip_serializing_if = "Option::is_none")]
245        #[ts(type = "JsonValue")]
246        params: Option<serde_json::Value>,
247    },
248    RemoveNode {
249        node_id: String,
250    },
251    Connect {
252        from_node: String,
253        from_pin: String,
254        to_node: String,
255        to_pin: String,
256        #[serde(default)]
257        mode: ConnectionMode,
258    },
259    Disconnect {
260        from_node: String,
261        from_pin: String,
262        to_node: String,
263        to_pin: String,
264    },
265}
266
267pub type Request = Message<RequestPayload>;
268
269// --- Server-to-Client Payloads (Responses & Events) ---
270
271// Allowed: This is an API contract where explicit boolean fields provide clarity
272// for TypeScript consumers. Using bitflags would complicate the API without benefit.
273#[allow(clippy::struct_excessive_bools)]
274#[derive(Serialize, Deserialize, Debug, Clone, TS)]
275#[ts(export, export_to = "bindings/")]
276pub struct PermissionsInfo {
277    pub create_sessions: bool,
278    pub destroy_sessions: bool,
279    pub list_sessions: bool,
280    pub modify_sessions: bool,
281    pub tune_nodes: bool,
282    pub load_plugins: bool,
283    pub delete_plugins: bool,
284    pub list_nodes: bool,
285    pub list_samples: bool,
286    pub read_samples: bool,
287    pub write_samples: bool,
288    pub delete_samples: bool,
289    pub access_all_sessions: bool,
290    pub upload_assets: bool,
291    pub delete_assets: bool,
292}
293
294#[derive(Serialize, Deserialize, Debug, TS)]
295#[ts(export)]
296#[serde(tag = "action")]
297#[serde(rename_all = "lowercase")]
298pub enum ResponsePayload {
299    SessionCreated {
300        session_id: String,
301        #[serde(skip_serializing_if = "Option::is_none")]
302        name: Option<String>,
303        /// ISO 8601 formatted timestamp when the session was created
304        created_at: String,
305    },
306    SessionDestroyed {
307        session_id: String,
308    },
309    SessionsListed {
310        sessions: Vec<SessionInfo>,
311    },
312    NodesListed {
313        nodes: Vec<NodeDefinition>,
314    },
315    Pipeline {
316        pipeline: ApiPipeline,
317    },
318    ValidationResult {
319        errors: Vec<ValidationError>,
320    },
321    BatchApplied {
322        success: bool,
323        errors: Vec<String>,
324    },
325    Permissions {
326        role: String,
327        permissions: PermissionsInfo,
328    },
329    Success,
330    Error {
331        message: String,
332    },
333}
334
335#[derive(Serialize, Deserialize, Debug, Clone, TS)]
336#[ts(export)]
337pub struct ValidationError {
338    pub error_type: ValidationErrorType,
339    pub message: String,
340    pub node_id: Option<String>,
341    pub connection_id: Option<String>,
342}
343
344#[derive(Serialize, Deserialize, Debug, Clone, TS)]
345#[ts(export)]
346#[serde(rename_all = "lowercase")]
347pub enum ValidationErrorType {
348    Error,
349    Warning,
350}
351
352#[derive(Serialize, Deserialize, Debug, Clone, TS)]
353#[ts(export)]
354pub struct SessionInfo {
355    pub id: String,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub name: Option<String>,
358    /// ISO 8601 formatted timestamp when the session was created
359    pub created_at: String,
360}
361
362pub type Response = Message<ResponsePayload>;
363
364// --- Event Payloads (Server-to-Client) ---
365
366/// Events are asynchronous notifications sent from the server to subscribed clients.
367/// Unlike responses, events are not correlated to specific requests.
368#[derive(Serialize, Deserialize, Debug, Clone, TS)]
369#[ts(export)]
370#[serde(tag = "event")]
371#[serde(rename_all = "lowercase")]
372pub enum EventPayload {
373    /// A node's state has changed (e.g., from Running to Recovering).
374    /// Clients can use this to update UI indicators and monitor pipeline health.
375    NodeStateChanged {
376        session_id: String,
377        node_id: String,
378        state: NodeState,
379        /// ISO 8601 formatted timestamp
380        timestamp: String,
381    },
382    /// A node's statistics have been updated (packets processed, discarded, errored).
383    /// These updates are throttled at the source to prevent overload.
384    NodeStatsUpdated {
385        session_id: String,
386        node_id: String,
387        stats: NodeStats,
388        /// ISO 8601 formatted timestamp
389        timestamp: String,
390    },
391    /// A node's parameters have been updated.
392    /// Clients can use this to keep their view of the pipeline state in sync.
393    NodeParamsChanged {
394        session_id: String,
395        node_id: String,
396        #[ts(type = "JsonValue")]
397        params: serde_json::Value,
398    },
399    // --- Session Lifecycle Events ---
400    SessionCreated {
401        session_id: String,
402        #[serde(skip_serializing_if = "Option::is_none")]
403        name: Option<String>,
404        /// ISO 8601 formatted timestamp when the session was created
405        created_at: String,
406    },
407    SessionDestroyed {
408        session_id: String,
409    },
410    // --- Pipeline Structure Events ---
411    NodeAdded {
412        session_id: String,
413        node_id: String,
414        kind: String,
415        #[ts(type = "JsonValue")]
416        params: Option<serde_json::Value>,
417    },
418    NodeRemoved {
419        session_id: String,
420        node_id: String,
421    },
422    ConnectionAdded {
423        session_id: String,
424        from_node: String,
425        from_pin: String,
426        to_node: String,
427        to_pin: String,
428    },
429    ConnectionRemoved {
430        session_id: String,
431        from_node: String,
432        from_pin: String,
433        to_node: String,
434        to_pin: String,
435    },
436    // --- Telemetry Events ---
437    /// Telemetry event from a node (transcription results, VAD events, LLM responses, etc.).
438    /// The data payload contains event-specific fields including event_type for filtering.
439    /// These events are best-effort and may be dropped under load.
440    NodeTelemetry {
441        /// The session this event belongs to
442        session_id: String,
443        /// The node that emitted this event
444        node_id: String,
445        /// Packet type identifier (e.g., "core::telemetry/event@1")
446        type_id: String,
447        /// Event payload containing event_type, correlation_id, turn_id, and event-specific data
448        #[ts(type = "JsonValue")]
449        data: serde_json::Value,
450        /// Microsecond timestamp from the packet metadata (if available)
451        #[serde(skip_serializing_if = "Option::is_none")]
452        timestamp_us: Option<u64>,
453        /// RFC 3339 formatted timestamp for convenience
454        timestamp: String,
455    },
456}
457
458pub type Event = Message<EventPayload>;
459
460// --- Pipeline Types (merged from pipeline crate) ---
461
462/// Engine execution mode
463#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Default, TS)]
464#[ts(export)]
465#[serde(rename_all = "lowercase")]
466pub enum EngineMode {
467    /// One-shot file conversion pipeline (requires http_input/http_output)
468    #[serde(rename = "oneshot")]
469    OneShot,
470    /// Long-running dynamic pipeline (for real-time processing)
471    #[default]
472    Dynamic,
473}
474
475/// Represents a connection between two nodes in the graph.
476#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, TS)]
477#[ts(export)]
478pub struct Connection {
479    pub from_node: String,
480    pub from_pin: String,
481    pub to_node: String,
482    pub to_pin: String,
483    /// How this connection handles backpressure. Defaults to `Reliable`.
484    #[serde(default, skip_serializing_if = "is_default_mode")]
485    pub mode: ConnectionMode,
486}
487
488#[allow(clippy::trivially_copy_pass_by_ref)] // serde skip_serializing_if requires reference
489fn is_default_mode(mode: &ConnectionMode) -> bool {
490    *mode == ConnectionMode::Reliable
491}
492
493/// Represents a single node's configuration within the pipeline.
494#[derive(Debug, Deserialize, Serialize, Clone, TS)]
495#[ts(export)]
496pub struct Node {
497    pub kind: String,
498    #[ts(type = "JsonValue")]
499    pub params: Option<serde_json::Value>,
500    /// Runtime state (only populated in API responses)
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub state: Option<NodeState>,
503}
504
505/// The top-level structure for a pipeline definition, used by the engine and API.
506#[derive(Debug, Deserialize, Serialize, Default, Clone, TS)]
507#[ts(export)]
508pub struct Pipeline {
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub name: Option<String>,
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub description: Option<String>,
513    #[serde(default)]
514    pub mode: EngineMode,
515    #[ts(type = "Record<string, Node>")]
516    pub nodes: indexmap::IndexMap<String, Node>,
517    pub connections: Vec<Connection>,
518}
519
520// Type aliases for backwards compatibility
521pub type ApiConnection = Connection;
522pub type ApiNode = Node;
523pub type ApiPipeline = Pipeline;
524
525// --- Sample Pipelines (for oneshot converter) ---
526
527#[derive(Serialize, Deserialize, Debug, Clone, TS)]
528#[ts(export)]
529pub struct SamplePipeline {
530    pub id: String,
531    pub name: String,
532    pub description: String,
533    pub yaml: String,
534    pub is_system: bool,
535    pub mode: String,
536    /// Whether this is a reusable fragment (partial pipeline) vs a complete pipeline
537    #[serde(default)]
538    pub is_fragment: bool,
539}
540
541#[derive(Serialize, Deserialize, Debug, Clone, TS)]
542#[ts(export)]
543pub struct SavePipelineRequest {
544    pub name: String,
545    pub description: String,
546    pub yaml: String,
547    #[serde(default)]
548    pub overwrite: bool,
549    /// Whether this is a fragment (partial pipeline) vs a complete pipeline
550    #[serde(default)]
551    pub is_fragment: bool,
552}
553
554// --- Audio Assets ---
555
556#[derive(Serialize, Deserialize, Debug, Clone, TS)]
557#[ts(export)]
558pub struct AudioAsset {
559    /// Unique identifier (filename, including extension)
560    pub id: String,
561    /// Display name
562    pub name: String,
563    /// Server-relative path suitable for `core::file_reader` (e.g., `samples/audio/system/foo.wav`)
564    pub path: String,
565    /// File extension/format (opus, ogg, flac, mp3, wav)
566    pub format: String,
567    /// File size in bytes
568    pub size_bytes: u64,
569    /// License information from .license file
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub license: Option<String>,
572    /// Whether this is a system asset (true) or user asset (false)
573    pub is_system: bool,
574}