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}