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
10pub type ClaudeCodeId = Uuid;
12
13#[derive(Debug, Clone, HandleEnum)]
22#[handle(plugin_id = "ClaudeCode::PLUGIN_ID", version = "1.0.0")]
23pub enum ClaudeCodeHandle {
24 #[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: String,
36 role: String,
38 name: String,
40 },
41
42 #[handle(method = "passthrough")]
46 Passthrough {
47 event_id: String,
49 event_type: String,
51 },
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60#[serde(tag = "type")]
61pub enum ResolveResult {
62 #[serde(rename = "resolved_message")]
64 Message {
65 id: String,
66 role: String,
67 content: String,
68 model: Option<String>,
69 name: String,
70 },
71 #[serde(rename = "error")]
73 Error { message: String },
74}
75
76pub type StreamId = Uuid;
78
79pub type MessageId = Uuid;
81
82#[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#[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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
141pub struct Position {
142 pub tree_id: TreeId,
144 pub node_id: NodeId,
146}
147
148impl Position {
149 pub fn new(tree_id: TreeId, node_id: NodeId) -> Self {
151 Self { tree_id, node_id }
152 }
153
154 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#[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 pub model_id: Option<String>,
173 pub input_tokens: Option<i64>,
175 pub output_tokens: Option<i64>,
176 pub cost_usd: Option<f64>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
182pub struct ClaudeCodeConfig {
183 pub id: ClaudeCodeId,
185 pub name: String,
187 pub claude_session_id: Option<String>,
189 pub head: Position,
191 pub working_dir: String,
193 pub model: Model,
195 pub system_prompt: Option<String>,
197 pub mcp_config: Option<Value>,
199 pub loopback_enabled: bool,
201 pub metadata: Option<Value>,
203 pub created_at: i64,
205 pub updated_at: i64,
207}
208
209impl ClaudeCodeConfig {
210 pub fn tree_id(&self) -> TreeId {
212 self.head.tree_id
213 }
214
215 pub fn node_id(&self) -> NodeId {
217 self.head.node_id
218 }
219}
220
221#[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#[derive(Debug, Clone, PartialEq, 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
264#[serde(rename_all = "snake_case")]
265pub enum StreamStatus {
266 Running,
268 AwaitingPermission,
270 Complete,
272 Failed,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
278pub struct StreamInfo {
279 pub stream_id: StreamId,
281 pub session_id: ClaudeCodeId,
283 pub status: StreamStatus,
285 pub user_position: Option<Position>,
287 pub event_count: u64,
289 pub read_position: u64,
291 pub started_at: i64,
293 pub ended_at: Option<i64>,
295 pub error: Option<String>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
301pub struct BufferedEvent {
302 pub seq: u64,
304 pub event: ChatEvent,
306 pub timestamp: i64,
308}
309
310#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
386#[serde(tag = "type", rename_all = "snake_case")]
387pub enum PollResult {
388 #[serde(rename = "ok")]
389 Ok {
390 status: StreamStatus,
392 events: Vec<BufferedEvent>,
394 read_position: u64,
396 total_events: u64,
398 has_more: bool,
400 },
401 #[serde(rename = "error")]
402 Err { message: String },
403}
404
405#[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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
421#[serde(tag = "type", rename_all = "snake_case")]
422pub enum ChatEvent {
423 #[serde(rename = "start")]
425 Start {
426 id: ClaudeCodeId,
427 user_position: Position,
428 },
429
430 #[serde(rename = "content")]
432 Content { text: String },
433
434 #[serde(rename = "thinking")]
436 Thinking { thinking: String },
437
438 #[serde(rename = "tool_use")]
440 ToolUse {
441 tool_name: String,
442 tool_use_id: String,
443 input: Value,
444 },
445
446 #[serde(rename = "tool_result")]
448 ToolResult {
449 tool_use_id: String,
450 output: String,
451 is_error: bool,
452 },
453
454 #[serde(rename = "complete")]
456 Complete {
457 new_head: Position,
458 claude_session_id: String,
459 usage: Option<ChatUsage>,
460 },
461
462 #[serde(rename = "passthrough")]
465 Passthrough {
466 event_type: String,
467 handle: String,
468 data: Value,
469 },
470
471 #[serde(rename = "error")]
473 Err { message: String },
474}
475
476#[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#[derive(Debug, Clone, Deserialize)]
508#[serde(tag = "type")]
509pub enum RawClaudeEvent {
510 #[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 #[serde(rename = "assistant")]
523 Assistant {
524 message: Option<RawMessage>,
525 },
526
527 #[serde(rename = "user")]
529 User {
530 message: Option<RawMessage>,
531 },
532
533 #[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 #[serde(rename = "stream_event")]
548 StreamEvent {
549 event: StreamEventInner,
550 session_id: Option<String>,
551 },
552
553 #[serde(skip)]
556 Unknown {
557 event_type: String,
558 data: Value,
559 },
560}
561
562#[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}
668
669#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
678#[serde(tag = "type", rename_all = "snake_case")]
679pub enum NodeEvent {
680 #[serde(rename = "user_message")]
682 UserMessage { content: String },
683
684 #[serde(rename = "assistant_start")]
686 AssistantStart,
687
688 #[serde(rename = "content_text")]
690 ContentText { text: String },
691
692 #[serde(rename = "content_tool_use")]
694 ContentToolUse {
695 id: String,
696 name: String,
697 input: Value,
698 },
699
700 #[serde(rename = "content_thinking")]
702 ContentThinking { thinking: String },
703
704 #[serde(rename = "user_tool_result")]
706 UserToolResult {
707 tool_use_id: String,
708 content: String,
709 is_error: bool,
710 },
711
712 #[serde(rename = "assistant_complete")]
714 AssistantComplete { usage: Option<ChatUsage> },
715}
716
717#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
719pub struct ClaudeMessage {
720 pub role: String,
722 pub content: Vec<ContentBlock>,
724}
725
726#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
728#[serde(tag = "type", rename_all = "snake_case")]
729pub enum ContentBlock {
730 #[serde(rename = "text")]
732 Text { text: String },
733
734 #[serde(rename = "tool_use")]
736 ToolUse {
737 id: String,
738 name: String,
739 input: Value,
740 },
741
742 #[serde(rename = "tool_result")]
744 ToolResult {
745 tool_use_id: String,
746 content: String,
747 is_error: bool,
748 },
749
750 #[serde(rename = "thinking")]
752 Thinking { thinking: String },
753}
754
755#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
757#[serde(tag = "type", rename_all = "snake_case")]
758pub enum RenderResult {
759 #[serde(rename = "ok")]
760 Ok { messages: Vec<ClaudeMessage> },
761 #[serde(rename = "error")]
762 Err { message: String },
763}
764
765#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
767#[serde(tag = "type", rename_all = "snake_case")]
768pub enum GetTreeResult {
769 #[serde(rename = "ok")]
770 Ok { tree_id: TreeId, head: NodeId },
771 #[serde(rename = "error")]
772 Err { message: String },
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
781#[serde(tag = "type", rename_all = "snake_case")]
782pub enum SessionsListResult {
783 #[serde(rename = "ok")]
784 Ok { sessions: Vec<String> },
785 #[serde(rename = "error")]
786 Err { message: String },
787}
788
789#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
791#[serde(tag = "type", rename_all = "snake_case")]
792pub enum SessionsGetResult {
793 #[serde(rename = "ok")]
794 Ok {
795 session_id: String,
796 event_count: usize,
797 events: Vec<serde_json::Value>,
798 },
799 #[serde(rename = "error")]
800 Err { message: String },
801}
802
803#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
805#[serde(tag = "type", rename_all = "snake_case")]
806pub enum SessionsImportResult {
807 #[serde(rename = "ok")]
808 Ok { tree_id: TreeId, session_id: String },
809 #[serde(rename = "error")]
810 Err { message: String },
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
815#[serde(tag = "type", rename_all = "snake_case")]
816pub enum SessionsExportResult {
817 #[serde(rename = "ok")]
818 Ok { tree_id: TreeId, session_id: String },
819 #[serde(rename = "error")]
820 Err { message: String },
821}
822
823#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
825#[serde(tag = "type", rename_all = "snake_case")]
826pub enum SessionsDeleteResult {
827 #[serde(rename = "ok")]
828 Ok { session_id: String, deleted: bool },
829 #[serde(rename = "error")]
830 Err { message: String },
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn test_node_event_serialization() {
839 let event = NodeEvent::ContentText {
840 text: "Hello".to_string(),
841 };
842 let json = serde_json::to_string(&event).unwrap();
843 let parsed: NodeEvent = serde_json::from_str(&json).unwrap();
844 assert_eq!(event, parsed);
845 }
846
847 #[test]
848 fn test_claude_message_structure() {
849 let msg = ClaudeMessage {
850 role: "user".to_string(),
851 content: vec![ContentBlock::Text {
852 text: "test".to_string(),
853 }],
854 };
855 let json = serde_json::to_value(&msg).unwrap();
856 assert_eq!(json["role"], "user");
857 assert_eq!(json["content"][0]["type"], "text");
858 }
859
860 #[test]
861 fn test_json_schema_generation() {
862 use schemars::schema_for;
863
864 let _schema = schema_for!(NodeEvent);
866 let _schema = schema_for!(ClaudeMessage);
867 let _schema = schema_for!(ContentBlock);
868 let _schema = schema_for!(RenderResult);
869 let _schema = schema_for!(GetTreeResult);
870 }
871
872 #[test]
873 fn test_all_node_event_variants() {
874 let events = vec![
876 NodeEvent::UserMessage {
877 content: "Hello".to_string(),
878 },
879 NodeEvent::AssistantStart,
880 NodeEvent::ContentText {
881 text: "Response".to_string(),
882 },
883 NodeEvent::ContentToolUse {
884 id: "tool_123".to_string(),
885 name: "Write".to_string(),
886 input: serde_json::json!({"file": "test.txt"}),
887 },
888 NodeEvent::ContentThinking {
889 thinking: "Let me think...".to_string(),
890 },
891 NodeEvent::UserToolResult {
892 tool_use_id: "tool_123".to_string(),
893 content: "Success".to_string(),
894 is_error: false,
895 },
896 NodeEvent::AssistantComplete {
897 usage: Some(ChatUsage {
898 input_tokens: Some(100),
899 output_tokens: Some(200),
900 cost_usd: Some(0.01),
901 num_turns: Some(1),
902 }),
903 },
904 ];
905
906 for event in events {
907 let json = serde_json::to_string(&event).unwrap();
908 let parsed: NodeEvent = serde_json::from_str(&json).unwrap();
909 assert_eq!(event, parsed);
910 }
911 }
912
913 #[test]
914 fn test_all_content_block_variants() {
915 let blocks = vec![
917 ContentBlock::Text {
918 text: "Hello".to_string(),
919 },
920 ContentBlock::ToolUse {
921 id: "tool_456".to_string(),
922 name: "Bash".to_string(),
923 input: serde_json::json!({"command": "ls"}),
924 },
925 ContentBlock::ToolResult {
926 tool_use_id: "tool_456".to_string(),
927 content: "file1.txt\nfile2.txt".to_string(),
928 is_error: false,
929 },
930 ContentBlock::Thinking {
931 thinking: "Analyzing...".to_string(),
932 },
933 ];
934
935 for block in blocks {
936 let json = serde_json::to_string(&block).unwrap();
937 let parsed: ContentBlock = serde_json::from_str(&json).unwrap();
938 assert_eq!(block, parsed);
939 }
940 }
941
942 #[test]
943 fn test_node_event_json_format() {
944 let event = NodeEvent::ContentToolUse {
946 id: "toolu_123".to_string(),
947 name: "Write".to_string(),
948 input: serde_json::json!({"path": "/tmp/test.txt"}),
949 };
950 let json = serde_json::to_value(&event).unwrap();
951 assert_eq!(json["type"], "content_tool_use");
952 assert_eq!(json["id"], "toolu_123");
953 assert_eq!(json["name"], "Write");
954 assert_eq!(json["input"]["path"], "/tmp/test.txt");
955 }
956
957 #[test]
958 fn test_render_result_variants() {
959 let result = RenderResult::Ok {
961 messages: vec![ClaudeMessage {
962 role: "user".to_string(),
963 content: vec![ContentBlock::Text {
964 text: "test".to_string(),
965 }],
966 }],
967 };
968 let json = serde_json::to_value(&result).unwrap();
969 assert_eq!(json["type"], "ok");
970 assert!(json["messages"].is_array());
971
972 let result = RenderResult::Err {
974 message: "test error".to_string(),
975 };
976 let json = serde_json::to_value(&result).unwrap();
977 assert_eq!(json["type"], "error");
978 assert_eq!(json["message"], "test error");
979 }
980
981 #[test]
982 fn test_get_tree_result_variants() {
983 use crate::activations::arbor::{NodeId, TreeId};
984
985 let tree_id = TreeId::new();
987 let node_id = NodeId::new();
988 let result = GetTreeResult::Ok {
989 tree_id,
990 head: node_id,
991 };
992 let json = serde_json::to_value(&result).unwrap();
993 assert_eq!(json["type"], "ok");
994
995 let result = GetTreeResult::Err {
997 message: "not found".to_string(),
998 };
999 let json = serde_json::to_value(&result).unwrap();
1000 assert_eq!(json["type"], "error");
1001 assert_eq!(json["message"], "not found");
1002 }
1003}