1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::shared::SessionError;
8use crate::{
9 client::{Opencode, RequestOptions},
10 error::OpencodeError,
11};
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct Session {
20 pub id: String,
22 #[serde(default)]
24 pub slug: String,
25 #[serde(rename = "projectID", default)]
27 pub project_id: String,
28 #[serde(default)]
30 pub directory: String,
31 pub time: SessionTime,
33 pub title: String,
35 pub version: String,
37 #[serde(rename = "parentID")]
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub parent_id: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub revert: Option<SessionRevert>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub share: Option<SessionShare>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub summary: Option<SessionSummary>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub permission: Option<PermissionRuleset>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct SessionTime {
58 pub created: f64,
60 pub updated: f64,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub compacting: Option<f64>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub archived: Option<f64>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72pub struct SessionRevert {
73 #[serde(rename = "messageID")]
75 pub message_id: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub diff: Option<String>,
79 #[serde(rename = "partID")]
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub part_id: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub snapshot: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90pub struct SessionShare {
91 pub url: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
97#[serde(rename_all = "lowercase")]
98pub enum FileDiffStatus {
99 Added,
101 Deleted,
103 Modified,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct FileDiff {
110 pub file: String,
112 pub before: String,
114 pub after: String,
116 pub additions: f64,
118 pub deletions: f64,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub status: Option<FileDiffStatus>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127pub struct SessionSummary {
128 pub additions: f64,
130 pub deletions: f64,
132 pub files: f64,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub diffs: Option<Vec<FileDiff>>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct PermissionRule {
142 pub permission: String,
144 pub pattern: String,
146 pub action: String,
148}
149
150pub type PermissionRuleset = Vec<PermissionRule>;
152
153#[allow(clippy::derive_partial_eq_without_eq)]
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
160#[serde(tag = "type")]
161pub enum OutputFormat {
162 #[serde(rename = "text")]
164 Text,
165 #[serde(rename = "json_schema")]
167 JsonSchema {
168 schema: serde_json::Value,
170 #[serde(rename = "retryCount", skip_serializing_if = "Option::is_none")]
172 retry_count: Option<u64>,
173 },
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182pub struct UserMessageSummary {
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub title: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub body: Option<String>,
189 pub diffs: Vec<FileDiff>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
195pub struct UserMessageModel {
196 #[serde(rename = "providerID")]
198 pub provider_id: String,
199 #[serde(rename = "modelID")]
201 pub model_id: String,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
210pub struct UserMessage {
211 pub id: String,
213 #[serde(rename = "sessionID")]
215 pub session_id: String,
216 pub time: UserMessageTime,
218 #[serde(default)]
220 pub agent: String,
221 #[serde(default)]
223 pub model: UserMessageModel,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub format: Option<OutputFormat>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub summary: Option<UserMessageSummary>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub system: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub tools: Option<HashMap<String, bool>>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub variant: Option<String>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
243pub struct UserMessageTime {
244 pub created: f64,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
250pub struct AssistantMessage {
251 #[serde(default)]
253 pub id: String,
254 #[serde(default)]
256 pub cost: f64,
257 #[serde(default)]
259 pub mode: String,
260 #[serde(rename = "modelID", default)]
262 pub model_id: String,
263 #[serde(default)]
265 pub path: AssistantMessagePath,
266 #[serde(rename = "providerID", default)]
268 pub provider_id: String,
269 #[serde(rename = "sessionID", default)]
271 pub session_id: String,
272 #[serde(rename = "parentID", default)]
274 pub parent_id: String,
275 #[serde(default)]
277 pub agent: String,
278 #[serde(default, skip_serializing_if = "Vec::is_empty")]
280 pub system: Vec<String>,
281 #[serde(default)]
283 pub time: AssistantMessageTime,
284 #[serde(default)]
286 pub tokens: AssistantMessageTokens,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub error: Option<SessionError>,
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub summary: Option<bool>,
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub variant: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub finish: Option<String>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub structured: Option<serde_json::Value>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
306pub struct AssistantMessagePath {
307 #[serde(default)]
309 pub cwd: String,
310 #[serde(default)]
312 pub root: String,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
317pub struct AssistantMessageTime {
318 #[serde(default)]
320 pub created: f64,
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub completed: Option<f64>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
328pub struct AssistantMessageTokens {
329 #[serde(default)]
331 pub cache: TokenCache,
332 #[serde(default)]
334 pub input: u64,
335 #[serde(default)]
337 pub output: u64,
338 #[serde(default)]
340 pub reasoning: u64,
341 #[serde(default)]
343 pub total: u64,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
348pub struct TokenCache {
349 #[serde(default)]
351 pub read: u64,
352 #[serde(default)]
354 pub write: u64,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
359#[serde(tag = "role")]
360pub enum Message {
361 #[serde(rename = "user")]
363 User(Box<UserMessage>),
364 #[serde(rename = "assistant")]
366 Assistant(Box<AssistantMessage>),
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
375pub struct TextPart {
376 pub id: String,
378 #[serde(rename = "messageID")]
380 pub message_id: String,
381 #[serde(rename = "sessionID")]
383 pub session_id: String,
384 pub text: String,
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub synthetic: Option<bool>,
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub time: Option<TextPartTime>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
396pub struct TextPartTime {
397 pub start: f64,
399 #[serde(skip_serializing_if = "Option::is_none")]
401 pub end: Option<f64>,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
406pub struct FilePart {
407 pub id: String,
409 #[serde(rename = "messageID")]
411 pub message_id: String,
412 pub mime: String,
414 #[serde(rename = "sessionID")]
416 pub session_id: String,
417 pub url: String,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub filename: Option<String>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub source: Option<FilePartSource>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
429pub struct ToolPart {
430 pub id: String,
432 #[serde(rename = "callID")]
434 pub call_id: String,
435 #[serde(rename = "messageID")]
437 pub message_id: String,
438 #[serde(rename = "sessionID")]
440 pub session_id: String,
441 pub state: ToolState,
443 pub tool: String,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
449pub struct StepStartPart {
450 pub id: String,
452 #[serde(rename = "messageID")]
454 pub message_id: String,
455 #[serde(rename = "sessionID")]
457 pub session_id: String,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
462pub struct StepFinishPart {
463 pub id: String,
465 pub cost: f64,
467 #[serde(rename = "messageID")]
469 pub message_id: String,
470 #[serde(rename = "sessionID")]
472 pub session_id: String,
473 pub tokens: StepFinishTokens,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
479pub struct StepFinishTokens {
480 pub cache: TokenCache,
482 pub input: u64,
484 pub output: u64,
486 pub reasoning: u64,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
492pub struct SnapshotPart {
493 pub id: String,
495 #[serde(rename = "messageID")]
497 pub message_id: String,
498 #[serde(rename = "sessionID")]
500 pub session_id: String,
501 pub snapshot: String,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
507pub struct PatchPart {
508 pub id: String,
510 pub files: Vec<String>,
512 pub hash: String,
514 #[serde(rename = "messageID")]
516 pub message_id: String,
517 #[serde(rename = "sessionID")]
519 pub session_id: String,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
524pub struct SubtaskPart {
525 pub id: String,
527 #[serde(rename = "sessionID")]
529 pub session_id: String,
530 #[serde(rename = "messageID")]
532 pub message_id: String,
533 pub prompt: String,
535 pub description: String,
537 pub agent: String,
539 #[serde(skip_serializing_if = "Option::is_none")]
541 pub model: Option<SubtaskPartModel>,
542 #[serde(skip_serializing_if = "Option::is_none")]
544 pub command: Option<String>,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
549pub struct SubtaskPartModel {
550 #[serde(rename = "providerID")]
552 pub provider_id: String,
553 #[serde(rename = "modelID")]
555 pub model_id: String,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
560pub struct ReasoningPart {
561 pub id: String,
563 #[serde(rename = "sessionID")]
565 pub session_id: String,
566 #[serde(rename = "messageID")]
568 pub message_id: String,
569 pub text: String,
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub metadata: Option<HashMap<String, serde_json::Value>>,
574 pub time: ReasoningPartTime,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
580pub struct ReasoningPartTime {
581 pub start: f64,
583 #[serde(skip_serializing_if = "Option::is_none")]
585 pub end: Option<f64>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
590pub struct AgentPart {
591 pub id: String,
593 #[serde(rename = "sessionID")]
595 pub session_id: String,
596 #[serde(rename = "messageID")]
598 pub message_id: String,
599 pub name: String,
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub source: Option<AgentPartSource>,
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
608pub struct AgentPartSource {
609 pub value: String,
611 pub start: i64,
613 pub end: i64,
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
619pub struct CompactionPart {
620 pub id: String,
622 #[serde(rename = "sessionID")]
624 pub session_id: String,
625 #[serde(rename = "messageID")]
627 pub message_id: String,
628 pub auto: bool,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
634pub struct RetryPart {
635 pub id: String,
637 #[serde(rename = "sessionID")]
639 pub session_id: String,
640 #[serde(rename = "messageID")]
642 pub message_id: String,
643 pub attempt: f64,
645 pub error: serde_json::Value,
647 pub time: RetryPartTime,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
653pub struct RetryPartTime {
654 pub created: f64,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
660#[serde(tag = "type")]
661pub enum Part {
662 #[serde(rename = "text")]
664 Text(TextPart),
665 #[serde(rename = "file")]
667 File(FilePart),
668 #[serde(rename = "tool")]
670 Tool(ToolPart),
671 #[serde(rename = "step-start")]
673 StepStart(StepStartPart),
674 #[serde(rename = "step-finish")]
676 StepFinish(StepFinishPart),
677 #[serde(rename = "snapshot")]
679 Snapshot(SnapshotPart),
680 #[serde(rename = "patch")]
682 Patch(PatchPart),
683 #[serde(rename = "subtask")]
685 Subtask(SubtaskPart),
686 #[serde(rename = "reasoning")]
688 Reasoning(ReasoningPart),
689 #[serde(rename = "agent")]
691 Agent(AgentPart),
692 #[serde(rename = "compaction")]
694 Compaction(CompactionPart),
695 #[serde(rename = "retry")]
697 Retry(RetryPart),
698 #[serde(other)]
700 Unknown,
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
709pub struct ToolStatePending {}
710
711#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
713pub struct ToolStateRunning {
714 pub time: ToolStateRunningTime,
716 #[serde(skip_serializing_if = "Option::is_none")]
718 pub input: Option<serde_json::Value>,
719 #[serde(skip_serializing_if = "Option::is_none")]
721 pub metadata: Option<HashMap<String, serde_json::Value>>,
722 #[serde(skip_serializing_if = "Option::is_none")]
724 pub title: Option<String>,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
729pub struct ToolStateRunningTime {
730 pub start: f64,
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
736pub struct ToolStateCompleted {
737 pub input: HashMap<String, serde_json::Value>,
739 pub metadata: HashMap<String, serde_json::Value>,
741 pub output: String,
743 pub time: ToolStateCompletedTime,
745 pub title: String,
747}
748
749#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
751pub struct ToolStateCompletedTime {
752 pub end: f64,
754 pub start: f64,
756}
757
758#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
760pub struct ToolStateError {
761 pub error: String,
763 pub input: HashMap<String, serde_json::Value>,
765 pub time: ToolStateErrorTime,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
771pub struct ToolStateErrorTime {
772 pub end: f64,
774 pub start: f64,
776}
777
778#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
780#[serde(tag = "status")]
781pub enum ToolState {
782 #[serde(rename = "pending")]
784 Pending(ToolStatePending),
785 #[serde(rename = "running")]
787 Running(ToolStateRunning),
788 #[serde(rename = "completed")]
790 Completed(ToolStateCompleted),
791 #[serde(rename = "error")]
793 Error(ToolStateError),
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
802pub struct FilePartSourceText {
803 pub end: u64,
805 pub start: u64,
807 pub value: String,
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
813pub struct FileSource {
814 pub path: String,
816 pub text: FilePartSourceText,
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
822pub struct SymbolSource {
823 pub kind: u64,
825 pub name: String,
827 pub path: String,
829 pub range: SymbolSourceRange,
831 pub text: FilePartSourceText,
833}
834
835#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
837pub struct SymbolSourceRange {
838 pub end: SymbolSourcePosition,
840 pub start: SymbolSourcePosition,
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
846pub struct SymbolSourcePosition {
847 pub character: u64,
849 pub line: u64,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
855#[serde(tag = "type")]
856pub enum FilePartSource {
857 #[serde(rename = "file")]
859 File(FileSource),
860 #[serde(rename = "symbol")]
862 Symbol(SymbolSource),
863}
864
865#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
871pub struct TextPartInput {
872 pub text: String,
874 #[serde(skip_serializing_if = "Option::is_none")]
876 pub id: Option<String>,
877 #[serde(skip_serializing_if = "Option::is_none")]
879 pub synthetic: Option<bool>,
880 #[serde(skip_serializing_if = "Option::is_none")]
882 pub ignored: Option<bool>,
883 #[serde(skip_serializing_if = "Option::is_none")]
885 pub time: Option<TextPartInputTime>,
886 #[serde(skip_serializing_if = "Option::is_none")]
888 pub metadata: Option<HashMap<String, serde_json::Value>>,
889}
890
891#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
893pub struct TextPartInputTime {
894 pub start: f64,
896 #[serde(skip_serializing_if = "Option::is_none")]
898 pub end: Option<f64>,
899}
900
901#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
903pub struct FilePartInput {
904 pub mime: String,
906 pub url: String,
908 #[serde(skip_serializing_if = "Option::is_none")]
910 pub id: Option<String>,
911 #[serde(skip_serializing_if = "Option::is_none")]
913 pub filename: Option<String>,
914 #[serde(skip_serializing_if = "Option::is_none")]
916 pub source: Option<FilePartSource>,
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
921pub struct AgentPartInput {
922 pub name: String,
924 #[serde(skip_serializing_if = "Option::is_none")]
926 pub id: Option<String>,
927 #[serde(skip_serializing_if = "Option::is_none")]
929 pub source: Option<AgentPartSource>,
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
934pub struct SubtaskPartInput {
935 pub prompt: String,
937 pub description: String,
939 pub agent: String,
941 #[serde(skip_serializing_if = "Option::is_none")]
943 pub id: Option<String>,
944 #[serde(skip_serializing_if = "Option::is_none")]
946 pub model: Option<SessionChatModel>,
947 #[serde(skip_serializing_if = "Option::is_none")]
949 pub command: Option<String>,
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
954#[serde(tag = "type")]
955pub enum PartInput {
956 #[serde(rename = "text")]
958 Text(TextPartInput),
959 #[serde(rename = "file")]
961 File(FilePartInput),
962 #[serde(rename = "agent")]
964 Agent(AgentPartInput),
965 #[serde(rename = "subtask")]
967 Subtask(SubtaskPartInput),
968}
969
970#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
976pub struct SessionMessagesResponseItem {
977 pub info: Message,
979 pub parts: Vec<Part>,
981}
982
983pub type SessionMessagesResponse = Vec<SessionMessagesResponseItem>;
985
986pub type SessionListResponse = Vec<Session>;
988
989pub type SessionDeleteResponse = bool;
991
992pub type SessionAbortResponse = bool;
994
995pub type SessionInitResponse = bool;
997
998pub type SessionSummarizeResponse = bool;
1000
1001#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1007pub struct SessionChatModel {
1008 #[serde(rename = "providerID")]
1010 pub provider_id: String,
1011 #[serde(rename = "modelID")]
1013 pub model_id: String,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1018pub struct SessionChatParams {
1019 pub parts: Vec<PartInput>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub model: Option<SessionChatModel>,
1024 #[serde(rename = "messageID")]
1026 #[serde(skip_serializing_if = "Option::is_none")]
1027 pub message_id: Option<String>,
1028 #[serde(skip_serializing_if = "Option::is_none")]
1030 pub agent: Option<String>,
1031 #[serde(rename = "noReply")]
1033 #[serde(skip_serializing_if = "Option::is_none")]
1034 pub no_reply: Option<bool>,
1035 #[serde(skip_serializing_if = "Option::is_none")]
1037 pub format: Option<OutputFormat>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub system: Option<String>,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub variant: Option<String>,
1044 #[serde(skip_serializing_if = "Option::is_none")]
1046 pub tools: Option<HashMap<String, bool>>,
1047}
1048
1049#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1051pub struct SessionInitParams {
1052 #[serde(rename = "messageID")]
1054 pub message_id: String,
1055 #[serde(rename = "modelID")]
1057 pub model_id: String,
1058 #[serde(rename = "providerID")]
1060 pub provider_id: String,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1065pub struct SessionRevertParams {
1066 #[serde(rename = "messageID")]
1068 pub message_id: String,
1069 #[serde(rename = "partID")]
1071 #[serde(skip_serializing_if = "Option::is_none")]
1072 pub part_id: Option<String>,
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1077pub struct SessionSummarizeParams {
1078 #[serde(rename = "modelID")]
1080 pub model_id: String,
1081 #[serde(rename = "providerID")]
1083 pub provider_id: String,
1084}
1085
1086pub struct SessionResource<'a> {
1092 client: &'a Opencode,
1093}
1094
1095impl<'a> SessionResource<'a> {
1096 pub(crate) const fn new(client: &'a Opencode) -> Self {
1098 Self { client }
1099 }
1100
1101 pub async fn create(&self, options: Option<&RequestOptions>) -> Result<Session, OpencodeError> {
1103 self.client.post::<Session, ()>("/session", None, options).await
1104 }
1105
1106 pub async fn list(
1108 &self,
1109 options: Option<&RequestOptions>,
1110 ) -> Result<SessionListResponse, OpencodeError> {
1111 self.client.get("/session", options).await
1112 }
1113
1114 pub async fn delete(
1116 &self,
1117 id: &str,
1118 options: Option<&RequestOptions>,
1119 ) -> Result<SessionDeleteResponse, OpencodeError> {
1120 self.client.delete::<bool, ()>(&format!("/session/{id}"), None, options).await
1121 }
1122
1123 pub async fn abort(
1125 &self,
1126 id: &str,
1127 options: Option<&RequestOptions>,
1128 ) -> Result<SessionAbortResponse, OpencodeError> {
1129 self.client.post::<bool, ()>(&format!("/session/{id}/abort"), None, options).await
1130 }
1131
1132 pub async fn chat(
1134 &self,
1135 id: &str,
1136 params: &SessionChatParams,
1137 options: Option<&RequestOptions>,
1138 ) -> Result<SessionMessagesResponseItem, OpencodeError> {
1139 self.client.post(&format!("/session/{id}/message"), Some(params), options).await
1140 }
1141
1142 pub async fn init(
1144 &self,
1145 id: &str,
1146 params: &SessionInitParams,
1147 options: Option<&RequestOptions>,
1148 ) -> Result<SessionInitResponse, OpencodeError> {
1149 self.client.post(&format!("/session/{id}/init"), Some(params), options).await
1150 }
1151
1152 pub async fn messages(
1154 &self,
1155 id: &str,
1156 options: Option<&RequestOptions>,
1157 ) -> Result<SessionMessagesResponse, OpencodeError> {
1158 self.client.get(&format!("/session/{id}/message"), options).await
1159 }
1160
1161 pub async fn revert(
1163 &self,
1164 id: &str,
1165 params: &SessionRevertParams,
1166 options: Option<&RequestOptions>,
1167 ) -> Result<Session, OpencodeError> {
1168 self.client.post(&format!("/session/{id}/revert"), Some(params), options).await
1169 }
1170
1171 pub async fn share(
1173 &self,
1174 id: &str,
1175 options: Option<&RequestOptions>,
1176 ) -> Result<Session, OpencodeError> {
1177 self.client.post::<Session, ()>(&format!("/session/{id}/share"), None, options).await
1178 }
1179
1180 pub async fn summarize(
1182 &self,
1183 id: &str,
1184 params: &SessionSummarizeParams,
1185 options: Option<&RequestOptions>,
1186 ) -> Result<SessionSummarizeResponse, OpencodeError> {
1187 self.client.post(&format!("/session/{id}/summarize"), Some(params), options).await
1188 }
1189
1190 pub async fn unrevert(
1192 &self,
1193 id: &str,
1194 options: Option<&RequestOptions>,
1195 ) -> Result<Session, OpencodeError> {
1196 self.client.post::<Session, ()>(&format!("/session/{id}/unrevert"), None, options).await
1197 }
1198
1199 pub async fn unshare(
1201 &self,
1202 id: &str,
1203 options: Option<&RequestOptions>,
1204 ) -> Result<Session, OpencodeError> {
1205 self.client.delete::<Session, ()>(&format!("/session/{id}/share"), None, options).await
1206 }
1207}
1208
1209#[cfg(test)]
1214mod tests {
1215 use serde_json::json;
1216
1217 use super::*;
1218
1219 #[test]
1222 fn session_full_round_trip() {
1223 let session = Session {
1224 id: "sess_001".into(),
1225 slug: "my-session".into(),
1226 project_id: "proj_001".into(),
1227 directory: "/home/user/project".into(),
1228 time: SessionTime {
1229 created: 1_700_000_000.0,
1230 updated: 1_700_001_000.0,
1231 compacting: None,
1232 archived: None,
1233 },
1234 title: "My Session".into(),
1235 version: "1".into(),
1236 parent_id: Some("sess_000".into()),
1237 revert: Some(SessionRevert {
1238 message_id: "msg_001".into(),
1239 diff: Some("--- a/file\n+++ b/file".into()),
1240 part_id: Some("part_001".into()),
1241 snapshot: Some("snapshot_data".into()),
1242 }),
1243 share: Some(SessionShare { url: "https://example.com/share/abc".into() }),
1244 summary: None,
1245 permission: None,
1246 };
1247 let json_str = serde_json::to_string(&session).unwrap();
1248 assert!(json_str.contains("parentID"));
1249 assert!(json_str.contains("messageID"));
1250 assert!(json_str.contains("partID"));
1251 let back: Session = serde_json::from_str(&json_str).unwrap();
1252 assert_eq!(session, back);
1253 }
1254
1255 #[test]
1256 fn session_minimal_round_trip() {
1257 let session = Session {
1258 id: "sess_002".into(),
1259 slug: "empty".into(),
1260 project_id: "proj_002".into(),
1261 directory: "/tmp".into(),
1262 time: SessionTime {
1263 created: 1_700_000_000.0,
1264 updated: 1_700_000_000.0,
1265 compacting: None,
1266 archived: None,
1267 },
1268 title: "Empty".into(),
1269 version: "1".into(),
1270 parent_id: None,
1271 revert: None,
1272 share: None,
1273 summary: None,
1274 permission: None,
1275 };
1276 let json_str = serde_json::to_string(&session).unwrap();
1277 assert!(!json_str.contains("parentID"));
1278 assert!(!json_str.contains("revert"));
1279 assert!(!json_str.contains("share"));
1280 let back: Session = serde_json::from_str(&json_str).unwrap();
1281 assert_eq!(session, back);
1282 }
1283
1284 #[test]
1287 fn user_message_round_trip() {
1288 let msg = UserMessage {
1289 id: "msg_u001".into(),
1290 session_id: "sess_001".into(),
1291 time: UserMessageTime { created: 1_700_000_100.0 },
1292 agent: "coder".into(),
1293 model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
1294 format: None,
1295 summary: None,
1296 system: None,
1297 tools: None,
1298 variant: None,
1299 };
1300 let json_str = serde_json::to_string(&msg).unwrap();
1301 assert!(json_str.contains("sessionID"));
1302 assert!(json_str.contains("agent"));
1303 assert!(json_str.contains("providerID"));
1304 assert!(json_str.contains("modelID"));
1305 let back: UserMessage = serde_json::from_str(&json_str).unwrap();
1306 assert_eq!(msg, back);
1307 }
1308
1309 #[test]
1310 fn assistant_message_round_trip() {
1311 let msg = AssistantMessage {
1312 id: "msg_a001".into(),
1313 cost: 0.0032,
1314 mode: "code".into(),
1315 model_id: "gpt-4o".into(),
1316 path: AssistantMessagePath {
1317 cwd: "/home/user/project".into(),
1318 root: "/home/user/project".into(),
1319 },
1320 provider_id: "openai".into(),
1321 session_id: "sess_001".into(),
1322 parent_id: "msg_parent".into(),
1323 agent: "default".into(),
1324 system: vec!["You are a helpful assistant.".into()],
1325 time: AssistantMessageTime {
1326 created: 1_700_000_200.0,
1327 completed: Some(1_700_000_210.0),
1328 },
1329 tokens: AssistantMessageTokens {
1330 cache: TokenCache { read: 100, write: 50 },
1331 input: 500,
1332 output: 200,
1333 reasoning: 0,
1334 total: 700,
1335 },
1336 error: None,
1337 summary: None,
1338 variant: None,
1339 finish: None,
1340 structured: None,
1341 };
1342 let json_str = serde_json::to_string(&msg).unwrap();
1343 assert!(json_str.contains("modelID"));
1344 assert!(json_str.contains("providerID"));
1345 assert!(json_str.contains("sessionID"));
1346 assert!(json_str.contains("parentID"));
1347 assert!(json_str.contains("agent"));
1348 let back: AssistantMessage = serde_json::from_str(&json_str).unwrap();
1349 assert_eq!(msg, back);
1350 }
1351
1352 #[test]
1353 fn assistant_message_with_error() {
1354 let msg = AssistantMessage {
1355 id: "msg_a002".into(),
1356 cost: 0.0,
1357 mode: "code".into(),
1358 model_id: "gpt-4o".into(),
1359 path: AssistantMessagePath { cwd: "/tmp".into(), root: "/tmp".into() },
1360 provider_id: "openai".into(),
1361 session_id: "sess_001".into(),
1362 parent_id: "msg_parent".into(),
1363 agent: "coder".into(),
1364 system: vec![],
1365 time: AssistantMessageTime { created: 1_700_000_300.0, completed: None },
1366 tokens: AssistantMessageTokens {
1367 cache: TokenCache { read: 0, write: 0 },
1368 input: 0,
1369 output: 0,
1370 reasoning: 0,
1371 total: 0,
1372 },
1373 error: Some(SessionError::ProviderAuthError {
1374 data: super::super::shared::ProviderAuthErrorData {
1375 message: "invalid key".into(),
1376 provider_id: "openai".into(),
1377 },
1378 }),
1379 summary: Some(true),
1380 variant: None,
1381 finish: None,
1382 structured: None,
1383 };
1384 let json_str = serde_json::to_string(&msg).unwrap();
1385 assert!(json_str.contains("ProviderAuthError"));
1386 let back: AssistantMessage = serde_json::from_str(&json_str).unwrap();
1387 assert_eq!(msg, back);
1388 }
1389
1390 #[test]
1393 fn message_enum_user_variant() {
1394 let msg = Message::User(Box::new(UserMessage {
1395 id: "msg_u002".into(),
1396 session_id: "sess_001".into(),
1397 time: UserMessageTime { created: 1_700_000_100.0 },
1398 agent: "coder".into(),
1399 model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
1400 format: None,
1401 summary: None,
1402 system: None,
1403 tools: None,
1404 variant: None,
1405 }));
1406 let json_str = serde_json::to_string(&msg).unwrap();
1407 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1408 assert_eq!(v["role"], "user");
1409 let back: Message = serde_json::from_str(&json_str).unwrap();
1410 assert_eq!(msg, back);
1411 }
1412
1413 #[test]
1414 fn message_enum_assistant_variant() {
1415 let msg = Message::Assistant(Box::new(AssistantMessage {
1416 id: "msg_a003".into(),
1417 cost: 0.001,
1418 mode: "default".into(),
1419 model_id: "claude-3-opus".into(),
1420 path: AssistantMessagePath { cwd: "/home".into(), root: "/home".into() },
1421 provider_id: "anthropic".into(),
1422 session_id: "sess_002".into(),
1423 parent_id: "msg_a002".into(),
1424 agent: "reviewer".into(),
1425 system: vec![],
1426 time: AssistantMessageTime {
1427 created: 1_700_000_500.0,
1428 completed: Some(1_700_000_510.0),
1429 },
1430 tokens: AssistantMessageTokens {
1431 cache: TokenCache { read: 10, write: 5 },
1432 input: 100,
1433 output: 50,
1434 reasoning: 20,
1435 total: 170,
1436 },
1437 error: None,
1438 summary: None,
1439 variant: None,
1440 finish: None,
1441 structured: None,
1442 }));
1443 let json_str = serde_json::to_string(&msg).unwrap();
1444 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1445 assert_eq!(v["role"], "assistant");
1446 let back: Message = serde_json::from_str(&json_str).unwrap();
1447 assert_eq!(msg, back);
1448 }
1449
1450 #[test]
1453 fn part_text_round_trip() {
1454 let part = Part::Text(TextPart {
1455 id: "p_001".into(),
1456 message_id: "msg_a001".into(),
1457 session_id: "sess_001".into(),
1458 text: "Hello, world!".into(),
1459 synthetic: None,
1460 time: Some(TextPartTime { start: 1_700_000_200.0, end: Some(1_700_000_201.0) }),
1461 });
1462 let json_str = serde_json::to_string(&part).unwrap();
1463 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1464 assert_eq!(v["type"], "text");
1465 let back: Part = serde_json::from_str(&json_str).unwrap();
1466 assert_eq!(part, back);
1467 }
1468
1469 #[test]
1470 fn part_tool_round_trip() {
1471 let part = Part::Tool(ToolPart {
1472 id: "p_002".into(),
1473 call_id: "call_001".into(),
1474 message_id: "msg_a001".into(),
1475 session_id: "sess_001".into(),
1476 state: ToolState::Completed(ToolStateCompleted {
1477 input: HashMap::from([("cmd".into(), json!("ls"))]),
1478 metadata: HashMap::new(),
1479 output: "file1.rs\nfile2.rs".into(),
1480 time: ToolStateCompletedTime { end: 1_700_000_205.0, start: 1_700_000_202.0 },
1481 title: "bash".into(),
1482 }),
1483 tool: "bash".into(),
1484 });
1485 let json_str = serde_json::to_string(&part).unwrap();
1486 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1487 assert_eq!(v["type"], "tool");
1488 assert_eq!(v["state"]["status"], "completed");
1489 let back: Part = serde_json::from_str(&json_str).unwrap();
1490 assert_eq!(part, back);
1491 }
1492
1493 #[test]
1494 fn part_step_start_round_trip() {
1495 let part = Part::StepStart(StepStartPart {
1496 id: "p_003".into(),
1497 message_id: "msg_a001".into(),
1498 session_id: "sess_001".into(),
1499 });
1500 let json_str = serde_json::to_string(&part).unwrap();
1501 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1502 assert_eq!(v["type"], "step-start");
1503 let back: Part = serde_json::from_str(&json_str).unwrap();
1504 assert_eq!(part, back);
1505 }
1506
1507 #[test]
1508 fn part_step_finish_round_trip() {
1509 let part = Part::StepFinish(StepFinishPart {
1510 id: "p_004".into(),
1511 cost: 0.001,
1512 message_id: "msg_a001".into(),
1513 session_id: "sess_001".into(),
1514 tokens: StepFinishTokens {
1515 cache: TokenCache { read: 10, write: 5 },
1516 input: 100,
1517 output: 50,
1518 reasoning: 0,
1519 },
1520 });
1521 let json_str = serde_json::to_string(&part).unwrap();
1522 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1523 assert_eq!(v["type"], "step-finish");
1524 let back: Part = serde_json::from_str(&json_str).unwrap();
1525 assert_eq!(part, back);
1526 }
1527
1528 #[test]
1529 fn part_patch_round_trip() {
1530 let part = Part::Patch(PatchPart {
1531 id: "p_005".into(),
1532 files: vec!["src/main.rs".into(), "Cargo.toml".into()],
1533 hash: "abc123".into(),
1534 message_id: "msg_a001".into(),
1535 session_id: "sess_001".into(),
1536 });
1537 let json_str = serde_json::to_string(&part).unwrap();
1538 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1539 assert_eq!(v["type"], "patch");
1540 let back: Part = serde_json::from_str(&json_str).unwrap();
1541 assert_eq!(part, back);
1542 }
1543
1544 #[test]
1545 fn part_snapshot_round_trip() {
1546 let part = Part::Snapshot(SnapshotPart {
1547 id: "p_006".into(),
1548 message_id: "msg_a001".into(),
1549 session_id: "sess_001".into(),
1550 snapshot: "{\"state\":\"data\"}".into(),
1551 });
1552 let json_str = serde_json::to_string(&part).unwrap();
1553 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1554 assert_eq!(v["type"], "snapshot");
1555 let back: Part = serde_json::from_str(&json_str).unwrap();
1556 assert_eq!(part, back);
1557 }
1558
1559 #[test]
1560 fn part_file_round_trip() {
1561 let part = Part::File(FilePart {
1562 id: "p_007".into(),
1563 message_id: "msg_a001".into(),
1564 mime: "image/png".into(),
1565 session_id: "sess_001".into(),
1566 url: "https://example.com/img.png".into(),
1567 filename: Some("screenshot.png".into()),
1568 source: None,
1569 });
1570 let json_str = serde_json::to_string(&part).unwrap();
1571 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1572 assert_eq!(v["type"], "file");
1573 let back: Part = serde_json::from_str(&json_str).unwrap();
1574 assert_eq!(part, back);
1575 }
1576
1577 #[test]
1580 fn tool_state_pending() {
1581 let state = ToolState::Pending(ToolStatePending {});
1582 let json_str = serde_json::to_string(&state).unwrap();
1583 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1584 assert_eq!(v["status"], "pending");
1585 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1586 assert_eq!(state, back);
1587 }
1588
1589 #[test]
1590 fn tool_state_running() {
1591 let state = ToolState::Running(ToolStateRunning {
1592 time: ToolStateRunningTime { start: 1_700_000_200.0 },
1593 input: Some(json!({"command": "echo hello"})),
1594 metadata: Some(HashMap::from([("key".into(), json!("value"))])),
1595 title: Some("Running bash".into()),
1596 });
1597 let json_str = serde_json::to_string(&state).unwrap();
1598 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1599 assert_eq!(v["status"], "running");
1600 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1601 assert_eq!(state, back);
1602 }
1603
1604 #[test]
1605 fn tool_state_completed() {
1606 let state = ToolState::Completed(ToolStateCompleted {
1607 input: HashMap::from([("cmd".into(), json!("ls -la"))]),
1608 metadata: HashMap::from([("exit_code".into(), json!(0))]),
1609 output: "total 42\ndrwxr-xr-x ...".into(),
1610 time: ToolStateCompletedTime { end: 1_700_000_210.0, start: 1_700_000_200.0 },
1611 title: "bash".into(),
1612 });
1613 let json_str = serde_json::to_string(&state).unwrap();
1614 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1615 assert_eq!(v["status"], "completed");
1616 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1617 assert_eq!(state, back);
1618 }
1619
1620 #[test]
1621 fn tool_state_error() {
1622 let state = ToolState::Error(ToolStateError {
1623 error: "command not found".into(),
1624 input: HashMap::from([("cmd".into(), json!("nonexistent"))]),
1625 time: ToolStateErrorTime { end: 1_700_000_201.0, start: 1_700_000_200.0 },
1626 });
1627 let json_str = serde_json::to_string(&state).unwrap();
1628 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1629 assert_eq!(v["status"], "error");
1630 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1631 assert_eq!(state, back);
1632 }
1633
1634 #[test]
1637 fn file_part_source_file_variant() {
1638 let src = FilePartSource::File(FileSource {
1639 path: "/home/user/main.rs".into(),
1640 text: FilePartSourceText { end: 100, start: 0, value: "fn main() {}".into() },
1641 });
1642 let json_str = serde_json::to_string(&src).unwrap();
1643 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1644 assert_eq!(v["type"], "file");
1645 let back: FilePartSource = serde_json::from_str(&json_str).unwrap();
1646 assert_eq!(src, back);
1647 }
1648
1649 #[test]
1650 fn file_part_source_symbol_variant() {
1651 let src = FilePartSource::Symbol(SymbolSource {
1652 kind: 12,
1653 name: "main".into(),
1654 path: "/home/user/main.rs".into(),
1655 range: SymbolSourceRange {
1656 end: SymbolSourcePosition { character: 1, line: 2 },
1657 start: SymbolSourcePosition { character: 0, line: 0 },
1658 },
1659 text: FilePartSourceText {
1660 end: 50,
1661 start: 0,
1662 value: "fn main() {\n println!(\"hello\");\n}".into(),
1663 },
1664 });
1665 let json_str = serde_json::to_string(&src).unwrap();
1666 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1667 assert_eq!(v["type"], "symbol");
1668 let back: FilePartSource = serde_json::from_str(&json_str).unwrap();
1669 assert_eq!(src, back);
1670 }
1671
1672 #[test]
1675 fn session_chat_params_full_round_trip() {
1676 let params = SessionChatParams {
1677 parts: vec![
1678 PartInput::Text(TextPartInput {
1679 text: "Hello!".into(),
1680 id: Some("input_001".into()),
1681 synthetic: None,
1682 ignored: None,
1683 time: Some(TextPartInputTime { start: 1_700_000_000.0, end: None }),
1684 metadata: None,
1685 }),
1686 PartInput::File(FilePartInput {
1687 mime: "text/plain".into(),
1688 url: "file:///tmp/test.txt".into(),
1689 id: None,
1690 filename: Some("test.txt".into()),
1691 source: None,
1692 }),
1693 ],
1694 model: Some(SessionChatModel {
1695 provider_id: "openai".into(),
1696 model_id: "gpt-4o".into(),
1697 }),
1698 message_id: Some("msg_001".into()),
1699 agent: None,
1700 no_reply: None,
1701 format: None,
1702 system: Some("Be concise.".into()),
1703 variant: None,
1704 tools: Some(HashMap::from([("bash".into(), true)])),
1705 };
1706 let json_str = serde_json::to_string(¶ms).unwrap();
1707 assert!(json_str.contains("modelID"));
1708 assert!(json_str.contains("providerID"));
1709 assert!(json_str.contains("messageID"));
1710 let back: SessionChatParams = serde_json::from_str(&json_str).unwrap();
1711 assert_eq!(params, back);
1712 }
1713
1714 #[test]
1715 fn session_chat_params_minimal() {
1716 let params = SessionChatParams {
1717 parts: vec![PartInput::Text(TextPartInput {
1718 text: "Hi".into(),
1719 id: None,
1720 synthetic: None,
1721 ignored: None,
1722 time: None,
1723 metadata: None,
1724 })],
1725 model: None,
1726 message_id: None,
1727 agent: None,
1728 no_reply: None,
1729 format: None,
1730 system: None,
1731 variant: None,
1732 tools: None,
1733 };
1734 let json_str = serde_json::to_string(¶ms).unwrap();
1735 assert!(!json_str.contains("messageID"));
1736 assert!(!json_str.contains("model"));
1737 assert!(!json_str.contains("system"));
1738 assert!(!json_str.contains("tools"));
1739 let back: SessionChatParams = serde_json::from_str(&json_str).unwrap();
1740 assert_eq!(params, back);
1741 }
1742
1743 #[test]
1746 fn part_input_text_round_trip() {
1747 let input = PartInput::Text(TextPartInput {
1748 text: "Hello".into(),
1749 id: None,
1750 synthetic: Some(true),
1751 ignored: None,
1752 time: None,
1753 metadata: None,
1754 });
1755 let json_str = serde_json::to_string(&input).unwrap();
1756 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1757 assert_eq!(v["type"], "text");
1758 let back: PartInput = serde_json::from_str(&json_str).unwrap();
1759 assert_eq!(input, back);
1760 }
1761
1762 #[test]
1763 fn part_input_file_round_trip() {
1764 let input = PartInput::File(FilePartInput {
1765 mime: "image/png".into(),
1766 url: "https://example.com/img.png".into(),
1767 id: Some("fi_001".into()),
1768 filename: Some("photo.png".into()),
1769 source: Some(FilePartSource::File(FileSource {
1770 path: "/tmp/photo.png".into(),
1771 text: FilePartSourceText { end: 0, start: 0, value: String::new() },
1772 })),
1773 });
1774 let json_str = serde_json::to_string(&input).unwrap();
1775 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1776 assert_eq!(v["type"], "file");
1777 let back: PartInput = serde_json::from_str(&json_str).unwrap();
1778 assert_eq!(input, back);
1779 }
1780
1781 #[test]
1784 fn session_messages_response_item_round_trip() {
1785 let item = SessionMessagesResponseItem {
1786 info: Message::User(Box::new(UserMessage {
1787 id: "msg_u010".into(),
1788 session_id: "sess_001".into(),
1789 time: UserMessageTime { created: 1_700_000_000.0 },
1790 agent: "coder".into(),
1791 model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
1792 format: None,
1793 summary: None,
1794 system: None,
1795 tools: None,
1796 variant: None,
1797 })),
1798 parts: vec![Part::Text(TextPart {
1799 id: "p_010".into(),
1800 message_id: "msg_u010".into(),
1801 session_id: "sess_001".into(),
1802 text: "What is Rust?".into(),
1803 synthetic: None,
1804 time: None,
1805 })],
1806 };
1807 let json_str = serde_json::to_string(&item).unwrap();
1808 let back: SessionMessagesResponseItem = serde_json::from_str(&json_str).unwrap();
1809 assert_eq!(item, back);
1810 }
1811
1812 #[test]
1815 fn session_init_params_round_trip() {
1816 let params = SessionInitParams {
1817 message_id: "msg_001".into(),
1818 model_id: "gpt-4o".into(),
1819 provider_id: "openai".into(),
1820 };
1821 let json_str = serde_json::to_string(¶ms).unwrap();
1822 assert!(json_str.contains("messageID"));
1823 assert!(json_str.contains("modelID"));
1824 assert!(json_str.contains("providerID"));
1825 let back: SessionInitParams = serde_json::from_str(&json_str).unwrap();
1826 assert_eq!(params, back);
1827 }
1828
1829 #[test]
1830 fn session_revert_params_round_trip() {
1831 let params =
1832 SessionRevertParams { message_id: "msg_001".into(), part_id: Some("part_001".into()) };
1833 let json_str = serde_json::to_string(¶ms).unwrap();
1834 assert!(json_str.contains("messageID"));
1835 assert!(json_str.contains("partID"));
1836 let back: SessionRevertParams = serde_json::from_str(&json_str).unwrap();
1837 assert_eq!(params, back);
1838 }
1839
1840 #[test]
1841 fn session_summarize_params_round_trip() {
1842 let params =
1843 SessionSummarizeParams { model_id: "gpt-4o".into(), provider_id: "openai".into() };
1844 let json_str = serde_json::to_string(¶ms).unwrap();
1845 assert!(json_str.contains("modelID"));
1846 assert!(json_str.contains("providerID"));
1847 let back: SessionSummarizeParams = serde_json::from_str(&json_str).unwrap();
1848 assert_eq!(params, back);
1849 }
1850
1851 #[test]
1854 fn deserialize_message_from_js_json() {
1855 let js_json = json!({
1856 "role": "user",
1857 "id": "msg_from_js",
1858 "sessionID": "sess_js",
1859 "time": { "created": 1700000000.0 }
1860 });
1861 let msg: Message = serde_json::from_value(js_json).unwrap();
1862 match msg {
1863 Message::User(u) => {
1864 assert_eq!(u.id, "msg_from_js");
1865 assert_eq!(u.session_id, "sess_js");
1866 }
1867 _ => panic!("expected User variant"),
1868 }
1869 }
1870
1871 #[test]
1872 fn deserialize_part_from_js_json() {
1873 let js_json = json!({
1874 "type": "step-start",
1875 "id": "p_js_001",
1876 "messageID": "msg_js_001",
1877 "sessionID": "sess_js"
1878 });
1879 let part: Part = serde_json::from_value(js_json).unwrap();
1880 match part {
1881 Part::StepStart(s) => {
1882 assert_eq!(s.id, "p_js_001");
1883 assert_eq!(s.message_id, "msg_js_001");
1884 }
1885 _ => panic!("expected StepStart variant"),
1886 }
1887 }
1888
1889 #[test]
1890 fn deserialize_tool_state_from_js_json() {
1891 let js_json = json!({
1892 "status": "error",
1893 "error": "timeout",
1894 "input": { "cmd": "sleep 999" },
1895 "time": { "start": 1700000000.0, "end": 1700000030.0 }
1896 });
1897 let state: ToolState = serde_json::from_value(js_json).unwrap();
1898 match state {
1899 ToolState::Error(e) => {
1900 assert_eq!(e.error, "timeout");
1901 }
1902 _ => panic!("expected Error variant"),
1903 }
1904 }
1905
1906 #[test]
1909 fn tool_state_running_minimal() {
1910 let state = ToolState::Running(ToolStateRunning {
1911 time: ToolStateRunningTime { start: 1_700_000_000.0 },
1912 input: None,
1913 metadata: None,
1914 title: None,
1915 });
1916 let json_str = serde_json::to_string(&state).unwrap();
1917 assert!(!json_str.contains("input"));
1918 assert!(!json_str.contains("metadata"));
1919 assert!(!json_str.contains("title"));
1920 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1921 assert_eq!(v["status"], "running");
1922 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1923 assert_eq!(state, back);
1924 }
1925
1926 #[test]
1927 fn text_part_no_synthetic_no_time() {
1928 let part = TextPart {
1929 id: "tp_001".into(),
1930 message_id: "msg_001".into(),
1931 session_id: "sess_001".into(),
1932 text: "bare text".into(),
1933 synthetic: None,
1934 time: None,
1935 };
1936 let json_str = serde_json::to_string(&part).unwrap();
1937 assert!(!json_str.contains("synthetic"));
1938 assert!(!json_str.contains("time"));
1939 let back: TextPart = serde_json::from_str(&json_str).unwrap();
1940 assert_eq!(part, back);
1941 }
1942
1943 #[test]
1944 fn file_part_no_filename_no_source() {
1945 let part = FilePart {
1946 id: "fp_001".into(),
1947 message_id: "msg_001".into(),
1948 mime: "application/octet-stream".into(),
1949 session_id: "sess_001".into(),
1950 url: "https://example.com/data.bin".into(),
1951 filename: None,
1952 source: None,
1953 };
1954 let json_str = serde_json::to_string(&part).unwrap();
1955 assert!(!json_str.contains("filename"));
1956 assert!(!json_str.contains("source"));
1957 let back: FilePart = serde_json::from_str(&json_str).unwrap();
1958 assert_eq!(part, back);
1959 }
1960
1961 #[test]
1962 fn part_file_minimal_round_trip() {
1963 let part = Part::File(FilePart {
1964 id: "fp_002".into(),
1965 message_id: "msg_001".into(),
1966 mime: "text/plain".into(),
1967 session_id: "sess_001".into(),
1968 url: "file:///tmp/a.txt".into(),
1969 filename: None,
1970 source: None,
1971 });
1972 let json_str = serde_json::to_string(&part).unwrap();
1973 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1974 assert_eq!(v["type"], "file");
1975 assert!(v.get("filename").is_none());
1976 assert!(v.get("source").is_none());
1977 let back: Part = serde_json::from_str(&json_str).unwrap();
1978 assert_eq!(part, back);
1979 }
1980
1981 #[test]
1982 fn assistant_message_no_error_no_summary() {
1983 let msg = AssistantMessage {
1984 id: "msg_edge".into(),
1985 cost: 0.0,
1986 mode: "plan".into(),
1987 model_id: "o1".into(),
1988 path: AssistantMessagePath { cwd: "/app".into(), root: "/app".into() },
1989 provider_id: "openai".into(),
1990 session_id: "sess_edge".into(),
1991 parent_id: "msg_prev".into(),
1992 agent: "planner".into(),
1993 system: vec![],
1994 time: AssistantMessageTime { created: 1_700_000_000.0, completed: None },
1995 tokens: AssistantMessageTokens {
1996 cache: TokenCache { read: 0, write: 0 },
1997 input: 10,
1998 output: 5,
1999 reasoning: 0,
2000 total: 15,
2001 },
2002 error: None,
2003 summary: None,
2004 variant: None,
2005 finish: None,
2006 structured: None,
2007 };
2008 let json_str = serde_json::to_string(&msg).unwrap();
2009 assert!(!json_str.contains("error"));
2010 assert!(!json_str.contains("summary"));
2011 assert!(!json_str.contains("variant"));
2012 assert!(!json_str.contains("finish"));
2013 assert!(!json_str.contains("structured"));
2014 assert!(!json_str.contains("system"));
2015 let back: AssistantMessage = serde_json::from_str(&json_str).unwrap();
2016 assert_eq!(msg, back);
2017 }
2018
2019 #[test]
2020 fn part_input_text_minimal() {
2021 let input = PartInput::Text(TextPartInput {
2022 text: "hi".into(),
2023 id: None,
2024 synthetic: None,
2025 ignored: None,
2026 time: None,
2027 metadata: None,
2028 });
2029 let json_str = serde_json::to_string(&input).unwrap();
2030 assert!(!json_str.contains("\"id\""));
2031 assert!(!json_str.contains("synthetic"));
2032 assert!(!json_str.contains("ignored"));
2033 assert!(!json_str.contains("time"));
2034 assert!(!json_str.contains("metadata"));
2035 let back: PartInput = serde_json::from_str(&json_str).unwrap();
2036 assert_eq!(input, back);
2037 }
2038
2039 #[test]
2040 fn part_input_file_minimal() {
2041 let input = PartInput::File(FilePartInput {
2042 mime: "text/csv".into(),
2043 url: "file:///data.csv".into(),
2044 id: None,
2045 filename: None,
2046 source: None,
2047 });
2048 let json_str = serde_json::to_string(&input).unwrap();
2049 assert!(!json_str.contains("\"id\""));
2050 assert!(!json_str.contains("filename"));
2051 assert!(!json_str.contains("source"));
2052 let back: PartInput = serde_json::from_str(&json_str).unwrap();
2053 assert_eq!(input, back);
2054 }
2055
2056 #[test]
2057 fn session_revert_minimal() {
2058 let revert = SessionRevert {
2059 message_id: "msg_r001".into(),
2060 diff: None,
2061 part_id: None,
2062 snapshot: None,
2063 };
2064 let json_str = serde_json::to_string(&revert).unwrap();
2065 assert!(!json_str.contains("diff"));
2066 assert!(!json_str.contains("partID"));
2067 assert!(!json_str.contains("snapshot"));
2068 let back: SessionRevert = serde_json::from_str(&json_str).unwrap();
2069 assert_eq!(revert, back);
2070 }
2071
2072 #[test]
2073 fn text_part_time_no_end() {
2074 let t = TextPartTime { start: 1_700_000_000.0, end: None };
2075 let json_str = serde_json::to_string(&t).unwrap();
2076 assert!(!json_str.contains("end"));
2077 let back: TextPartTime = serde_json::from_str(&json_str).unwrap();
2078 assert_eq!(t, back);
2079 }
2080
2081 #[test]
2082 fn assistant_message_time_no_completed() {
2083 let t = AssistantMessageTime { created: 1_700_000_000.0, completed: None };
2084 let json_str = serde_json::to_string(&t).unwrap();
2085 assert!(!json_str.contains("completed"));
2086 let back: AssistantMessageTime = serde_json::from_str(&json_str).unwrap();
2087 assert_eq!(t, back);
2088 }
2089
2090 #[test]
2091 fn session_revert_params_no_part_id() {
2092 let params = SessionRevertParams { message_id: "msg_001".into(), part_id: None };
2093 let json_str = serde_json::to_string(¶ms).unwrap();
2094 assert!(!json_str.contains("partID"));
2095 let back: SessionRevertParams = serde_json::from_str(&json_str).unwrap();
2096 assert_eq!(params, back);
2097 }
2098
2099 #[test]
2100 fn file_part_with_symbol_source() {
2101 let part = Part::File(FilePart {
2102 id: "fp_sym".into(),
2103 message_id: "msg_001".into(),
2104 mime: "text/x-rust".into(),
2105 session_id: "sess_001".into(),
2106 url: "file:///src/lib.rs".into(),
2107 filename: Some("lib.rs".into()),
2108 source: Some(FilePartSource::Symbol(SymbolSource {
2109 kind: 6,
2110 name: "MyStruct".into(),
2111 path: "/src/lib.rs".into(),
2112 range: SymbolSourceRange {
2113 end: SymbolSourcePosition { character: 1, line: 10 },
2114 start: SymbolSourcePosition { character: 0, line: 5 },
2115 },
2116 text: FilePartSourceText {
2117 end: 200,
2118 start: 100,
2119 value: "struct MyStruct {}".into(),
2120 },
2121 })),
2122 });
2123 let json_str = serde_json::to_string(&part).unwrap();
2124 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2125 assert_eq!(v["source"]["type"], "symbol");
2126 let back: Part = serde_json::from_str(&json_str).unwrap();
2127 assert_eq!(part, back);
2128 }
2129
2130 #[test]
2133 fn session_summary_round_trip() {
2134 let summary = SessionSummary {
2135 additions: 10.0,
2136 deletions: 3.0,
2137 files: 2.0,
2138 diffs: Some(vec![FileDiff {
2139 file: "src/main.rs".into(),
2140 before: "fn old() {}".into(),
2141 after: "fn new() {}".into(),
2142 additions: 5.0,
2143 deletions: 2.0,
2144 status: Some(FileDiffStatus::Modified),
2145 }]),
2146 };
2147 let json_str = serde_json::to_string(&summary).unwrap();
2148 assert!(json_str.contains("\"status\":\"modified\""));
2149 let back: SessionSummary = serde_json::from_str(&json_str).unwrap();
2150 assert_eq!(summary, back);
2151 }
2152
2153 #[test]
2154 fn session_summary_minimal() {
2155 let summary = SessionSummary { additions: 0.0, deletions: 0.0, files: 0.0, diffs: None };
2156 let json_str = serde_json::to_string(&summary).unwrap();
2157 assert!(!json_str.contains("diffs"));
2158 let back: SessionSummary = serde_json::from_str(&json_str).unwrap();
2159 assert_eq!(summary, back);
2160 }
2161
2162 #[test]
2163 fn file_diff_round_trip() {
2164 let diff = FileDiff {
2165 file: "README.md".into(),
2166 before: "# Old".into(),
2167 after: "# New".into(),
2168 additions: 1.0,
2169 deletions: 1.0,
2170 status: Some(FileDiffStatus::Modified),
2171 };
2172 let json_str = serde_json::to_string(&diff).unwrap();
2173 let back: FileDiff = serde_json::from_str(&json_str).unwrap();
2174 assert_eq!(diff, back);
2175 }
2176
2177 #[test]
2178 fn file_diff_no_status() {
2179 let diff = FileDiff {
2180 file: "new.rs".into(),
2181 before: String::new(),
2182 after: "fn main() {}".into(),
2183 additions: 1.0,
2184 deletions: 0.0,
2185 status: None,
2186 };
2187 let json_str = serde_json::to_string(&diff).unwrap();
2188 assert!(!json_str.contains("status"));
2189 let back: FileDiff = serde_json::from_str(&json_str).unwrap();
2190 assert_eq!(diff, back);
2191 }
2192
2193 #[test]
2194 fn file_diff_status_variants() {
2195 for (variant, expected) in [
2196 (FileDiffStatus::Added, "\"added\""),
2197 (FileDiffStatus::Deleted, "\"deleted\""),
2198 (FileDiffStatus::Modified, "\"modified\""),
2199 ] {
2200 let json_str = serde_json::to_string(&variant).unwrap();
2201 assert_eq!(json_str, expected);
2202 let back: FileDiffStatus = serde_json::from_str(&json_str).unwrap();
2203 assert_eq!(variant, back);
2204 }
2205 }
2206
2207 #[test]
2208 fn permission_rule_round_trip() {
2209 let rule = PermissionRule {
2210 permission: "file:write".into(),
2211 pattern: "src/**".into(),
2212 action: "allow".into(),
2213 };
2214 let json_str = serde_json::to_string(&rule).unwrap();
2215 let back: PermissionRule = serde_json::from_str(&json_str).unwrap();
2216 assert_eq!(rule, back);
2217 }
2218
2219 #[test]
2220 fn permission_ruleset_round_trip() {
2221 let ruleset: PermissionRuleset = vec![
2222 PermissionRule {
2223 permission: "file:write".into(),
2224 pattern: "src/**".into(),
2225 action: "allow".into(),
2226 },
2227 PermissionRule {
2228 permission: "file:read".into(),
2229 pattern: "**".into(),
2230 action: "deny".into(),
2231 },
2232 ];
2233 let json_str = serde_json::to_string(&ruleset).unwrap();
2234 let back: PermissionRuleset = serde_json::from_str(&json_str).unwrap();
2235 assert_eq!(ruleset, back);
2236 }
2237
2238 #[test]
2239 fn session_time_with_compacting_archived() {
2240 let time = SessionTime {
2241 created: 1_700_000_000.0,
2242 updated: 1_700_001_000.0,
2243 compacting: Some(1_700_002_000.0),
2244 archived: Some(1_700_003_000.0),
2245 };
2246 let json_str = serde_json::to_string(&time).unwrap();
2247 assert!(json_str.contains("compacting"));
2248 assert!(json_str.contains("archived"));
2249 let back: SessionTime = serde_json::from_str(&json_str).unwrap();
2250 assert_eq!(time, back);
2251 }
2252
2253 #[test]
2254 fn session_time_without_compacting_archived() {
2255 let time = SessionTime {
2256 created: 1_700_000_000.0,
2257 updated: 1_700_001_000.0,
2258 compacting: None,
2259 archived: None,
2260 };
2261 let json_str = serde_json::to_string(&time).unwrap();
2262 assert!(!json_str.contains("compacting"));
2263 assert!(!json_str.contains("archived"));
2264 let back: SessionTime = serde_json::from_str(&json_str).unwrap();
2265 assert_eq!(time, back);
2266 }
2267
2268 #[test]
2269 fn session_from_spec_compliant_json() {
2270 let json = json!({
2271 "id": "ses_abc123",
2272 "slug": "my-session",
2273 "projectID": "proj_xyz",
2274 "directory": "/home/user/project",
2275 "title": "Full Session",
2276 "version": "2",
2277 "time": {
2278 "created": 1_700_000_000.0,
2279 "updated": 1_700_001_000.0,
2280 "compacting": 1_700_002_000.0,
2281 "archived": 1_700_003_000.0
2282 },
2283 "parentID": "ses_parent",
2284 "summary": {
2285 "additions": 10.0,
2286 "deletions": 3.0,
2287 "files": 2.0,
2288 "diffs": [
2289 {
2290 "file": "src/main.rs",
2291 "before": "old code",
2292 "after": "new code",
2293 "additions": 5.0,
2294 "deletions": 2.0,
2295 "status": "added"
2296 }
2297 ]
2298 },
2299 "share": { "url": "https://example.com/share/abc" },
2300 "permission": [
2301 { "permission": "file:write", "pattern": "src/**", "action": "allow" }
2302 ],
2303 "revert": {
2304 "messageID": "msg_001",
2305 "diff": "some diff",
2306 "partID": "part_001",
2307 "snapshot": "snap"
2308 }
2309 });
2310 let session: Session = serde_json::from_value(json).unwrap();
2311 assert_eq!(session.id, "ses_abc123");
2312 assert_eq!(session.slug, "my-session");
2313 assert_eq!(session.project_id, "proj_xyz");
2314 assert_eq!(session.directory, "/home/user/project");
2315 assert_eq!(session.time.compacting, Some(1_700_002_000.0));
2316 assert_eq!(session.time.archived, Some(1_700_003_000.0));
2317 assert!(session.summary.is_some());
2318 let summary = session.summary.unwrap();
2319 assert_eq!(summary.additions, 10.0);
2320 assert_eq!(summary.diffs.as_ref().unwrap().len(), 1);
2321 assert_eq!(summary.diffs.as_ref().unwrap()[0].status, Some(FileDiffStatus::Added));
2322 assert!(session.permission.is_some());
2323 assert_eq!(session.permission.unwrap().len(), 1);
2324 assert_eq!(session.parent_id.as_deref(), Some("ses_parent"));
2325 }
2326
2327 #[test]
2328 fn session_deserialize_without_new_fields() {
2329 let json = json!({
2331 "id": "ses_old",
2332 "title": "Old Session",
2333 "version": "1",
2334 "time": { "created": 100.0, "updated": 200.0 }
2335 });
2336 let session: Session = serde_json::from_value(json).unwrap();
2337 assert_eq!(session.id, "ses_old");
2338 assert_eq!(session.slug, "");
2339 assert_eq!(session.project_id, "");
2340 assert_eq!(session.directory, "");
2341 assert!(session.summary.is_none());
2342 assert!(session.permission.is_none());
2343 assert!(session.time.compacting.is_none());
2344 assert!(session.time.archived.is_none());
2345 }
2346
2347 #[test]
2348 fn assistant_message_from_spec_compliant_json() {
2349 let json = json!({
2350 "id": "msg_spec",
2351 "sessionID": "sess_spec",
2352 "role": "assistant",
2353 "parentID": "msg_parent_spec",
2354 "modelID": "gpt-4o",
2355 "providerID": "openai",
2356 "mode": "code",
2357 "agent": "coder",
2358 "path": { "cwd": "/project", "root": "/project" },
2359 "cost": 0.005,
2360 "time": { "created": 1_700_000_000.0, "completed": 1_700_000_010.0 },
2361 "tokens": {
2362 "total": 1500,
2363 "input": 1000,
2364 "output": 400,
2365 "reasoning": 100,
2366 "cache": { "read": 500, "write": 200 }
2367 }
2368 });
2369 let msg: AssistantMessage = serde_json::from_value(json).unwrap();
2370 assert_eq!(msg.id, "msg_spec");
2371 assert_eq!(msg.parent_id, "msg_parent_spec");
2372 assert_eq!(msg.agent, "coder");
2373 assert_eq!(msg.tokens.total, 1500);
2374 assert_eq!(msg.tokens.input, 1000);
2375 assert_eq!(msg.tokens.output, 400);
2376 assert_eq!(msg.tokens.reasoning, 100);
2377 assert_eq!(msg.tokens.cache.read, 500);
2378 assert_eq!(msg.tokens.cache.write, 200);
2379 assert!(msg.variant.is_none());
2380 assert!(msg.finish.is_none());
2381 assert!(msg.structured.is_none());
2382 }
2383
2384 #[test]
2385 fn assistant_message_with_optional_fields_populated() {
2386 let json = json!({
2387 "id": "msg_opt",
2388 "sessionID": "sess_opt",
2389 "parentID": "msg_p",
2390 "modelID": "claude-3-opus",
2391 "providerID": "anthropic",
2392 "mode": "code",
2393 "agent": "reviewer",
2394 "path": { "cwd": "/home", "root": "/home" },
2395 "cost": 0.01,
2396 "time": { "created": 1_700_000_000.0 },
2397 "tokens": {
2398 "total": 500,
2399 "input": 300,
2400 "output": 150,
2401 "reasoning": 50,
2402 "cache": { "read": 100, "write": 50 }
2403 },
2404 "variant": "v2",
2405 "finish": "stop",
2406 "structured": { "key": "value" }
2407 });
2408 let msg: AssistantMessage = serde_json::from_value(json).unwrap();
2409 assert_eq!(msg.variant.as_deref(), Some("v2"));
2410 assert_eq!(msg.finish.as_deref(), Some("stop"));
2411 assert_eq!(msg.structured.as_ref().unwrap()["key"], "value");
2412 assert_eq!(msg.parent_id, "msg_p");
2413 assert_eq!(msg.agent, "reviewer");
2414 assert_eq!(msg.tokens.total, 500);
2415 }
2416
2417 #[test]
2420 fn user_message_from_spec_json() {
2421 let json = json!({
2422 "id": "msg_u_spec",
2423 "sessionID": "sess_spec",
2424 "role": "user",
2425 "time": { "created": 1_700_000_000.0 },
2426 "agent": "coder",
2427 "model": { "providerID": "openai", "modelID": "gpt-4o" },
2428 "format": { "type": "text" },
2429 "summary": {
2430 "title": "Summary Title",
2431 "body": "Summary body text",
2432 "diffs": [
2433 {
2434 "file": "src/main.rs",
2435 "before": "old",
2436 "after": "new",
2437 "additions": 1.0,
2438 "deletions": 1.0,
2439 "status": "modified"
2440 }
2441 ]
2442 },
2443 "system": "Be concise.",
2444 "tools": { "bash": true, "read_file": false },
2445 "variant": "v2"
2446 });
2447 let msg: UserMessage = serde_json::from_value(json).unwrap();
2448 assert_eq!(msg.id, "msg_u_spec");
2449 assert_eq!(msg.session_id, "sess_spec");
2450 assert_eq!(msg.agent, "coder");
2451 assert_eq!(msg.model.provider_id, "openai");
2452 assert_eq!(msg.model.model_id, "gpt-4o");
2453 assert!(matches!(msg.format, Some(OutputFormat::Text)));
2454 let summary = msg.summary.unwrap();
2455 assert_eq!(summary.title.as_deref(), Some("Summary Title"));
2456 assert_eq!(summary.body.as_deref(), Some("Summary body text"));
2457 assert_eq!(summary.diffs.len(), 1);
2458 assert_eq!(msg.system.as_deref(), Some("Be concise."));
2459 let tools = msg.tools.unwrap();
2460 assert_eq!(tools.get("bash"), Some(&true));
2461 assert_eq!(tools.get("read_file"), Some(&false));
2462 assert_eq!(msg.variant.as_deref(), Some("v2"));
2463 }
2464
2465 #[test]
2466 fn user_message_with_output_format() {
2467 let msg_text = UserMessage {
2469 id: "msg_fmt_text".into(),
2470 session_id: "sess_001".into(),
2471 time: UserMessageTime { created: 1_700_000_000.0 },
2472 agent: "coder".into(),
2473 model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
2474 format: Some(OutputFormat::Text),
2475 summary: None,
2476 system: None,
2477 tools: None,
2478 variant: None,
2479 };
2480 let json_str = serde_json::to_string(&msg_text).unwrap();
2481 assert!(json_str.contains("\"type\":\"text\""));
2482 let back: UserMessage = serde_json::from_str(&json_str).unwrap();
2483 assert_eq!(msg_text, back);
2484
2485 let msg_schema = UserMessage {
2487 id: "msg_fmt_schema".into(),
2488 session_id: "sess_001".into(),
2489 time: UserMessageTime { created: 1_700_000_000.0 },
2490 agent: "coder".into(),
2491 model: UserMessageModel {
2492 provider_id: "anthropic".into(),
2493 model_id: "claude-3-opus".into(),
2494 },
2495 format: Some(OutputFormat::JsonSchema {
2496 schema: json!({ "type": "object", "properties": { "answer": { "type": "string" } } }),
2497 retry_count: Some(3),
2498 }),
2499 summary: None,
2500 system: None,
2501 tools: None,
2502 variant: None,
2503 };
2504 let json_str = serde_json::to_string(&msg_schema).unwrap();
2505 assert!(json_str.contains("json_schema"));
2506 assert!(json_str.contains("retryCount"));
2507 let back: UserMessage = serde_json::from_str(&json_str).unwrap();
2508 assert_eq!(msg_schema, back);
2509 }
2510
2511 #[test]
2512 fn user_message_with_summary() {
2513 let msg = UserMessage {
2514 id: "msg_sum".into(),
2515 session_id: "sess_001".into(),
2516 time: UserMessageTime { created: 1_700_000_000.0 },
2517 agent: "reviewer".into(),
2518 model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
2519 format: None,
2520 summary: Some(UserMessageSummary {
2521 title: Some("Refactored main".into()),
2522 body: Some("Cleaned up imports".into()),
2523 diffs: vec![FileDiff {
2524 file: "src/main.rs".into(),
2525 before: "use old;".into(),
2526 after: "use new;".into(),
2527 additions: 1.0,
2528 deletions: 1.0,
2529 status: Some(FileDiffStatus::Modified),
2530 }],
2531 }),
2532 system: None,
2533 tools: None,
2534 variant: None,
2535 };
2536 let json_str = serde_json::to_string(&msg).unwrap();
2537 assert!(json_str.contains("Refactored main"));
2538 assert!(json_str.contains("Cleaned up imports"));
2539 let back: UserMessage = serde_json::from_str(&json_str).unwrap();
2540 assert_eq!(msg, back);
2541 }
2542
2543 #[test]
2546 fn part_subtask_round_trip() {
2547 let part = Part::Subtask(SubtaskPart {
2548 id: "p_sub_001".into(),
2549 session_id: "sess_001".into(),
2550 message_id: "msg_a001".into(),
2551 prompt: "Fix the bug".into(),
2552 description: "Fix the null pointer bug in parser".into(),
2553 agent: "coder".into(),
2554 model: Some(SubtaskPartModel {
2555 provider_id: "openai".into(),
2556 model_id: "gpt-4o".into(),
2557 }),
2558 command: Some("cargo test".into()),
2559 });
2560 let json_str = serde_json::to_string(&part).unwrap();
2561 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2562 assert_eq!(v["type"], "subtask");
2563 assert_eq!(v["sessionID"], "sess_001");
2564 assert_eq!(v["messageID"], "msg_a001");
2565 let back: Part = serde_json::from_str(&json_str).unwrap();
2566 assert_eq!(part, back);
2567
2568 let minimal = Part::Subtask(SubtaskPart {
2570 id: "p_sub_002".into(),
2571 session_id: "sess_001".into(),
2572 message_id: "msg_a001".into(),
2573 prompt: "Do it".into(),
2574 description: "desc".into(),
2575 agent: "coder".into(),
2576 model: None,
2577 command: None,
2578 });
2579 let json_str = serde_json::to_string(&minimal).unwrap();
2580 assert!(!json_str.contains("model"));
2581 assert!(!json_str.contains("command"));
2582 let back: Part = serde_json::from_str(&json_str).unwrap();
2583 assert_eq!(minimal, back);
2584 }
2585
2586 #[test]
2587 fn part_reasoning_round_trip() {
2588 let part = Part::Reasoning(ReasoningPart {
2589 id: "p_reason_001".into(),
2590 session_id: "sess_001".into(),
2591 message_id: "msg_a001".into(),
2592 text: "Let me think about this...".into(),
2593 metadata: Some(HashMap::from([("key".into(), json!("value"))])),
2594 time: ReasoningPartTime { start: 1_700_000_200.0, end: Some(1_700_000_201.0) },
2595 });
2596 let json_str = serde_json::to_string(&part).unwrap();
2597 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2598 assert_eq!(v["type"], "reasoning");
2599 assert_eq!(v["sessionID"], "sess_001");
2600 assert_eq!(v["messageID"], "msg_a001");
2601 let back: Part = serde_json::from_str(&json_str).unwrap();
2602 assert_eq!(part, back);
2603
2604 let minimal = Part::Reasoning(ReasoningPart {
2606 id: "p_reason_002".into(),
2607 session_id: "sess_001".into(),
2608 message_id: "msg_a001".into(),
2609 text: "thinking".into(),
2610 metadata: None,
2611 time: ReasoningPartTime { start: 1_700_000_200.0, end: None },
2612 });
2613 let json_str = serde_json::to_string(&minimal).unwrap();
2614 assert!(!json_str.contains("metadata"));
2615 let back: Part = serde_json::from_str(&json_str).unwrap();
2616 assert_eq!(minimal, back);
2617 }
2618
2619 #[test]
2620 fn part_agent_round_trip() {
2621 let part = Part::Agent(AgentPart {
2622 id: "p_agent_001".into(),
2623 session_id: "sess_001".into(),
2624 message_id: "msg_a001".into(),
2625 name: "coder".into(),
2626 source: Some(AgentPartSource {
2627 value: "some source content".into(),
2628 start: 0,
2629 end: 42,
2630 }),
2631 });
2632 let json_str = serde_json::to_string(&part).unwrap();
2633 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2634 assert_eq!(v["type"], "agent");
2635 assert_eq!(v["sessionID"], "sess_001");
2636 assert_eq!(v["messageID"], "msg_a001");
2637 let back: Part = serde_json::from_str(&json_str).unwrap();
2638 assert_eq!(part, back);
2639
2640 let minimal = Part::Agent(AgentPart {
2642 id: "p_agent_002".into(),
2643 session_id: "sess_001".into(),
2644 message_id: "msg_a001".into(),
2645 name: "reviewer".into(),
2646 source: None,
2647 });
2648 let json_str = serde_json::to_string(&minimal).unwrap();
2649 assert!(!json_str.contains("source"));
2650 let back: Part = serde_json::from_str(&json_str).unwrap();
2651 assert_eq!(minimal, back);
2652 }
2653
2654 #[test]
2655 fn part_compaction_round_trip() {
2656 let part = Part::Compaction(CompactionPart {
2657 id: "p_compact_001".into(),
2658 session_id: "sess_001".into(),
2659 message_id: "msg_a001".into(),
2660 auto: true,
2661 });
2662 let json_str = serde_json::to_string(&part).unwrap();
2663 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2664 assert_eq!(v["type"], "compaction");
2665 assert_eq!(v["sessionID"], "sess_001");
2666 assert_eq!(v["messageID"], "msg_a001");
2667 assert_eq!(v["auto"], true);
2668 let back: Part = serde_json::from_str(&json_str).unwrap();
2669 assert_eq!(part, back);
2670
2671 let part_false = Part::Compaction(CompactionPart {
2673 id: "p_compact_002".into(),
2674 session_id: "sess_001".into(),
2675 message_id: "msg_a001".into(),
2676 auto: false,
2677 });
2678 let json_str = serde_json::to_string(&part_false).unwrap();
2679 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2680 assert_eq!(v["auto"], false);
2681 let back: Part = serde_json::from_str(&json_str).unwrap();
2682 assert_eq!(part_false, back);
2683 }
2684
2685 #[test]
2686 fn part_retry_round_trip() {
2687 let part = Part::Retry(RetryPart {
2688 id: "p_retry_001".into(),
2689 session_id: "sess_001".into(),
2690 message_id: "msg_a001".into(),
2691 attempt: 2.0,
2692 error: json!({ "message": "rate limited", "code": 429 }),
2693 time: RetryPartTime { created: 1_700_000_200.0 },
2694 });
2695 let json_str = serde_json::to_string(&part).unwrap();
2696 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2697 assert_eq!(v["type"], "retry");
2698 assert_eq!(v["sessionID"], "sess_001");
2699 assert_eq!(v["messageID"], "msg_a001");
2700 assert_eq!(v["attempt"], 2.0);
2701 let back: Part = serde_json::from_str(&json_str).unwrap();
2702 assert_eq!(part, back);
2703 }
2704
2705 #[test]
2706 fn output_format_round_trip() {
2707 let text = OutputFormat::Text;
2709 let json_str = serde_json::to_string(&text).unwrap();
2710 let back: OutputFormat = serde_json::from_str(&json_str).unwrap();
2711 assert_eq!(text, back);
2712
2713 let schema_no_retry =
2715 OutputFormat::JsonSchema { schema: json!({ "type": "string" }), retry_count: None };
2716 let json_str = serde_json::to_string(&schema_no_retry).unwrap();
2717 assert!(!json_str.contains("retryCount"));
2718 let back: OutputFormat = serde_json::from_str(&json_str).unwrap();
2719 assert_eq!(schema_no_retry, back);
2720
2721 let schema_retry =
2723 OutputFormat::JsonSchema { schema: json!({ "type": "object" }), retry_count: Some(2) };
2724 let json_str = serde_json::to_string(&schema_retry).unwrap();
2725 assert!(json_str.contains("retryCount"));
2726 let back: OutputFormat = serde_json::from_str(&json_str).unwrap();
2727 assert_eq!(schema_retry, back);
2728 }
2729}