oxi/rpc_mode/protocol.rs
1//! JSONL framing, JSON-RPC 2.0 types, RPC command/response/event types,
2//! and serialization helpers.
3
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::io::Write;
8use std::sync::Arc;
9
10// ============================================================================
11// JSONL Framing
12// ============================================================================
13
14/// Serialize a value as a strict JSONL record (JSON + LF).
15///
16/// Framing is LF-only. Payload strings may contain other Unicode separators
17/// such as U+2028 and U+2029. Clients must split records on `\n` only.
18pub fn serialize_json_line(value: &Value) -> String {
19 format!("{}\n", serde_json::to_string(value).unwrap_or_default())
20}
21
22/// Serialize a serializable value as a JSONL record.
23pub fn serialize_json_line_obj<T: Serialize>(value: &T) -> String {
24 match serde_json::to_string(value) {
25 Ok(s) => format!("{}\n", s),
26 Err(e) => {
27 tracing::error!("Failed to serialize JSONL: {}", e);
28 "{\"type\":\"response\",\"command\":\"internal\",\"success\":false,\"error\":\"Serialization error\"}\n".to_string()
29 }
30 }
31}
32
33/// Parse a JSONL line, trimming any trailing CR.
34pub fn parse_json_line(line: &str) -> Result<Value, serde_json::Error> {
35 let trimmed = line.trim_end_matches('\r');
36 serde_json::from_str(trimmed)
37}
38
39// ============================================================================
40// JSON-RPC 2.0 Types
41// ============================================================================
42
43/// JSON-RPC 2.0 request
44#[derive(Debug, Clone, Deserialize)]
45pub struct JsonRpcRequest {
46 /// JSON-RPC protocol version.
47 pub jsonrpc: String, // must be "2.0"
48 #[serde(default)]
49 /// Request correlation ID.
50 pub id: Option<Value>,
51 /// JSON-RPC method name.
52 pub method: String,
53 #[serde(default)]
54 /// Method parameters.
55 pub params: Option<Value>,
56}
57
58/// JSON-RPC 2.0 success response
59#[derive(Debug, Clone, Serialize)]
60pub struct JsonRpcSuccessResponse {
61 /// JSON-RPC protocol version.
62 pub jsonrpc: String,
63 /// Request correlation ID.
64 pub id: Value,
65 /// Result value.
66 pub result: Value,
67}
68
69/// JSON-RPC 2.0 error object
70#[derive(Debug, Clone, Serialize)]
71pub struct JsonRpcError {
72 /// Error code.
73 pub code: i64,
74 /// Error message.
75 pub message: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 /// Response data.
78 pub data: Option<Value>,
79}
80
81/// JSON-RPC 2.0 error response
82#[derive(Debug, Clone, Serialize)]
83pub struct JsonRpcErrorResponse {
84 /// JSON-RPC protocol version.
85 pub jsonrpc: String,
86 /// Request correlation ID.
87 pub id: Value,
88 /// Error message.
89 pub error: JsonRpcError,
90}
91
92// Standard JSON-RPC error codes
93/// JSON-RPC parse error code.
94pub const JSONRPC_PARSE_ERROR: i64 = -32700;
95/// JSON-RPC invalid request error code.
96pub const JSONRPC_INVALID_REQUEST: i64 = -32600;
97/// JSON-RPC method-not-found error code.
98pub const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;
99/// JSON-RPC invalid params error code.
100pub const JSONRPC_INVALID_PARAMS: i64 = -32602;
101/// JSON-RPC internal error code.
102pub const JSONRPC_INTERNAL_ERROR: i64 = -32603;
103
104// ============================================================================
105// RPC Types
106// ============================================================================
107
108/// RPC command from client
109#[derive(Debug, Clone, Deserialize)]
110#[serde(tag = "type", rename_all = "snake_case")]
111pub enum RpcCommand {
112 // ── Prompting ───────────────────────────────────────────────────
113 /// Send a prompt message to the agent.
114 Prompt {
115 /// Optional client-side request correlation ID.
116 id: Option<String>,
117 /// The user's prompt text.
118 message: String,
119 /// Optional inline images attached to the prompt.
120 images: Option<Vec<ImageData>>,
121 #[serde(default)]
122 /// Desired streaming behavior (e.g. "full", "partial").
123 streaming_behavior: Option<String>,
124 },
125 /// Send a steering message to interrupt the current stream.
126 Steer {
127 /// Optional client-side request correlation ID.
128 id: Option<String>,
129 /// The steering instruction text.
130 message: String,
131 /// Optional images attached to the steering message.
132 images: Option<Vec<ImageData>>,
133 },
134 /// Send a follow-up message to be processed after the current stream.
135 FollowUp {
136 /// Optional client-side request correlation ID.
137 id: Option<String>,
138 /// The follow-up message text.
139 message: String,
140 /// Optional images attached to the follow-up.
141 images: Option<Vec<ImageData>>,
142 },
143 /// Abort the current agent response.
144 Abort {
145 /// Optional client-side request correlation ID.
146 id: Option<String>,
147 },
148 /// Create a new session.
149 NewSession {
150 /// Optional client-side request correlation ID.
151 id: Option<String>,
152 /// Optional parent session ID for forked sessions.
153 parent_session: Option<String>,
154 },
155
156 // ── State ───────────────────────────────────────────────────────
157 /// Get the current agent state.
158 GetState {
159 /// Optional client-side request correlation ID.
160 id: Option<String>,
161 },
162
163 // ── Model ──────────────────────────────────────────────────────
164 /// Set the active model.
165 SetModel {
166 /// Optional client-side request correlation ID.
167 id: Option<String>,
168 /// Provider name (e.g. "anthropic").
169 provider: String,
170 /// Model identifier (e.g. "claude-sonnet-4-5").
171 model_id: String,
172 },
173 /// Cycle to the next available model.
174 CycleModel {
175 /// Optional client-side request correlation ID.
176 id: Option<String>,
177 },
178 /// List all available models.
179 GetAvailableModels {
180 /// Optional client-side request correlation ID.
181 id: Option<String>,
182 },
183
184 // ── Thinking ────────────────────────────────────────────────────
185 /// Set the thinking level.
186 SetThinkingLevel {
187 /// Optional client-side request correlation ID.
188 id: Option<String>,
189 /// Thinking level (e.g. "off", "low", "medium", "high", "xhigh").
190 level: String,
191 },
192 /// Cycle to the next thinking level.
193 CycleThinkingLevel {
194 /// Optional client-side request correlation ID.
195 id: Option<String>,
196 },
197
198 // ── Queue modes ─────────────────────────────────────────────────
199 /// Set the steering mode.
200 SetSteeringMode {
201 /// Optional client-side request correlation ID.
202 id: Option<String>,
203 /// Steering mode (e.g. "all", "system").
204 mode: String,
205 },
206 /// Set the follow-up mode.
207 SetFollowUpMode {
208 /// Optional client-side request correlation ID.
209 id: Option<String>,
210 /// Follow-up mode (e.g. "all", "system").
211 mode: String,
212 },
213
214 // ── Compaction ──────────────────────────────────────────────────
215 /// Manually trigger compaction.
216 Compact {
217 /// Optional client-side request correlation ID.
218 id: Option<String>,
219 /// Optional custom instructions for the compaction prompt.
220 custom_instructions: Option<String>,
221 },
222 /// Enable or disable auto-compaction.
223 SetAutoCompaction {
224 /// Optional client-side request correlation ID.
225 id: Option<String>,
226 /// Whether auto-compaction is enabled.
227 enabled: bool,
228 },
229
230 // ── Retry ───────────────────────────────────────────────────────
231 /// Enable or disable auto-retry.
232 SetAutoRetry {
233 /// Optional client-side request correlation ID.
234 id: Option<String>,
235 /// Whether auto-retry is enabled.
236 enabled: bool,
237 },
238 /// Abort the current retry attempt.
239 AbortRetry {
240 /// Optional client-side request correlation ID.
241 id: Option<String>,
242 },
243
244 // ── Bash ────────────────────────────────────────────────────────
245 /// Execute a bash command.
246 Bash {
247 /// Optional client-side request correlation ID.
248 id: Option<String>,
249 /// The shell command to execute.
250 command: String,
251 },
252 /// Abort the running bash command.
253 AbortBash {
254 /// Optional client-side request correlation ID.
255 id: Option<String>,
256 },
257
258 // ── Session ────────────────────────────────────────────────────
259 /// Get session statistics.
260 GetSessionStats {
261 /// Optional client-side request correlation ID.
262 id: Option<String>,
263 },
264 /// Export the session as HTML.
265 ExportHtml {
266 /// Optional client-side request correlation ID.
267 id: Option<String>,
268 /// Filesystem path for the exported HTML file.
269 output_path: Option<String>,
270 },
271 /// Switch to a different session.
272 SwitchSession {
273 /// Optional client-side request correlation ID.
274 id: Option<String>,
275 /// Path to the session file to switch to.
276 session_path: String,
277 },
278 /// Fork the session at a given entry.
279 Fork {
280 /// Optional client-side request correlation ID.
281 id: Option<String>,
282 /// ID of the entry at which to fork.
283 entry_id: String,
284 },
285 /// Clone the current session.
286 Clone {
287 /// Optional client-side request correlation ID.
288 id: Option<String>,
289 },
290 /// Get messages from the forked session.
291 GetForkMessages {
292 /// Optional client-side request correlation ID.
293 id: Option<String>,
294 },
295 /// Get the last assistant text response.
296 GetLastAssistantText {
297 /// Optional client-side request correlation ID.
298 id: Option<String>,
299 },
300 /// Set the session display name.
301 SetSessionName {
302 /// Optional client-side request correlation ID.
303 id: Option<String>,
304 /// New display name for the session.
305 name: String,
306 },
307
308 // ── Messages ───────────────────────────────────────────────────
309 /// Get all session messages.
310 GetMessages {
311 /// Optional client-side request correlation ID.
312 id: Option<String>,
313 },
314
315 // ── Commands ────────────────────────────────────────────────────
316 /// get commands.
317 GetCommands {
318 /// Optional client-side request correlation ID.
319 id: Option<String>,
320 },
321}
322
323/// Image data for RPC commands
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ImageData {
326 /// Base64-encoded image data or URL source.
327 pub source: String,
328 #[serde(rename = "type")]
329 /// Image media type (e.g. "image/png").
330 pub media_type: String,
331}
332
333/// Base64-encoded image data
334#[derive(Debug, Clone)]
335pub struct RpcImageSource {
336 /// Decoded image bytes.
337 pub data: Vec<u8>,
338 /// MIME type (e.g. "image/png").
339 pub mime_type: String,
340}
341
342/// RPC response to client
343#[derive(Debug, Clone, Serialize)]
344#[serde(tag = "type", rename_all = "snake_case")]
345pub enum RpcResponse {
346 /// response.
347 Response {
348 /// Correlation ID matching the original request.
349 id: Option<String>,
350 /// Name of the command this responds to.
351 command: String,
352 /// Whether the command succeeded.
353 success: bool,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 /// Response payload on success.
356 data: Option<Value>,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 /// Error message on failure.
359 error: Option<String>,
360 },
361 /// extension ui request.
362 ExtensionUiRequest(RpcExtensionUiRequest),
363}
364
365/// Extension UI request
366#[derive(Debug, Clone, Serialize)]
367#[serde(tag = "method", rename_all = "snake_case")]
368pub enum RpcExtensionUiRequest {
369 /// Present a single-select menu to the user.
370 Select {
371 /// Unique request identifier.
372 id: String,
373 /// Dialog title.
374 title: String,
375 /// Available choices.
376 options: Vec<String>,
377 #[serde(skip_serializing_if = "Option::is_none")]
378 /// Timeout in seconds.
379 timeout: Option<u64>,
380 },
381 /// Ask the user for a yes/no confirmation.
382 Confirm {
383 /// Unique request identifier.
384 id: String,
385 /// Dialog title.
386 title: String,
387 /// Confirmation prompt text.
388 message: String,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 /// Timeout in seconds.
391 timeout: Option<u64>,
392 },
393 /// Ask the user for a free-form text input.
394 Input {
395 /// Unique request identifier.
396 id: String,
397 /// Dialog title.
398 title: String,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 /// Placeholder text for the input field.
401 placeholder: Option<String>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 /// Timeout in seconds.
404 timeout: Option<u64>,
405 },
406 /// Open a full-screen editor for multi-line text input.
407 Editor {
408 /// Unique request identifier.
409 id: String,
410 /// Dialog title.
411 title: String,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 /// Pre-filled editor content.
414 prefill: Option<String>,
415 },
416 /// Display a non-blocking notification to the user.
417 Notify {
418 /// Unique request identifier.
419 id: String,
420 /// Notification body text.
421 message: String,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 /// Notification type hint (e.g. "info", "warning", "error").
424 notify_type: Option<String>,
425 },
426 /// Set a status bar entry.
427 SetStatus {
428 /// Unique request identifier.
429 id: String,
430 /// Status bar key for deduplication.
431 status_key: String,
432 /// Status text to display; `None` clears the entry.
433 status_text: Option<String>,
434 },
435 /// Set or update a custom widget in the UI.
436 SetWidget {
437 /// Unique request identifier.
438 id: String,
439 /// Widget key for deduplication.
440 widget_key: String,
441 /// Lines of content to render.
442 widget_lines: Option<Vec<String>>,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 /// Widget placement hint (e.g. "bottom", "sidebar").
445 widget_placement: Option<String>,
446 },
447 /// Set the window/terminal title.
448 SetTitle {
449 /// Unique request identifier.
450 id: String,
451 /// New title text.
452 title: String,
453 },
454 /// Replace the text in the user's input editor.
455 SetEditorText {
456 /// Unique request identifier.
457 id: String,
458 /// New editor text content.
459 text: String,
460 },
461}
462
463/// Extension UI response from client
464#[derive(Debug, Clone, Deserialize)]
465#[serde(tag = "type", rename_all = "snake_case")]
466pub enum RpcExtensionUiResponse {
467 /// Respond to an extension UI request.
468 ExtensionUiResponse {
469 /// The request ID being responded to.
470 id: String,
471 #[serde(default)]
472 /// The user's text input (for Input/Editor requests).
473 value: Option<String>,
474 #[serde(default)]
475 /// Whether the user confirmed (for Confirm requests).
476 confirmed: Option<bool>,
477 #[serde(default)]
478 /// Whether the user dismissed the request without answering.
479 cancelled: Option<bool>,
480 },
481}
482
483// ============================================================================
484// State / Info Types
485// ============================================================================
486
487/// Session state for get_state response
488#[derive(Debug, Clone, Serialize)]
489pub struct SessionState {
490 /// Current model info
491 #[serde(skip_serializing_if = "Option::is_none")]
492 pub model: Option<ModelInfo>,
493 /// thinking level.
494 pub thinking_level: String,
495 /// is streaming.
496 pub is_streaming: bool,
497 /// is compacting.
498 pub is_compacting: bool,
499 /// steering mode.
500 pub steering_mode: String,
501 /// follow up mode.
502 pub follow_up_mode: String,
503 /// Session file path
504 #[serde(skip_serializing_if = "Option::is_none")]
505 pub session_file: Option<String>,
506 /// Session identifier.
507 pub session_id: String,
508 #[serde(skip_serializing_if = "Option::is_none")]
509 /// session name.
510 pub session_name: Option<String>,
511 /// auto compaction enabled.
512 pub auto_compaction_enabled: bool,
513 /// message count.
514 pub message_count: usize,
515 /// pending message count.
516 pub pending_message_count: usize,
517}
518
519/// Model information for session state
520#[derive(Debug, Clone, Serialize)]
521pub struct ModelInfo {
522 /// Provider name.
523 pub provider: String,
524 /// Request correlation ID.
525 pub id: String,
526}
527
528/// Command info for get_commands response
529#[derive(Debug, Clone, Serialize)]
530pub struct CommandInfo {
531 /// Name value.
532 pub name: String,
533 #[serde(skip_serializing_if = "Option::is_none")]
534 /// description.
535 pub description: Option<String>,
536 /// source.
537 pub source: String,
538 #[serde(skip_serializing_if = "Option::is_none")]
539 /// source info.
540 pub source_info: Option<SourceInfo>,
541}
542
543/// Source info for command provenance
544#[derive(Debug, Clone, Serialize)]
545pub struct SourceInfo {
546 #[serde(skip_serializing_if = "Option::is_none")]
547 /// File path.
548 pub path: Option<String>,
549 #[serde(skip_serializing_if = "Option::is_none")]
550 /// origin.
551 pub origin: Option<String>,
552}
553
554/// Result of a session stat query
555#[derive(Debug, Clone, Serialize)]
556pub struct SessionStats {
557 /// message count.
558 pub message_count: usize,
559 /// token count.
560 pub token_count: Option<usize>,
561 /// last activity.
562 pub last_activity: Option<i64>,
563}
564
565/// Result of compaction
566#[derive(Debug, Clone, Serialize)]
567pub struct CompactionResult {
568 /// original count.
569 pub original_count: usize,
570 /// compacted count.
571 pub compacted_count: usize,
572 /// tokens saved.
573 pub tokens_saved: Option<usize>,
574}
575
576// ============================================================================
577// Event Types
578// ============================================================================
579
580/// Events emitted by the RPC server to the client during streaming.
581#[derive(Debug, Clone, Serialize)]
582#[serde(tag = "type", rename_all = "snake_case")]
583pub enum RpcEvent {
584 /// Agent started processing
585 AgentStart,
586 /// Streaming text chunk
587 TextChunk {
588 /// Streaming text content.
589 text: String,
590 },
591 /// Agent is thinking
592 Thinking,
593 /// Tool execution started
594 ToolStart {
595 /// Name of the tool being executed.
596 tool: String,
597 },
598 /// Tool execution finished
599 ToolEnd {
600 /// Name of the tool that finished.
601 tool: String,
602 },
603 /// Agent finished processing
604 AgentEnd,
605 /// Error occurred
606 Error {
607 /// Error message.
608 message: String,
609 },
610 /// Extension error
611 ExtensionError {
612 /// Filesystem path of the extension that errored.
613 extension_path: String,
614 /// Event name during which the error occurred.
615 event: String,
616 /// Error message.
617 error: String,
618 },
619}
620
621// ============================================================================
622// Pending Extension UI Request
623// ============================================================================
624
625/// Tracks pending extension UI requests awaiting client responses.
626pub struct PendingExtensionRequest {
627 /// Sender to deliver the client's response back to the awaiter.
628 pub resolve: tokio::sync::oneshot::Sender<RpcExtensionUiResponse>,
629}
630
631// ============================================================================
632// JSON-RPC 2.0 Method Mapping
633// ============================================================================
634
635/// Map a JSON-RPC 2.0 method + params to an internal RPC command value.
636pub fn jsonrpc_to_command(method: &str, params: Option<Value>, id: Option<Value>) -> Option<Value> {
637 let id_str = id.map(|v| match v {
638 Value::String(s) => s,
639 Value::Number(n) => n.to_string(),
640 _ => v.to_string(),
641 });
642
643 let cmd = match method {
644 "prompt" => serde_json::json!({
645 "type": "prompt",
646 "id": id_str,
647 "message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
648 "images": params.as_ref().and_then(|p| p.get("images")),
649 "streaming_behavior": params.as_ref().and_then(|p| p.get("streaming_behavior")),
650 }),
651 "steer" => serde_json::json!({
652 "type": "steer",
653 "id": id_str,
654 "message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
655 }),
656 "follow_up" => serde_json::json!({
657 "type": "follow_up",
658 "id": id_str,
659 "message": params.as_ref().and_then(|p| p.get("message")).and_then(|m| m.as_str()).unwrap_or(""),
660 }),
661 "abort" => serde_json::json!({ "type": "abort", "id": id_str }),
662 "new_session" => serde_json::json!({
663 "type": "new_session",
664 "id": id_str,
665 "parent_session": params.as_ref().and_then(|p| p.get("parent_session")).and_then(|v| v.as_str()),
666 }),
667 "get_state" => serde_json::json!({ "type": "get_state", "id": id_str }),
668 "set_model" => serde_json::json!({
669 "type": "set_model",
670 "id": id_str,
671 "provider": params.as_ref().and_then(|p| p.get("provider")).and_then(|v| v.as_str()).unwrap_or(""),
672 "model_id": params.as_ref().and_then(|p| p.get("modelId")).and_then(|v| v.as_str()).unwrap_or(""),
673 }),
674 "cycle_model" => serde_json::json!({ "type": "cycle_model", "id": id_str }),
675 "get_available_models" => {
676 serde_json::json!({ "type": "get_available_models", "id": id_str })
677 }
678 "set_thinking_level" => serde_json::json!({
679 "type": "set_thinking_level",
680 "id": id_str,
681 "level": params.as_ref().and_then(|p| p.get("level")).and_then(|v| v.as_str()).unwrap_or("default"),
682 }),
683 "cycle_thinking_level" => {
684 serde_json::json!({ "type": "cycle_thinking_level", "id": id_str })
685 }
686 "set_steering_mode" => serde_json::json!({
687 "type": "set_steering_mode",
688 "id": id_str,
689 "mode": params.as_ref().and_then(|p| p.get("mode")).and_then(|v| v.as_str()).unwrap_or("all"),
690 }),
691 "set_follow_up_mode" => serde_json::json!({
692 "type": "set_follow_up_mode",
693 "id": id_str,
694 "mode": params.as_ref().and_then(|p| p.get("mode")).and_then(|v| v.as_str()).unwrap_or("all"),
695 }),
696 "compact" => serde_json::json!({
697 "type": "compact",
698 "id": id_str,
699 "custom_instructions": params.as_ref().and_then(|p| p.get("customInstructions")).and_then(|v| v.as_str()),
700 }),
701 "set_auto_compaction" => serde_json::json!({
702 "type": "set_auto_compaction",
703 "id": id_str,
704 "enabled": params.as_ref().and_then(|p| p.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(true),
705 }),
706 "set_auto_retry" => serde_json::json!({
707 "type": "set_auto_retry",
708 "id": id_str,
709 "enabled": params.as_ref().and_then(|p| p.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(true),
710 }),
711 "abort_retry" => serde_json::json!({ "type": "abort_retry", "id": id_str }),
712 "bash" => serde_json::json!({
713 "type": "bash",
714 "id": id_str,
715 "command": params.as_ref().and_then(|p| p.get("command")).and_then(|v| v.as_str()).unwrap_or(""),
716 }),
717 "abort_bash" => serde_json::json!({ "type": "abort_bash", "id": id_str }),
718 "get_session_stats" => serde_json::json!({ "type": "get_session_stats", "id": id_str }),
719 "export_html" => serde_json::json!({
720 "type": "export_html",
721 "id": id_str,
722 "output_path": params.as_ref().and_then(|p| p.get("outputPath")).and_then(|v| v.as_str()),
723 }),
724 "switch_session" => serde_json::json!({
725 "type": "switch_session",
726 "id": id_str,
727 "session_path": params.as_ref().and_then(|p| p.get("sessionPath")).and_then(|v| v.as_str()).unwrap_or(""),
728 }),
729 "fork" => serde_json::json!({
730 "type": "fork",
731 "id": id_str,
732 "entry_id": params.as_ref().and_then(|p| p.get("entryId")).and_then(|v| v.as_str()).unwrap_or(""),
733 }),
734 "clone" => serde_json::json!({ "type": "clone", "id": id_str }),
735 "get_fork_messages" => serde_json::json!({ "type": "get_fork_messages", "id": id_str }),
736 "get_last_assistant_text" => {
737 serde_json::json!({ "type": "get_last_assistant_text", "id": id_str })
738 }
739 "set_session_name" => serde_json::json!({
740 "type": "set_session_name",
741 "id": id_str,
742 "name": params.as_ref().and_then(|p| p.get("name")).and_then(|v| v.as_str()).unwrap_or(""),
743 }),
744 "get_messages" => serde_json::json!({ "type": "get_messages", "id": id_str }),
745 "get_commands" => serde_json::json!({ "type": "get_commands", "id": id_str }),
746 _ => return None,
747 };
748
749 Some(cmd)
750}
751
752/// Convert an internal RpcResponse to a JSON-RPC 2.0 response.
753pub fn rpc_response_to_jsonrpc(response: &RpcResponse, rpc_id: Value) -> String {
754 match response {
755 RpcResponse::Response {
756 success: true,
757 data,
758 ..
759 } => {
760 let result = data.clone().unwrap_or(Value::Null);
761 let resp = JsonRpcSuccessResponse {
762 jsonrpc: "2.0".to_string(),
763 id: rpc_id,
764 result,
765 };
766 serde_json::to_string(&resp).unwrap_or_default()
767 }
768 RpcResponse::Response {
769 success: false,
770 error,
771 ..
772 } => {
773 let resp = JsonRpcErrorResponse {
774 jsonrpc: "2.0".to_string(),
775 id: rpc_id,
776 error: JsonRpcError {
777 code: JSONRPC_INTERNAL_ERROR,
778 message: error.clone().unwrap_or_default(),
779 data: None,
780 },
781 };
782 serde_json::to_string(&resp).unwrap_or_default()
783 }
784 RpcResponse::ExtensionUiRequest(_) => {
785 // Extension UI requests don't map cleanly to JSON-RPC
786 // In JSON-RPC mode we skip these or send as notification
787 String::new()
788 }
789 }
790}
791
792// ============================================================================
793// JSONL Line Reader
794// ============================================================================
795
796/// A strict JSONL line reader that splits on LF only (not CR or other separators).
797/// This is important because JSON strings may contain U+2028/U+2029 which
798/// Node.js readline would incorrectly split on.
799pub struct JsonlLineReader {
800 buffer: String,
801}
802
803impl JsonlLineReader {
804 /// Create a new JSONL line reader.
805 pub fn new() -> Self {
806 Self {
807 buffer: String::new(),
808 }
809 }
810
811 /// Feed data into the reader, returning complete lines.
812 /// Lines are terminated by `\n` only. Trailing `\r` is stripped.
813 pub fn feed(&mut self, data: &str) -> Vec<String> {
814 self.buffer.push_str(data);
815 let mut lines = Vec::new();
816
817 while let Some(pos) = self.buffer.find('\n') {
818 let line = self.buffer[..pos].to_string();
819 self.buffer = self.buffer[pos + 1..].to_string();
820 // Strip trailing CR
821 let trimmed = line.trim_end_matches('\r').to_string();
822 if !trimmed.is_empty() {
823 lines.push(trimmed);
824 }
825 }
826
827 lines
828 }
829
830 /// Flush any remaining data as a final line (for EOF handling).
831 pub fn flush(&mut self) -> Option<String> {
832 if self.buffer.is_empty() {
833 return None;
834 }
835 let line = self.buffer.trim_end_matches('\r').to_string();
836 self.buffer.clear();
837 if line.is_empty() { None } else { Some(line) }
838 }
839
840 /// Check if there's buffered data.
841 pub fn has_buffered_data(&self) -> bool {
842 !self.buffer.is_empty()
843 }
844}
845
846impl Default for JsonlLineReader {
847 fn default() -> Self {
848 Self::new()
849 }
850}
851
852// ============================================================================
853// Output Writer
854// ============================================================================
855
856/// Thread-safe output writer that ensures atomic JSONL writes.
857pub struct RpcOutput {
858 inner: Arc<parking_lot::Mutex<std::io::Stdout>>,
859}
860
861impl RpcOutput {
862 /// Create a new output writer wrapping stdout.
863 pub fn new() -> Self {
864 Self {
865 inner: Arc::new(parking_lot::Mutex::new(std::io::stdout())),
866 }
867 }
868
869 /// Write a JSONL record atomically.
870 pub fn write_line(&self, value: &Value) {
871 let line = serialize_json_line(value);
872 let mut out = self.inner.lock();
873 let _ = out.write_all(line.as_bytes());
874 let _ = out.flush();
875 }
876
877 /// Write a serializable value as a JSONL record.
878 pub fn write_obj<T: Serialize>(&self, value: &T) {
879 let line = serialize_json_line_obj(value);
880 let mut out = self.inner.lock();
881 let _ = out.write_all(line.as_bytes());
882 let _ = out.flush();
883 }
884
885 /// Write a raw string (must already be a complete JSONL record with trailing LF).
886 pub fn write_raw(&self, raw: &str) {
887 let mut out = self.inner.lock();
888 let _ = out.write_all(raw.as_bytes());
889 let _ = out.flush();
890 }
891}
892
893impl Default for RpcOutput {
894 fn default() -> Self {
895 Self::new()
896 }
897}
898
899impl Clone for RpcOutput {
900 fn clone(&self) -> Self {
901 Self {
902 inner: Arc::clone(&self.inner),
903 }
904 }
905}
906
907// ============================================================================
908// Session Handoff Protocol
909// ============================================================================
910
911/// Information about a session for handoff between processes.
912#[derive(Debug, Clone, Serialize, Deserialize)]
913pub struct SessionHandoff {
914 /// Unique session identifier
915 pub session_id: String,
916 /// Session file path
917 pub session_file: Option<String>,
918 /// Session display name
919 pub session_name: Option<String>,
920 /// Parent session ID (for forked sessions)
921 pub parent_session_id: Option<String>,
922 /// Model being used
923 pub model_id: Option<String>,
924 /// Thinking level
925 pub thinking_level: Option<String>,
926 /// Message count at handoff time
927 pub message_count: usize,
928 /// Timestamp of handoff
929 pub timestamp: i64,
930}
931
932impl SessionHandoff {
933 /// Create a handoff from current session state.
934 pub fn from_state(state: &SessionState) -> Self {
935 Self {
936 session_id: state.session_id.clone(),
937 session_file: state.session_file.clone(),
938 session_name: state.session_name.clone(),
939 parent_session_id: None,
940 model_id: state
941 .model
942 .as_ref()
943 .map(|m| format!("{}/{}", m.provider, m.id)),
944 thinking_level: Some(state.thinking_level.clone()),
945 message_count: state.message_count,
946 timestamp: chrono::Utc::now().timestamp(),
947 }
948 }
949
950 /// Serialize to JSON for transmission.
951 pub fn to_json(&self) -> Result<String> {
952 Ok(serde_json::to_string(self)?)
953 }
954
955 /// Deserialize from JSON.
956 pub fn from_json(json: &str) -> Result<Self> {
957 Ok(serde_json::from_str(json)?)
958 }
959}