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 uuid::Uuid;
7
8use super::activation::ClaudeCode;
9
10/// Unique identifier for a ClaudeCode session
11pub type ClaudeCodeId = Uuid;
12
13// ============================================================================
14// Handle types for ClaudeCode activation
15// ============================================================================
16
17/// Type-safe handles for ClaudeCode activation data
18///
19/// Handles reference data stored in the ClaudeCode database and can be embedded
20/// in Arbor tree nodes for external resolution.
21#[derive(Debug, Clone, HandleEnum)]
22#[handle(plugin_id = "ClaudeCode::PLUGIN_ID", version = "1.0.0")]
23pub enum ClaudeCodeHandle {
24    /// Handle to a message in the claudecode database
25    /// Format: `{plugin_id}@1.0.0::chat:msg-{uuid}:{role}:{name}`
26    #[handle(
27        method = "chat",
28        table = "messages",
29        key = "id",
30        key_field = "message_id",
31        strip_prefix = "msg-"
32    )]
33    Message {
34        /// Message ID with "msg-" prefix (e.g., "msg-550e8400-...")
35        message_id: String,
36        /// Role: "user", "assistant", or "system"
37        role: String,
38        /// Display name
39        name: String,
40    },
41
42    /// Handle to an unknown/passthrough event
43    /// Format: `{plugin_id}@1.0.0::passthrough:{event_id}:{event_type}`
44    /// Note: No resolution - passthrough events are inline only
45    #[handle(method = "passthrough")]
46    Passthrough {
47        /// Event ID
48        event_id: String,
49        /// Event type string
50        event_type: String,
51    },
52}
53
54// ============================================================================
55// Handle resolution result types
56// ============================================================================
57
58/// Result of resolving a ClaudeCode handle
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60#[serde(tag = "type")]
61pub enum ResolveResult {
62    /// Successfully resolved message
63    #[serde(rename = "resolved_message")]
64    Message {
65        id: String,
66        role: String,
67        content: String,
68        model: Option<String>,
69        name: String,
70    },
71    /// Resolution error
72    #[serde(rename = "error")]
73    Error { message: String },
74}
75
76/// Unique identifier for an active stream
77pub type StreamId = Uuid;
78
79/// Unique identifier for a message
80pub type MessageId = Uuid;
81
82/// Role of a message sender
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84#[serde(rename_all = "snake_case")]
85pub enum MessageRole {
86    User,
87    Assistant,
88    System,
89}
90
91impl MessageRole {
92    pub fn as_str(&self) -> &'static str {
93        match self {
94            MessageRole::User => "user",
95            MessageRole::Assistant => "assistant",
96            MessageRole::System => "system",
97        }
98    }
99
100    pub fn from_str(s: &str) -> Option<Self> {
101        match s {
102            "user" => Some(MessageRole::User),
103            "assistant" => Some(MessageRole::Assistant),
104            "system" => Some(MessageRole::System),
105            _ => None,
106        }
107    }
108}
109
110/// Model selection for Claude Code
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
112#[serde(rename_all = "lowercase")]
113pub enum Model {
114    Opus,
115    Sonnet,
116    Haiku,
117}
118
119impl Model {
120    pub fn as_str(&self) -> &'static str {
121        match self {
122            Model::Opus => "opus",
123            Model::Sonnet => "sonnet",
124            Model::Haiku => "haiku",
125        }
126    }
127
128    pub fn from_str(s: &str) -> Option<Self> {
129        match s.to_lowercase().as_str() {
130            "opus" => Some(Model::Opus),
131            "sonnet" => Some(Model::Sonnet),
132            "haiku" => Some(Model::Haiku),
133            _ => None,
134        }
135    }
136}
137
138/// A position in the context tree - couples tree_id and node_id together.
139/// Same structure as Cone's Position for consistency.
140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
141pub struct Position {
142    /// The tree containing this position
143    pub tree_id: TreeId,
144    /// The specific node within the tree
145    pub node_id: NodeId,
146}
147
148impl Position {
149    /// Create a new position
150    pub fn new(tree_id: TreeId, node_id: NodeId) -> Self {
151        Self { tree_id, node_id }
152    }
153
154    /// Advance to a new node in the same tree
155    pub fn advance(&self, new_node_id: NodeId) -> Self {
156        Self {
157            tree_id: self.tree_id,
158            node_id: new_node_id,
159        }
160    }
161}
162
163/// A message stored in the claudecode database
164#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
165pub struct Message {
166    pub id: MessageId,
167    pub session_id: ClaudeCodeId,
168    pub role: MessageRole,
169    pub content: String,
170    pub created_at: i64,
171    /// Model used (for assistant messages)
172    pub model_id: Option<String>,
173    /// Token usage (for assistant messages)
174    pub input_tokens: Option<i64>,
175    pub output_tokens: Option<i64>,
176    /// Cost in USD (from Claude Code)
177    pub cost_usd: Option<f64>,
178}
179
180/// ClaudeCode session configuration
181#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
182pub struct ClaudeCodeConfig {
183    /// Unique identifier for this session
184    pub id: ClaudeCodeId,
185    /// Human-readable name
186    pub name: String,
187    /// Claude Code's internal session ID (for --resume)
188    pub claude_session_id: Option<String>,
189    /// The canonical head - current position in conversation tree
190    pub head: Position,
191    /// Working directory for Claude Code
192    pub working_dir: String,
193    /// Model to use
194    pub model: Model,
195    /// System prompt / instructions
196    pub system_prompt: Option<String>,
197    /// MCP server configuration (JSON)
198    pub mcp_config: Option<Value>,
199    /// Enable loopback mode - routes tool permissions through parent for approval
200    pub loopback_enabled: bool,
201    /// Additional metadata
202    pub metadata: Option<Value>,
203    /// Created timestamp
204    pub created_at: i64,
205    /// Last updated timestamp
206    pub updated_at: i64,
207}
208
209impl ClaudeCodeConfig {
210    /// Get the tree ID (convenience accessor)
211    pub fn tree_id(&self) -> TreeId {
212        self.head.tree_id
213    }
214
215    /// Get the current node ID (convenience accessor)
216    pub fn node_id(&self) -> NodeId {
217        self.head.node_id
218    }
219}
220
221/// Lightweight session info (for listing)
222#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
223pub struct ClaudeCodeInfo {
224    pub id: ClaudeCodeId,
225    pub name: String,
226    pub model: Model,
227    pub head: Position,
228    pub claude_session_id: Option<String>,
229    pub working_dir: String,
230    pub loopback_enabled: bool,
231    pub created_at: i64,
232}
233
234impl From<&ClaudeCodeConfig> for ClaudeCodeInfo {
235    fn from(config: &ClaudeCodeConfig) -> Self {
236        Self {
237            id: config.id,
238            name: config.name.clone(),
239            model: config.model,
240            head: config.head,
241            claude_session_id: config.claude_session_id.clone(),
242            working_dir: config.working_dir.clone(),
243            loopback_enabled: config.loopback_enabled,
244            created_at: config.created_at,
245        }
246    }
247}
248
249/// Token usage information
250#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
251pub struct ChatUsage {
252    pub input_tokens: Option<u64>,
253    pub output_tokens: Option<u64>,
254    pub cost_usd: Option<f64>,
255    pub num_turns: Option<i32>,
256}
257
258// ═══════════════════════════════════════════════════════════════════════════
259// STREAM MANAGEMENT TYPES (for non-blocking chat with loopback)
260// ═══════════════════════════════════════════════════════════════════════════
261
262/// Status of an active stream
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
264#[serde(rename_all = "snake_case")]
265pub enum StreamStatus {
266    /// Stream is actively receiving events
267    Running,
268    /// Stream is waiting for tool permission approval
269    AwaitingPermission,
270    /// Stream completed successfully
271    Complete,
272    /// Stream failed with an error
273    Failed,
274}
275
276/// Information about an active stream
277#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
278pub struct StreamInfo {
279    /// Unique stream identifier
280    pub stream_id: StreamId,
281    /// Session this stream belongs to
282    pub session_id: ClaudeCodeId,
283    /// Current status
284    pub status: StreamStatus,
285    /// Position of the user message node (set at start)
286    pub user_position: Option<Position>,
287    /// Number of events buffered
288    pub event_count: u64,
289    /// Read position (how many events have been consumed)
290    pub read_position: u64,
291    /// When the stream started
292    pub started_at: i64,
293    /// When the stream ended (if complete/failed)
294    pub ended_at: Option<i64>,
295    /// Error message if failed
296    pub error: Option<String>,
297}
298
299/// A buffered event in the stream
300#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
301pub struct BufferedEvent {
302    /// Sequence number within the stream
303    pub seq: u64,
304    /// The chat event
305    pub event: ChatEvent,
306    /// Timestamp when event was received
307    pub timestamp: i64,
308}
309
310// ═══════════════════════════════════════════════════════════════════════════
311// METHOD-SPECIFIC RETURN TYPES
312// Each method returns exactly what it needs - no shared enums
313// ═══════════════════════════════════════════════════════════════════════════
314
315/// Result of creating a session
316#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
317#[serde(tag = "type", rename_all = "snake_case")]
318pub enum CreateResult {
319    #[serde(rename = "created")]
320    Ok {
321        id: ClaudeCodeId,
322        head: Position,
323    },
324    #[serde(rename = "error")]
325    Err { message: String },
326}
327
328/// Result of getting a session
329#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
330#[serde(tag = "type", rename_all = "snake_case")]
331pub enum GetResult {
332    #[serde(rename = "ok")]
333    Ok { config: ClaudeCodeConfig },
334    #[serde(rename = "error")]
335    Err { message: String },
336}
337
338/// Result of listing sessions
339#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
340#[serde(tag = "type", rename_all = "snake_case")]
341pub enum ListResult {
342    #[serde(rename = "ok")]
343    Ok { sessions: Vec<ClaudeCodeInfo> },
344    #[serde(rename = "error")]
345    Err { message: String },
346}
347
348/// Result of deleting a session
349#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
350#[serde(tag = "type", rename_all = "snake_case")]
351pub enum DeleteResult {
352    #[serde(rename = "deleted")]
353    Ok { id: ClaudeCodeId },
354    #[serde(rename = "error")]
355    Err { message: String },
356}
357
358/// Result of forking a session
359#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
360#[serde(tag = "type", rename_all = "snake_case")]
361pub enum ForkResult {
362    #[serde(rename = "forked")]
363    Ok {
364        id: ClaudeCodeId,
365        head: Position,
366    },
367    #[serde(rename = "error")]
368    Err { message: String },
369}
370
371/// Result of starting an async chat (non-blocking)
372#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
373#[serde(tag = "type", rename_all = "snake_case")]
374pub enum ChatStartResult {
375    #[serde(rename = "started")]
376    Ok {
377        stream_id: StreamId,
378        session_id: ClaudeCodeId,
379    },
380    #[serde(rename = "error")]
381    Err { message: String },
382}
383
384/// Result of polling a stream for events
385#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
386#[serde(tag = "type", rename_all = "snake_case")]
387pub enum PollResult {
388    #[serde(rename = "ok")]
389    Ok {
390        /// Current stream status
391        status: StreamStatus,
392        /// Events since last poll (or from specified offset)
393        events: Vec<BufferedEvent>,
394        /// Current read position after this poll
395        read_position: u64,
396        /// Total events in buffer
397        total_events: u64,
398        /// True if there are more events available
399        has_more: bool,
400    },
401    #[serde(rename = "error")]
402    Err { message: String },
403}
404
405/// Result of listing active streams
406#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
407#[serde(tag = "type", rename_all = "snake_case")]
408pub enum StreamListResult {
409    #[serde(rename = "ok")]
410    Ok { streams: Vec<StreamInfo> },
411    #[serde(rename = "error")]
412    Err { message: String },
413}
414
415// ═══════════════════════════════════════════════════════════════════════════
416// CHAT EVENTS - Streaming conversation (needs enum for multiple event types)
417// ═══════════════════════════════════════════════════════════════════════════
418
419/// Events emitted during chat streaming
420#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
421#[serde(tag = "type", rename_all = "snake_case")]
422pub enum ChatEvent {
423    /// Chat started - user message stored, streaming begins
424    #[serde(rename = "start")]
425    Start {
426        id: ClaudeCodeId,
427        user_position: Position,
428    },
429
430    /// Content chunk (streaming tokens)
431    #[serde(rename = "content")]
432    Content { text: String },
433
434    /// Thinking block - Claude's internal reasoning
435    #[serde(rename = "thinking")]
436    Thinking { thinking: String },
437
438    /// Tool use detected
439    #[serde(rename = "tool_use")]
440    ToolUse {
441        tool_name: String,
442        tool_use_id: String,
443        input: Value,
444    },
445
446    /// Tool result received
447    #[serde(rename = "tool_result")]
448    ToolResult {
449        tool_use_id: String,
450        output: String,
451        is_error: bool,
452    },
453
454    /// Chat complete - response stored, head updated
455    #[serde(rename = "complete")]
456    Complete {
457        new_head: Position,
458        claude_session_id: String,
459        usage: Option<ChatUsage>,
460    },
461
462    /// Passthrough for unrecognized Claude Code events
463    /// Data is stored separately (referenced by handle) and also forwarded inline
464    #[serde(rename = "passthrough")]
465    Passthrough {
466        event_type: String,
467        handle: String,
468        data: Value,
469    },
470
471    /// Error during chat
472    #[serde(rename = "error")]
473    Err { message: String },
474}
475
476/// Error type for ClaudeCode operations
477#[derive(Debug, Clone)]
478pub struct ClaudeCodeError {
479    pub message: String,
480}
481
482impl std::fmt::Display for ClaudeCodeError {
483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484        write!(f, "{}", self.message)
485    }
486}
487
488impl std::error::Error for ClaudeCodeError {}
489
490impl From<String> for ClaudeCodeError {
491    fn from(s: String) -> Self {
492        Self { message: s }
493    }
494}
495
496impl From<&str> for ClaudeCodeError {
497    fn from(s: &str) -> Self {
498        Self { message: s.to_string() }
499    }
500}
501
502// ═══════════════════════════════════════════════════════════════════════════
503// Raw events from Claude Code CLI (for parsing stream-json output)
504// ═══════════════════════════════════════════════════════════════════════════
505
506/// Raw events from Claude Code's stream-json output
507#[derive(Debug, Clone, Deserialize)]
508#[serde(tag = "type")]
509pub enum RawClaudeEvent {
510    /// System initialization event
511    #[serde(rename = "system")]
512    System {
513        subtype: Option<String>,
514        #[serde(rename = "session_id")]
515        session_id: Option<String>,
516        model: Option<String>,
517        cwd: Option<String>,
518        tools: Option<Vec<String>>,
519    },
520
521    /// Assistant message event
522    #[serde(rename = "assistant")]
523    Assistant {
524        message: Option<RawMessage>,
525    },
526
527    /// User message event
528    #[serde(rename = "user")]
529    User {
530        message: Option<RawMessage>,
531    },
532
533    /// Result event (session complete)
534    #[serde(rename = "result")]
535    Result {
536        subtype: Option<String>,
537        session_id: Option<String>,
538        cost_usd: Option<f64>,
539        is_error: Option<bool>,
540        duration_ms: Option<i64>,
541        num_turns: Option<i32>,
542        result: Option<String>,
543        error: Option<String>,
544    },
545
546    /// Stream event (partial message chunks from --include-partial-messages)
547    #[serde(rename = "stream_event")]
548    StreamEvent {
549        event: StreamEventInner,
550        session_id: Option<String>,
551    },
552
553    /// Unknown event type - captures events we don't recognize
554    /// This is constructed manually in executor.rs, not via serde
555    #[serde(skip)]
556    Unknown {
557        event_type: String,
558        data: Value,
559    },
560}
561
562/// Inner event types for stream_event
563#[derive(Debug, Clone, Deserialize)]
564#[serde(tag = "type")]
565pub enum StreamEventInner {
566    #[serde(rename = "message_start")]
567    MessageStart {
568        message: Option<StreamMessage>,
569    },
570
571    #[serde(rename = "content_block_start")]
572    ContentBlockStart {
573        index: usize,
574        content_block: Option<StreamContentBlock>,
575    },
576
577    #[serde(rename = "content_block_delta")]
578    ContentBlockDelta {
579        index: usize,
580        delta: StreamDelta,
581    },
582
583    #[serde(rename = "content_block_stop")]
584    ContentBlockStop {
585        index: usize,
586    },
587
588    #[serde(rename = "message_delta")]
589    MessageDelta {
590        delta: MessageDeltaInfo,
591    },
592
593    #[serde(rename = "message_stop")]
594    MessageStop,
595}
596
597#[derive(Debug, Clone, Deserialize)]
598pub struct StreamMessage {
599    pub model: Option<String>,
600    pub role: Option<String>,
601}
602
603#[derive(Debug, Clone, Deserialize)]
604#[serde(tag = "type")]
605pub enum StreamContentBlock {
606    #[serde(rename = "text")]
607    Text { text: Option<String> },
608
609    #[serde(rename = "tool_use")]
610    ToolUse {
611        id: String,
612        name: String,
613        input: Option<Value>,
614    },
615}
616
617#[derive(Debug, Clone, Deserialize)]
618#[serde(tag = "type")]
619pub enum StreamDelta {
620    #[serde(rename = "text_delta")]
621    TextDelta { text: String },
622
623    #[serde(rename = "input_json_delta")]
624    InputJsonDelta { partial_json: String },
625}
626
627#[derive(Debug, Clone, Deserialize)]
628pub struct MessageDeltaInfo {
629    pub stop_reason: Option<String>,
630    pub stop_sequence: Option<String>,
631}
632
633#[derive(Debug, Clone, Deserialize)]
634pub struct RawMessage {
635    pub id: Option<String>,
636    pub role: Option<String>,
637    pub model: Option<String>,
638    pub content: Option<Vec<RawContentBlock>>,
639}
640
641#[derive(Debug, Clone, Deserialize)]
642#[serde(tag = "type")]
643pub enum RawContentBlock {
644    #[serde(rename = "text")]
645    Text { text: String },
646
647    #[serde(rename = "thinking")]
648    Thinking {
649        thinking: String,
650        #[serde(default)]
651        signature: Option<String>,
652    },
653
654    #[serde(rename = "tool_use")]
655    ToolUse {
656        id: String,
657        name: String,
658        input: Value,
659    },
660
661    #[serde(rename = "tool_result")]
662    ToolResult {
663        tool_use_id: String,
664        content: Option<String>,
665        is_error: Option<bool>,
666    },
667}