Skip to main content

plexus_substrate/activations/claudecode/
types.rs

1use crate::activations::arbor::{NodeId, TreeId};
2use plexus_macros::HandleEnum;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7use uuid::Uuid;
8
9use super::activation::ClaudeCode;
10
11/// Unique identifier for a ClaudeCode session
12pub type ClaudeCodeId = Uuid;
13
14// ============================================================================
15// Handle types for ClaudeCode activation
16// ============================================================================
17
18/// Type-safe handles for ClaudeCode activation data
19///
20/// Handles reference data stored in the ClaudeCode database and can be embedded
21/// in Arbor tree nodes for external resolution.
22#[derive(Debug, Clone, HandleEnum)]
23#[handle(plugin_id = "ClaudeCode::PLUGIN_ID", version = "1.0.0")]
24pub enum ClaudeCodeHandle {
25    /// Handle to a message in the claudecode database
26    /// Format: `{plugin_id}@1.0.0::chat:msg-{uuid}:{role}:{name}`
27    #[handle(
28        method = "chat",
29        table = "messages",
30        key = "id",
31        key_field = "message_id",
32        strip_prefix = "msg-"
33    )]
34    Message {
35        /// Message ID with "msg-" prefix (e.g., "msg-550e8400-...")
36        message_id: String,
37        /// Role: "user", "assistant", or "system"
38        role: String,
39        /// Display name
40        name: String,
41    },
42
43    /// Handle to an unknown/passthrough event
44    /// Format: `{plugin_id}@1.0.0::passthrough:{event_id}:{event_type}`
45    /// Note: No resolution - passthrough events are inline only
46    #[handle(method = "passthrough")]
47    Passthrough {
48        /// Event ID
49        event_id: String,
50        /// Event type string
51        event_type: String,
52    },
53}
54
55// ============================================================================
56// Handle resolution result types
57// ============================================================================
58
59/// Result of resolving a ClaudeCode handle
60#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
61#[serde(tag = "type")]
62pub enum ResolveResult {
63    /// Successfully resolved message
64    #[serde(rename = "resolved_message")]
65    Message {
66        id: String,
67        role: String,
68        content: String,
69        model: Option<String>,
70        name: String,
71    },
72    /// Resolution error
73    #[serde(rename = "error")]
74    Error { message: String },
75}
76
77/// Unique identifier for an active stream
78pub type StreamId = Uuid;
79
80/// Unique identifier for a message
81pub type MessageId = Uuid;
82
83/// Role of a message sender
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(rename_all = "snake_case")]
86pub enum MessageRole {
87    User,
88    Assistant,
89    System,
90}
91
92impl MessageRole {
93    pub fn as_str(&self) -> &'static str {
94        match self {
95            MessageRole::User => "user",
96            MessageRole::Assistant => "assistant",
97            MessageRole::System => "system",
98        }
99    }
100
101    pub fn from_str(s: &str) -> Option<Self> {
102        match s {
103            "user" => Some(MessageRole::User),
104            "assistant" => Some(MessageRole::Assistant),
105            "system" => Some(MessageRole::System),
106            _ => None,
107        }
108    }
109}
110
111/// Model selection for Claude Code
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
113#[serde(rename_all = "lowercase")]
114pub enum Model {
115    Opus,
116    Sonnet,
117    Haiku,
118}
119
120impl Model {
121    pub fn as_str(&self) -> &'static str {
122        match self {
123            Model::Opus => "opus",
124            Model::Sonnet => "sonnet",
125            Model::Haiku => "haiku",
126        }
127    }
128
129    pub fn from_str(s: &str) -> Option<Self> {
130        match s.to_lowercase().as_str() {
131            "opus" => Some(Model::Opus),
132            "sonnet" => Some(Model::Sonnet),
133            "haiku" => Some(Model::Haiku),
134            _ => None,
135        }
136    }
137}
138
139/// A position in the context tree - couples tree_id and node_id together.
140/// Same structure as Cone's Position for consistency.
141#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
142pub struct Position {
143    /// The tree containing this position
144    pub tree_id: TreeId,
145    /// The specific node within the tree
146    pub node_id: NodeId,
147}
148
149impl Position {
150    /// Create a new position
151    pub fn new(tree_id: TreeId, node_id: NodeId) -> Self {
152        Self { tree_id, node_id }
153    }
154
155    /// Advance to a new node in the same tree
156    pub fn advance(&self, new_node_id: NodeId) -> Self {
157        Self {
158            tree_id: self.tree_id,
159            node_id: new_node_id,
160        }
161    }
162}
163
164/// A message stored in the claudecode database
165#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
166pub struct Message {
167    pub id: MessageId,
168    pub session_id: ClaudeCodeId,
169    pub role: MessageRole,
170    pub content: String,
171    pub created_at: i64,
172    /// Model used (for assistant messages)
173    pub model_id: Option<String>,
174    /// Token usage (for assistant messages)
175    pub input_tokens: Option<i64>,
176    pub output_tokens: Option<i64>,
177    /// Cost in USD (from Claude Code)
178    pub cost_usd: Option<f64>,
179}
180
181/// ClaudeCode session configuration
182#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
183pub struct ClaudeCodeConfig {
184    /// Unique identifier for this session
185    pub id: ClaudeCodeId,
186    /// Human-readable name
187    pub name: String,
188    /// Claude Code's internal session ID (for --resume, populated after first chat)
189    pub claude_session_id: Option<String>,
190    /// Session ID for loopback MCP URL correlation (e.g., orcha-xxx-claude-yyy)
191    pub loopback_session_id: Option<String>,
192    /// The canonical head - current position in conversation tree
193    pub head: Position,
194    /// Working directory for Claude Code
195    pub working_dir: String,
196    /// Model to use
197    pub model: Model,
198    /// System prompt / instructions
199    pub system_prompt: Option<String>,
200    /// MCP server configuration (JSON)
201    pub mcp_config: Option<Value>,
202    /// Enable loopback mode - routes tool permissions through parent for approval
203    pub loopback_enabled: bool,
204    /// Additional metadata
205    pub metadata: Option<Value>,
206    /// Created timestamp
207    pub created_at: i64,
208    /// Last updated timestamp
209    pub updated_at: i64,
210}
211
212impl ClaudeCodeConfig {
213    /// Get the tree ID (convenience accessor)
214    pub fn tree_id(&self) -> TreeId {
215        self.head.tree_id
216    }
217
218    /// Get the current node ID (convenience accessor)
219    pub fn node_id(&self) -> NodeId {
220        self.head.node_id
221    }
222}
223
224/// Lightweight session info (for listing)
225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
226pub struct ClaudeCodeInfo {
227    pub id: ClaudeCodeId,
228    pub name: String,
229    pub model: Model,
230    pub head: Position,
231    pub claude_session_id: Option<String>,
232    pub working_dir: String,
233    pub loopback_enabled: bool,
234    pub created_at: i64,
235}
236
237impl From<&ClaudeCodeConfig> for ClaudeCodeInfo {
238    fn from(config: &ClaudeCodeConfig) -> Self {
239        Self {
240            id: config.id,
241            name: config.name.clone(),
242            model: config.model,
243            head: config.head,
244            claude_session_id: config.claude_session_id.clone(),
245            working_dir: config.working_dir.clone(),
246            loopback_enabled: config.loopback_enabled,
247            created_at: config.created_at,
248        }
249    }
250}
251
252/// Token usage information
253#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
254pub struct ChatUsage {
255    pub input_tokens: Option<u64>,
256    pub output_tokens: Option<u64>,
257    pub cost_usd: Option<f64>,
258    pub num_turns: Option<i32>,
259}
260
261// ═══════════════════════════════════════════════════════════════════════════
262// STREAM MANAGEMENT TYPES (for non-blocking chat with loopback)
263// ═══════════════════════════════════════════════════════════════════════════
264
265/// Status of an active stream
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
267#[serde(rename_all = "snake_case")]
268pub enum StreamStatus {
269    /// Stream is actively receiving events
270    Running,
271    /// Stream is waiting for tool permission approval
272    AwaitingPermission,
273    /// Stream completed successfully
274    Complete,
275    /// Stream failed with an error
276    Failed,
277}
278
279/// Information about an active stream
280#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
281pub struct StreamInfo {
282    /// Unique stream identifier
283    pub stream_id: StreamId,
284    /// Session this stream belongs to
285    pub session_id: ClaudeCodeId,
286    /// Current status
287    pub status: StreamStatus,
288    /// Position of the user message node (set at start)
289    pub user_position: Option<Position>,
290    /// Number of events buffered
291    pub event_count: u64,
292    /// Read position (how many events have been consumed)
293    pub read_position: u64,
294    /// When the stream started
295    pub started_at: i64,
296    /// When the stream ended (if complete/failed)
297    pub ended_at: Option<i64>,
298    /// Error message if failed
299    pub error: Option<String>,
300}
301
302/// A buffered event in the stream
303#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
304pub struct BufferedEvent {
305    /// Sequence number within the stream
306    pub seq: u64,
307    /// The chat event
308    pub event: ChatEvent,
309    /// Timestamp when event was received
310    pub timestamp: i64,
311}
312
313// ═══════════════════════════════════════════════════════════════════════════
314// METHOD-SPECIFIC RETURN TYPES
315// Each method returns exactly what it needs - no shared enums
316// ═══════════════════════════════════════════════════════════════════════════
317
318/// Result of creating a session
319#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
320#[serde(tag = "type", rename_all = "snake_case")]
321pub enum CreateResult {
322    #[serde(rename = "created")]
323    Ok {
324        id: ClaudeCodeId,
325        head: Position,
326    },
327    #[serde(rename = "error")]
328    Err { message: String },
329}
330
331/// Result of getting a session
332#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
333#[serde(tag = "type", rename_all = "snake_case")]
334pub enum GetResult {
335    #[serde(rename = "ok")]
336    Ok { config: ClaudeCodeConfig },
337    #[serde(rename = "error")]
338    Err { message: String },
339}
340
341/// Result of listing sessions
342#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
343#[serde(tag = "type", rename_all = "snake_case")]
344pub enum ListResult {
345    #[serde(rename = "ok")]
346    Ok { sessions: Vec<ClaudeCodeInfo> },
347    #[serde(rename = "error")]
348    Err { message: String },
349}
350
351/// Result of deleting a session
352#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
353#[serde(tag = "type", rename_all = "snake_case")]
354pub enum DeleteResult {
355    #[serde(rename = "deleted")]
356    Ok { id: ClaudeCodeId },
357    #[serde(rename = "error")]
358    Err { message: String },
359}
360
361/// Result of forking a session
362#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
363#[serde(tag = "type", rename_all = "snake_case")]
364pub enum ForkResult {
365    #[serde(rename = "forked")]
366    Ok {
367        id: ClaudeCodeId,
368        head: Position,
369    },
370    #[serde(rename = "error")]
371    Err { message: String },
372}
373
374/// Result of starting an async chat (non-blocking)
375#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
376#[serde(tag = "type", rename_all = "snake_case")]
377pub enum ChatStartResult {
378    #[serde(rename = "started")]
379    Ok {
380        stream_id: StreamId,
381        session_id: ClaudeCodeId,
382    },
383    #[serde(rename = "error")]
384    Err { message: String },
385}
386
387/// Result of polling a stream for events
388#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
389#[serde(tag = "type", rename_all = "snake_case")]
390pub enum PollResult {
391    #[serde(rename = "ok")]
392    Ok {
393        /// Current stream status
394        status: StreamStatus,
395        /// Events since last poll (or from specified offset)
396        events: Vec<BufferedEvent>,
397        /// Current read position after this poll
398        read_position: u64,
399        /// Total events in buffer
400        total_events: u64,
401        /// True if there are more events available
402        has_more: bool,
403    },
404    #[serde(rename = "error")]
405    Err { message: String },
406}
407
408/// Result of listing active streams
409#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
410#[serde(tag = "type", rename_all = "snake_case")]
411pub enum StreamListResult {
412    #[serde(rename = "ok")]
413    Ok { streams: Vec<StreamInfo> },
414    #[serde(rename = "error")]
415    Err { message: String },
416}
417
418// ═══════════════════════════════════════════════════════════════════════════
419// CHAT EVENTS - Streaming conversation (needs enum for multiple event types)
420// ═══════════════════════════════════════════════════════════════════════════
421
422/// Events emitted during chat streaming
423#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
424#[serde(tag = "type", rename_all = "snake_case")]
425pub enum ChatEvent {
426    /// Chat started - user message stored, streaming begins
427    #[serde(rename = "start")]
428    Start {
429        id: ClaudeCodeId,
430        user_position: Position,
431    },
432
433    /// Content chunk (streaming tokens)
434    #[serde(rename = "content")]
435    Content { text: String },
436
437    /// Thinking block - Claude's internal reasoning
438    #[serde(rename = "thinking")]
439    Thinking { thinking: String },
440
441    /// Tool use detected
442    #[serde(rename = "tool_use")]
443    ToolUse {
444        tool_name: String,
445        tool_use_id: String,
446        input: Value,
447    },
448
449    /// Tool result received
450    #[serde(rename = "tool_result")]
451    ToolResult {
452        tool_use_id: String,
453        output: String,
454        is_error: bool,
455    },
456
457    /// Chat complete - response stored, head updated
458    #[serde(rename = "complete")]
459    Complete {
460        new_head: Position,
461        claude_session_id: String,
462        usage: Option<ChatUsage>,
463    },
464
465    /// Passthrough for unrecognized Claude Code events
466    /// Data is stored separately (referenced by handle) and also forwarded inline
467    #[serde(rename = "passthrough")]
468    Passthrough {
469        event_type: String,
470        handle: String,
471        data: Value,
472    },
473
474    /// Error during chat
475    #[serde(rename = "error")]
476    Err { message: String },
477}
478
479/// Typed errors for ClaudeCode operations
480#[derive(Debug, Error)]
481pub enum ClaudeCodeError {
482    #[error("failed to resolve working directory '{path}': {source}")]
483    PathResolution { path: String, source: std::io::Error },
484
485    #[error("session not found: {identifier}")]
486    SessionNotFound { identifier: String },
487
488    #[error("ambiguous session name '{name}' matches multiple sessions: {matches}")]
489    AmbiguousSession { name: String, matches: String },
490
491    #[error("database error: {operation}: {source}")]
492    Database { operation: &'static str, source: sqlx::Error },
493
494    #[error("parse error: {context}: {detail}")]
495    Parse { context: &'static str, detail: String },
496
497    #[error("serialization error: {0}")]
498    Serialization(#[from] serde_json::Error),
499
500    #[error("arbor error: {0}")]
501    Arbor(String),
502}
503
504// ═══════════════════════════════════════════════════════════════════════════
505// Raw events from Claude Code CLI (for parsing stream-json output)
506// ═══════════════════════════════════════════════════════════════════════════
507
508/// Raw events from Claude Code's stream-json output
509#[derive(Debug, Clone, Deserialize)]
510#[serde(tag = "type")]
511pub enum RawClaudeEvent {
512    /// System initialization event
513    #[serde(rename = "system")]
514    System {
515        subtype: Option<String>,
516        #[serde(rename = "session_id")]
517        session_id: Option<String>,
518        model: Option<String>,
519        cwd: Option<String>,
520        tools: Option<Vec<String>>,
521    },
522
523    /// Assistant message event
524    #[serde(rename = "assistant")]
525    Assistant {
526        message: Option<RawMessage>,
527    },
528
529    /// User message event
530    #[serde(rename = "user")]
531    User {
532        message: Option<RawMessage>,
533    },
534
535    /// Result event (session complete)
536    #[serde(rename = "result")]
537    Result {
538        subtype: Option<String>,
539        session_id: Option<String>,
540        cost_usd: Option<f64>,
541        is_error: Option<bool>,
542        duration_ms: Option<i64>,
543        num_turns: Option<i32>,
544        result: Option<String>,
545        error: Option<String>,
546    },
547
548    /// Stream event (partial message chunks from --include-partial-messages)
549    #[serde(rename = "stream_event")]
550    StreamEvent {
551        event: StreamEventInner,
552        session_id: Option<String>,
553    },
554
555    /// Unknown event type - captures events we don't recognize
556    /// This is constructed manually in executor.rs, not via serde
557    #[serde(skip)]
558    Unknown {
559        event_type: String,
560        data: Value,
561    },
562
563    /// The exact shell command launched (emitted before spawn, constructed manually)
564    #[serde(skip)]
565    LaunchCommand { command: String },
566
567    /// A line from Claude's stderr (emitted after stdout closes, constructed manually)
568    #[serde(skip)]
569    Stderr { text: String },
570}
571
572/// Inner event types for stream_event
573#[derive(Debug, Clone, Deserialize)]
574#[serde(tag = "type")]
575pub enum StreamEventInner {
576    #[serde(rename = "message_start")]
577    MessageStart {
578        message: Option<StreamMessage>,
579    },
580
581    #[serde(rename = "content_block_start")]
582    ContentBlockStart {
583        index: usize,
584        content_block: Option<StreamContentBlock>,
585    },
586
587    #[serde(rename = "content_block_delta")]
588    ContentBlockDelta {
589        index: usize,
590        delta: StreamDelta,
591    },
592
593    #[serde(rename = "content_block_stop")]
594    ContentBlockStop {
595        index: usize,
596    },
597
598    #[serde(rename = "message_delta")]
599    MessageDelta {
600        delta: MessageDeltaInfo,
601    },
602
603    #[serde(rename = "message_stop")]
604    MessageStop,
605}
606
607#[derive(Debug, Clone, Deserialize)]
608pub struct StreamMessage {
609    pub model: Option<String>,
610    pub role: Option<String>,
611}
612
613#[derive(Debug, Clone, Deserialize)]
614#[serde(tag = "type")]
615pub enum StreamContentBlock {
616    #[serde(rename = "text")]
617    Text { text: Option<String> },
618
619    #[serde(rename = "tool_use")]
620    ToolUse {
621        id: String,
622        name: String,
623        input: Option<Value>,
624    },
625}
626
627#[derive(Debug, Clone, Deserialize)]
628#[serde(tag = "type")]
629pub enum StreamDelta {
630    #[serde(rename = "text_delta")]
631    TextDelta { text: String },
632
633    #[serde(rename = "input_json_delta")]
634    InputJsonDelta { partial_json: String },
635}
636
637#[derive(Debug, Clone, Deserialize)]
638pub struct MessageDeltaInfo {
639    pub stop_reason: Option<String>,
640    pub stop_sequence: Option<String>,
641}
642
643#[derive(Debug, Clone, Deserialize)]
644pub struct RawMessage {
645    pub id: Option<String>,
646    pub role: Option<String>,
647    pub model: Option<String>,
648    pub content: Option<Vec<RawContentBlock>>,
649}
650
651#[derive(Debug, Clone, Deserialize)]
652#[serde(tag = "type")]
653pub enum RawContentBlock {
654    #[serde(rename = "text")]
655    Text { text: String },
656
657    #[serde(rename = "thinking")]
658    Thinking {
659        thinking: String,
660        #[serde(default)]
661        signature: Option<String>,
662    },
663
664    #[serde(rename = "tool_use")]
665    ToolUse {
666        id: String,
667        name: String,
668        input: Value,
669    },
670
671    #[serde(rename = "tool_result")]
672    ToolResult {
673        tool_use_id: String,
674        content: Option<String>,
675        is_error: Option<bool>,
676    },
677}
678
679// ═══════════════════════════════════════════════════════════════════════════
680// ARBOR SOURCE OF TRUTH TYPES (Milestone 1)
681// These types enable storing conversation events as arbor nodes and rendering
682// them back into Claude API message format for time travel, forking, etc.
683// ═══════════════════════════════════════════════════════════════════════════
684
685/// Events stored as arbor text nodes - each event is a self-describing JSON blob
686/// that maps 1:1 to Claude API structures
687#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
688#[serde(tag = "type", rename_all = "snake_case")]
689pub enum NodeEvent {
690    /// User message node
691    #[serde(rename = "user_message")]
692    UserMessage { content: String },
693
694    /// Assistant turn start marker
695    #[serde(rename = "assistant_start")]
696    AssistantStart,
697
698    /// Text content block (child of assistant_start)
699    #[serde(rename = "content_text")]
700    ContentText { text: String },
701
702    /// Tool use block (child of assistant_start)
703    #[serde(rename = "content_tool_use")]
704    ContentToolUse {
705        id: String,
706        name: String,
707        input: Value,
708    },
709
710    /// Thinking block (child of assistant_start)
711    #[serde(rename = "content_thinking")]
712    ContentThinking { thinking: String },
713
714    /// Tool result message (becomes a user message in Claude API)
715    #[serde(rename = "user_tool_result")]
716    UserToolResult {
717        tool_use_id: String,
718        content: String,
719        is_error: bool,
720    },
721
722    /// Assistant turn complete marker
723    #[serde(rename = "assistant_complete")]
724    AssistantComplete { usage: Option<ChatUsage> },
725
726    /// The exact shell command used to launch Claude (for debugging)
727    #[serde(rename = "launch_command")]
728    LaunchCommand { command: String },
729
730    /// Stderr output captured from the Claude process (errors, warnings)
731    #[serde(rename = "claude_stderr")]
732    ClaudeStderr { text: String },
733}
734
735/// Claude API message format - what we render arbor nodes into
736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
737pub struct ClaudeMessage {
738    /// Role: "user" or "assistant"
739    pub role: String,
740    /// Message content blocks
741    pub content: Vec<ContentBlock>,
742}
743
744/// Content blocks within a Claude message
745#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
746#[serde(tag = "type", rename_all = "snake_case")]
747pub enum ContentBlock {
748    /// Text content
749    #[serde(rename = "text")]
750    Text { text: String },
751
752    /// Tool use
753    #[serde(rename = "tool_use")]
754    ToolUse {
755        id: String,
756        name: String,
757        input: Value,
758    },
759
760    /// Tool result
761    #[serde(rename = "tool_result")]
762    ToolResult {
763        tool_use_id: String,
764        content: String,
765        is_error: bool,
766    },
767
768    /// Thinking block
769    #[serde(rename = "thinking")]
770    Thinking { thinking: String },
771}
772
773/// Result of render_context method
774#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
775#[serde(tag = "type", rename_all = "snake_case")]
776pub enum RenderResult {
777    #[serde(rename = "ok")]
778    Ok { messages: Vec<ClaudeMessage> },
779    #[serde(rename = "error")]
780    Err { message: String },
781}
782
783/// Result of get_tree method
784#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
785#[serde(tag = "type", rename_all = "snake_case")]
786pub enum GetTreeResult {
787    #[serde(rename = "ok")]
788    Ok { tree_id: TreeId, head: NodeId },
789    #[serde(rename = "error")]
790    Err { message: String },
791}
792
793// ═══════════════════════════════════════════════════════════════════════════
794// SESSION FILE CRUD RESULTS
795// ═══════════════════════════════════════════════════════════════════════════
796
797/// Result of sessions_list method
798#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
799#[serde(tag = "type", rename_all = "snake_case")]
800pub enum SessionsListResult {
801    #[serde(rename = "ok")]
802    Ok { sessions: Vec<String> },
803    #[serde(rename = "error")]
804    Err { message: String },
805}
806
807/// Result of sessions_get method
808#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
809#[serde(tag = "type", rename_all = "snake_case")]
810pub enum SessionsGetResult {
811    #[serde(rename = "ok")]
812    Ok {
813        session_id: String,
814        event_count: usize,
815        events: Vec<serde_json::Value>,
816    },
817    #[serde(rename = "error")]
818    Err { message: String },
819}
820
821/// Result of sessions_import method
822#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
823#[serde(tag = "type", rename_all = "snake_case")]
824pub enum SessionsImportResult {
825    #[serde(rename = "ok")]
826    Ok { tree_id: TreeId, session_id: String },
827    #[serde(rename = "error")]
828    Err { message: String },
829}
830
831/// Result of sessions_export method
832#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
833#[serde(tag = "type", rename_all = "snake_case")]
834pub enum SessionsExportResult {
835    #[serde(rename = "ok")]
836    Ok { tree_id: TreeId, session_id: String },
837    #[serde(rename = "error")]
838    Err { message: String },
839}
840
841/// Result of sessions_delete method
842#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
843#[serde(tag = "type", rename_all = "snake_case")]
844pub enum SessionsDeleteResult {
845    #[serde(rename = "ok")]
846    Ok { session_id: String, deleted: bool },
847    #[serde(rename = "error")]
848    Err { message: String },
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854
855    #[test]
856    fn test_node_event_serialization() {
857        let event = NodeEvent::ContentText {
858            text: "Hello".to_string(),
859        };
860        let json = serde_json::to_string(&event).unwrap();
861        let parsed: NodeEvent = serde_json::from_str(&json).unwrap();
862        assert_eq!(event, parsed);
863    }
864
865    #[test]
866    fn test_claude_message_structure() {
867        let msg = ClaudeMessage {
868            role: "user".to_string(),
869            content: vec![ContentBlock::Text {
870                text: "test".to_string(),
871            }],
872        };
873        let json = serde_json::to_value(&msg).unwrap();
874        assert_eq!(json["role"], "user");
875        assert_eq!(json["content"][0]["type"], "text");
876    }
877
878    #[test]
879    fn test_json_schema_generation() {
880        use schemars::schema_for;
881
882        // Test that all new types generate schemas without panicking
883        let _schema = schema_for!(NodeEvent);
884        let _schema = schema_for!(ClaudeMessage);
885        let _schema = schema_for!(ContentBlock);
886        let _schema = schema_for!(RenderResult);
887        let _schema = schema_for!(GetTreeResult);
888    }
889
890    #[test]
891    fn test_all_node_event_variants() {
892        // Test serialization of all NodeEvent variants
893        let events = vec![
894            NodeEvent::UserMessage {
895                content: "Hello".to_string(),
896            },
897            NodeEvent::AssistantStart,
898            NodeEvent::ContentText {
899                text: "Response".to_string(),
900            },
901            NodeEvent::ContentToolUse {
902                id: "tool_123".to_string(),
903                name: "Write".to_string(),
904                input: serde_json::json!({"file": "test.txt"}),
905            },
906            NodeEvent::ContentThinking {
907                thinking: "Let me think...".to_string(),
908            },
909            NodeEvent::UserToolResult {
910                tool_use_id: "tool_123".to_string(),
911                content: "Success".to_string(),
912                is_error: false,
913            },
914            NodeEvent::AssistantComplete {
915                usage: Some(ChatUsage {
916                    input_tokens: Some(100),
917                    output_tokens: Some(200),
918                    cost_usd: Some(0.01),
919                    num_turns: Some(1),
920                }),
921            },
922        ];
923
924        for event in events {
925            let json = serde_json::to_string(&event).unwrap();
926            let parsed: NodeEvent = serde_json::from_str(&json).unwrap();
927            assert_eq!(event, parsed);
928        }
929    }
930
931    #[test]
932    fn test_all_content_block_variants() {
933        // Test serialization of all ContentBlock variants
934        let blocks = vec![
935            ContentBlock::Text {
936                text: "Hello".to_string(),
937            },
938            ContentBlock::ToolUse {
939                id: "tool_456".to_string(),
940                name: "Bash".to_string(),
941                input: serde_json::json!({"command": "ls"}),
942            },
943            ContentBlock::ToolResult {
944                tool_use_id: "tool_456".to_string(),
945                content: "file1.txt\nfile2.txt".to_string(),
946                is_error: false,
947            },
948            ContentBlock::Thinking {
949                thinking: "Analyzing...".to_string(),
950            },
951        ];
952
953        for block in blocks {
954            let json = serde_json::to_string(&block).unwrap();
955            let parsed: ContentBlock = serde_json::from_str(&json).unwrap();
956            assert_eq!(block, parsed);
957        }
958    }
959
960    #[test]
961    fn test_node_event_json_format() {
962        // Verify that NodeEvent produces the expected JSON structure
963        let event = NodeEvent::ContentToolUse {
964            id: "toolu_123".to_string(),
965            name: "Write".to_string(),
966            input: serde_json::json!({"path": "/tmp/test.txt"}),
967        };
968        let json = serde_json::to_value(&event).unwrap();
969        assert_eq!(json["type"], "content_tool_use");
970        assert_eq!(json["id"], "toolu_123");
971        assert_eq!(json["name"], "Write");
972        assert_eq!(json["input"]["path"], "/tmp/test.txt");
973    }
974
975    #[test]
976    fn test_render_result_variants() {
977        // Test RenderResult::Ok
978        let result = RenderResult::Ok {
979            messages: vec![ClaudeMessage {
980                role: "user".to_string(),
981                content: vec![ContentBlock::Text {
982                    text: "test".to_string(),
983                }],
984            }],
985        };
986        let json = serde_json::to_value(&result).unwrap();
987        assert_eq!(json["type"], "ok");
988        assert!(json["messages"].is_array());
989
990        // Test RenderResult::Err
991        let result = RenderResult::Err {
992            message: "test error".to_string(),
993        };
994        let json = serde_json::to_value(&result).unwrap();
995        assert_eq!(json["type"], "error");
996        assert_eq!(json["message"], "test error");
997    }
998
999    #[test]
1000    fn test_get_tree_result_variants() {
1001        use crate::activations::arbor::{NodeId, TreeId};
1002
1003        // Test GetTreeResult::Ok
1004        let tree_id = TreeId::new();
1005        let node_id = NodeId::new();
1006        let result = GetTreeResult::Ok {
1007            tree_id,
1008            head: node_id,
1009        };
1010        let json = serde_json::to_value(&result).unwrap();
1011        assert_eq!(json["type"], "ok");
1012
1013        // Test GetTreeResult::Err
1014        let result = GetTreeResult::Err {
1015            message: "not found".to_string(),
1016        };
1017        let json = serde_json::to_value(&result).unwrap();
1018        assert_eq!(json["type"], "error");
1019        assert_eq!(json["message"], "not found");
1020    }
1021}