1use std::fmt;
11use std::path::PathBuf;
12
13use bytes::Bytes;
14use chrono::{DateTime, Utc};
15use indexmap::IndexMap;
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use thiserror::Error;
20use uuid::Uuid;
21
22pub type SharedStr = String;
24
25pub const DEFAULT_TEMPERATURE: f32 = 0.7;
29pub type MediaType = String;
31pub type ReplaySignature = String;
33pub type ContentHash = String;
35pub type Timestamp = DateTime<Utc>;
37
38macro_rules! id_type {
39 ($name:ident) => {
40 #[derive(
41 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
42 )]
43 #[doc = concat!("Opaque identifier for a protocol `", stringify!($name), "`.")]
44 pub struct $name(pub String);
45
46 impl $name {
47 #[must_use]
49 pub fn new() -> Self {
50 Self(Uuid::new_v4().to_string())
51 }
52 }
53
54 impl Default for $name {
55 fn default() -> Self {
56 Self::new()
57 }
58 }
59
60 impl fmt::Display for $name {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str(&self.0)
63 }
64 }
65
66 impl From<&str> for $name {
67 fn from(value: &str) -> Self {
68 Self(value.to_owned())
69 }
70 }
71
72 impl From<String> for $name {
73 fn from(value: String) -> Self {
74 Self(value)
75 }
76 }
77 };
78}
79
80macro_rules! string_wrapper {
81 ($name:ident) => {
82 #[derive(
83 Debug,
84 Clone,
85 PartialEq,
86 Eq,
87 PartialOrd,
88 Ord,
89 Hash,
90 Default,
91 Serialize,
92 Deserialize,
93 JsonSchema,
94 )]
95 #[doc = concat!("String newtype for a protocol `", stringify!($name), "`.")]
96 pub struct $name(pub String);
97
98 impl fmt::Display for $name {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 f.write_str(&self.0)
101 }
102 }
103
104 impl From<&str> for $name {
105 fn from(value: &str) -> Self {
106 Self(value.to_owned())
107 }
108 }
109
110 impl From<String> for $name {
111 fn from(value: String) -> Self {
112 Self(value)
113 }
114 }
115 };
116}
117
118id_type!(MessageId);
119id_type!(BlockId);
120id_type!(ToolCallId);
121id_type!(PromptId);
122id_type!(PromptSegmentId);
123id_type!(SessionId);
124id_type!(TurnId);
125id_type!(SkillId);
126id_type!(PluginId);
127id_type!(AgentId);
128
129string_wrapper!(Revision);
130string_wrapper!(ModelId);
131string_wrapper!(ToolName);
132string_wrapper!(ToolAlias);
133string_wrapper!(SkillName);
134string_wrapper!(AgentName);
135string_wrapper!(ProviderName);
136
137#[derive(
138 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
139)]
140#[serde(rename_all = "snake_case")]
141pub enum ModelRole {
143 Default,
145 Plan,
147 Subagent,
149 Small,
151}
152
153impl ModelRole {
154 #[must_use]
156 pub const fn default_role() -> Self {
157 Self::Default
158 }
159
160 #[must_use]
162 pub const fn plan() -> Self {
163 Self::Plan
164 }
165
166 #[must_use]
168 pub const fn subagent() -> Self {
169 Self::Subagent
170 }
171
172 #[must_use]
174 pub const fn small() -> Self {
175 Self::Small
176 }
177
178 #[must_use]
180 pub const fn as_str(&self) -> &'static str {
181 match self {
182 Self::Default => "default",
183 Self::Plan => "plan",
184 Self::Subagent => "subagent",
185 Self::Small => "small",
186 }
187 }
188}
189
190impl Default for ModelRole {
191 fn default() -> Self {
192 Self::default_role()
193 }
194}
195
196impl std::str::FromStr for ModelRole {
197 type Err = String;
198
199 fn from_str(value: &str) -> Result<Self, Self::Err> {
200 match value {
201 "default" => Ok(Self::Default),
202 "plan" => Ok(Self::Plan),
203 "subagent" => Ok(Self::Subagent),
204 "small" => Ok(Self::Small),
205 other => Err(format!(
206 "unknown ModelRole '{other}'; expected one of: default, plan, subagent, small"
207 )),
208 }
209 }
210}
211
212impl fmt::Display for ModelRole {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 f.write_str(self.as_str())
215 }
216}
217
218#[derive(
219 Debug,
220 Clone,
221 Copy,
222 Default,
223 PartialEq,
224 Eq,
225 PartialOrd,
226 Ord,
227 Hash,
228 Serialize,
229 Deserialize,
230 JsonSchema,
231)]
232#[serde(rename_all = "snake_case")]
233pub enum SubagentEventForwarding {
235 #[default]
237 Off,
238 All,
240}
241
242impl SubagentEventForwarding {
243 #[must_use]
245 pub const fn is_enabled(self) -> bool {
246 matches!(self, Self::All)
247 }
248}
249
250#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
251#[serde(rename_all = "snake_case")]
252pub enum ProviderKind {
254 Anthropic,
256 OpenAi,
258 OpenRouter,
260 Fake,
262}
263
264#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
265#[serde(rename_all = "snake_case")]
266pub enum ApiKind {
268 AnthropicMessages,
270 OpenAiResponses,
272 OpenAiChat,
274 Fake,
276}
277
278#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
279#[serde(rename_all = "snake_case")]
280pub enum ReasoningEffort {
282 Low,
284 Medium,
286 High,
288 Xhigh,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
293pub struct Usage {
295 pub input_tokens: u64,
296 pub output_tokens: u64,
297 pub cache_creation_input_tokens: u64,
298 pub cache_read_input_tokens: u64,
299}
300
301#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
302#[serde(rename_all = "snake_case")]
303pub enum StopReason {
305 EndTurn,
307 ToolUse,
309 Interrupted,
311 MaxTokens,
313 Error,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
318pub struct ReplayMeta {
320 pub provider_name: Option<ProviderName>,
321 pub model: Option<ModelId>,
322}
323
324#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
325#[serde(rename_all = "snake_case")]
326pub enum HookWarningSeverity {
328 #[default]
330 Warning,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
334pub struct HookWarning {
336 pub severity: HookWarningSeverity,
337 pub category: SharedStr,
338 pub plugin_id: Option<PluginId>,
339 pub plugin_name: Option<SharedStr>,
340 pub source_path: Option<PathBuf>,
341 pub message: SharedStr,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
345pub struct SystemMessage {
347 pub id: MessageId,
348 pub created_at: Timestamp,
349 pub text: SharedStr,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
353pub struct UserMessage {
355 pub id: MessageId,
356 pub created_at: Timestamp,
357 pub parts: Vec<UserPart>,
358}
359
360impl UserMessage {
361 #[must_use]
363 pub fn text(text: impl Into<String>) -> Self {
364 Self {
365 id: MessageId::new(),
366 created_at: Utc::now(),
367 parts: vec![UserPart::Text { text: text.into() }],
368 }
369 }
370
371 #[must_use]
373 pub fn plain_text(&self) -> String {
374 self.parts
375 .iter()
376 .filter_map(|part| match part {
377 UserPart::Text { text } => Some(text.as_str()),
378 UserPart::Image { .. } | UserPart::Document { .. } => None,
379 })
380 .collect::<Vec<_>>()
381 .join("\n")
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
386#[serde(tag = "kind", rename_all = "snake_case")]
387pub enum UserPart {
389 Text { text: SharedStr },
391 Image {
393 media_type: MediaType,
394 #[schemars(with = "Vec<u8>")]
395 data: Bytes,
396 },
397 Document {
399 media_type: MediaType,
400 #[schemars(with = "Vec<u8>")]
401 data: Bytes,
402 },
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
406pub struct AssistantMessage {
408 pub id: MessageId,
409 pub created_at: Timestamp,
410 pub parts: Vec<AssistantPart>,
411 pub stop_reason: Option<StopReason>,
412 pub usage: Option<Usage>,
413 pub replay_meta: ReplayMeta,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
417#[serde(tag = "kind", rename_all = "snake_case")]
418pub enum AssistantPart {
420 Text { text: SharedStr },
422 Thinking(ThinkingBlock),
424 ToolCall(ToolCall),
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
429pub struct ThinkingBlock {
431 pub text: SharedStr,
432 pub signature: Option<ReplaySignature>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
436pub struct ToolCall {
438 pub id: ToolCallId,
439 pub name: ToolName,
440 pub arguments: Value,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
444pub struct ToolResultMessage {
446 pub id: MessageId,
447 pub call_id: ToolCallId,
448 pub content: ToolResult,
449 pub error: Option<ToolError>,
450 pub created_at: Timestamp,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
454#[serde(tag = "role", rename_all = "snake_case")]
455pub enum Message {
457 System(SystemMessage),
459 User(UserMessage),
461 Assistant(AssistantMessage),
463 Tool(ToolResultMessage),
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
468#[serde(tag = "kind", rename_all = "snake_case")]
469pub enum StreamEvent {
471 MessageStart { id: MessageId },
473 TextStart { id: BlockId },
475 TextDelta { id: BlockId, delta: SharedStr },
477 TextEnd { id: BlockId },
479 ThinkingStart { id: BlockId },
481 ThinkingDelta { id: BlockId, delta: SharedStr },
483 ThinkingEnd {
485 id: BlockId,
486 signature: Option<ReplaySignature>,
487 },
488 ToolCallStart {
490 id: BlockId,
491 tool_call_id: ToolCallId,
492 name: ToolName,
493 },
494 ToolArgsDelta { id: BlockId, delta: SharedStr },
496 ToolCallEnd { id: BlockId },
498 UsageUpdate { usage: Usage },
500 MessageEnd {
502 id: MessageId,
503 stop_reason: StopReason,
504 #[serde(default, skip_serializing_if = "Option::is_none")]
506 response_id: Option<String>,
507 },
508 ProviderWarning { message: SharedStr },
510 Error { error: ProviderError },
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
515pub struct Turn {
517 pub id: TurnId,
518 pub user_message: UserMessage,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub default_model: Option<ModelId>,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub subagent_model: Option<ModelId>,
523}
524
525impl Turn {
526 #[must_use]
528 pub fn user(text: impl Into<String>) -> Self {
529 Self {
530 id: TurnId::new(),
531 user_message: UserMessage::text(text),
532 default_model: None,
533 subagent_model: None,
534 }
535 }
536
537 #[must_use]
539 pub fn with_default_model(mut self, model: impl Into<ModelId>) -> Self {
540 self.default_model = Some(model.into());
541 self
542 }
543
544 #[must_use]
546 pub fn with_subagent_model(mut self, model: impl Into<ModelId>) -> Self {
547 self.subagent_model = Some(model.into());
548 self
549 }
550}
551
552#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
553#[serde(rename_all = "snake_case")]
554pub enum SubagentState {
556 Running,
558 Completed,
560 Failed,
562 Cancelled,
564 Closed,
566}
567
568impl SubagentState {
569 #[must_use]
571 pub fn is_terminal(self) -> bool {
572 matches!(
573 self,
574 Self::Completed | Self::Failed | Self::Cancelled | Self::Closed
575 )
576 }
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
580pub struct SpawnSubagentRequest {
582 pub message: String,
583 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub agent_type: Option<AgentName>,
585 #[serde(default)]
586 pub fork_context: bool,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
588 pub model: Option<ModelId>,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
592pub struct SendSubagentInputRequest {
594 pub target: AgentId,
595 pub message: String,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
599pub struct WaitSubagentRequest {
601 pub targets: Vec<AgentId>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub timeout_ms: Option<u64>,
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
607pub struct CloseSubagentRequest {
609 pub target: AgentId,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
613pub struct SubagentStatus {
615 pub agent_id: AgentId,
616 pub session_id: SessionId,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub agent_type: Option<AgentName>,
619 pub task: String,
620 pub state: SubagentState,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub last_message: Option<String>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub usage: Option<Usage>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub error: Option<String>,
627}
628
629impl SubagentStatus {
630 #[must_use]
632 pub fn is_terminal(&self) -> bool {
633 self.state.is_terminal()
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
638pub struct WaitSubagentResponse {
640 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub status: Option<SubagentStatus>,
642 pub timed_out: bool,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
646pub struct CloseSubagentResponse {
648 pub previous_status: SubagentStatus,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
652pub struct SubagentSpecWire {
654 pub role: Option<ModelRole>,
655 pub task: String,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
659#[serde(tag = "kind", rename_all = "snake_case")]
660pub enum SessionCommand {
662 SubmitTurn { turn: Turn },
664 InterruptTurn,
666 AppendSystemPrompt { id: PromptId, text: SharedStr },
668 SetModelRole { role: ModelRole },
670 SetModel { model: ModelId },
672 SpawnSubagent { spec: SubagentSpecWire },
674 ReloadResources,
676 Shutdown,
678}
679
680#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
681pub enum Delivery {
683 Lossless,
685 BestEffort,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
690pub struct DeltaItem {
692 pub text: String,
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
696pub struct ToolExecutionOutcome {
698 pub call: ToolCall,
699 pub result: Result<ToolResult, ToolError>,
700}
701
702#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
703#[serde(rename_all = "snake_case")]
704pub enum HookHandlerType {
706 Command,
707 Http,
708 Prompt,
709 Agent,
710 Callback,
711 Function,
712}
713
714#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
715#[serde(rename_all = "snake_case")]
716pub enum HookRunStatus {
718 Running,
719 Completed,
720 Failed,
721 Blocked,
722 Stopped,
723 Cancelled,
724}
725
726#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
727#[serde(rename_all = "snake_case")]
728pub enum HookOutputKind {
730 Warning,
731 Stop,
732 Feedback,
733 Context,
734 Error,
735}
736
737#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
738pub struct HookOutputEntry {
740 pub kind: HookOutputKind,
741 pub text: String,
742}
743
744#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
745#[serde(rename_all = "snake_case")]
746pub enum HookSessionStartSource {
748 Startup,
749 Resume,
750 Clear,
751 Compact,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
755pub struct HookRunSummary {
757 pub run_id: String,
758 pub event_name: String,
759 pub handler_type: HookHandlerType,
760 pub plugin_id: PluginId,
761 pub plugin_root: PathBuf,
762 pub status: HookRunStatus,
763 pub status_message: Option<String>,
764 pub started_at: Timestamp,
765 pub completed_at: Option<Timestamp>,
766 pub duration_ms: Option<u64>,
767 pub entries: Vec<HookOutputEntry>,
768}
769
770#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
771#[serde(tag = "kind", rename_all = "snake_case")]
772pub enum SessionEventPayload {
774 SessionStarted,
775 Warning {
776 message: String,
777 },
778 TurnStarted {
779 turn_id: TurnId,
780 },
781 MessageItem {
782 message: Message,
783 },
784 DeltaItem {
785 delta: DeltaItem,
786 },
787 ToolExecutionStarted {
788 call: ToolCall,
789 },
790 ToolOutput {
791 call_id: ToolCallId,
792 tool_name: ToolName,
793 chunk: SharedStr,
794 },
795 HookStarted {
796 run: HookRunSummary,
797 },
798 HookCompleted {
799 run: HookRunSummary,
800 },
801 ToolExecutionCompleted {
802 outcome: ToolExecutionOutcome,
803 },
804 ApprovalRequested {
805 tool_name: ToolName,
806 reason: String,
807 },
808 ContextCompacted {
809 summary: String,
810 },
811 TurnCompleted {
812 turn_id: TurnId,
813 usage: Usage,
814 },
815 TurnFailed {
816 turn_id: TurnId,
817 error: String,
818 #[serde(default)]
820 cancelled: bool,
821 #[serde(default)]
825 retryable: bool,
826 },
827 Lagged {
828 dropped_events: u64,
829 },
830 SessionShutdownComplete,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
839pub struct SessionEvent {
840 pub session_id: SessionId,
841 pub(crate) sequence: u64,
842 pub delivery: Delivery,
843 pub payload: SessionEventPayload,
844}
845
846impl SessionEvent {
847 #[must_use]
851 pub fn new_committed(
852 session_id: SessionId,
853 sequence: u64,
854 delivery: Delivery,
855 payload: SessionEventPayload,
856 ) -> Self {
857 Self {
858 session_id,
859 sequence,
860 delivery,
861 payload,
862 }
863 }
864
865 #[must_use]
867 pub fn sequence(&self) -> u64 {
868 self.sequence
869 }
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
877pub struct PendingEvent {
878 pub session_id: SessionId,
879 pub delivery: Delivery,
880 pub payload: SessionEventPayload,
881}
882
883impl PendingEvent {
884 #[must_use]
886 pub fn new(session_id: SessionId, delivery: Delivery, payload: SessionEventPayload) -> Self {
887 Self {
888 session_id,
889 delivery,
890 payload,
891 }
892 }
893
894 #[must_use]
896 pub fn into_committed(self, sequence: u64) -> SessionEvent {
897 SessionEvent {
898 session_id: self.session_id,
899 sequence,
900 delivery: self.delivery,
901 payload: self.payload,
902 }
903 }
904}
905
906#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
907pub enum ToolConcurrency {
909 Exclusive,
911 ReadOnly,
913 ParallelSafe,
915}
916
917#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
918pub struct ToolCapabilities {
920 pub mutating: bool,
921 pub requires_approval: bool,
922 pub cancellable: bool,
923 pub long_running: bool,
924}
925
926#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
927pub struct ToolSpec {
929 pub name: ToolName,
930 pub description: SharedStr,
931 pub input_schema: Value,
932 pub concurrency: ToolConcurrency,
933 pub capabilities: ToolCapabilities,
934 pub provider_aliases: IndexMap<ProviderKind, ToolAlias>,
935}
936
937#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
938#[serde(tag = "kind", rename_all = "snake_case")]
939pub enum ToolResult {
941 Empty,
943 Text { text: String },
945 Json { value: Value },
947}
948
949#[derive(Debug, Clone, Error, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
950#[error("{message}")]
951pub struct ToolError {
953 pub message: String,
954}
955
956impl ToolError {
957 #[must_use]
959 pub fn new(message: impl Into<String>) -> Self {
960 Self {
961 message: message.into(),
962 }
963 }
964}
965
966#[derive(Debug, Clone, Error, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
967#[error("{message}")]
968pub struct ProviderError {
970 pub message: String,
971 pub retryable: bool,
972}
973
974impl ProviderError {
975 pub const CANCELLED_MESSAGE: &str = "failed to execute provider request: request cancelled";
979
980 #[must_use]
982 pub fn new(message: impl Into<String>, retryable: bool) -> Self {
983 Self {
984 message: message.into(),
985 retryable,
986 }
987 }
988
989 #[must_use]
993 pub fn cancelled() -> Self {
994 Self {
995 message: Self::CANCELLED_MESSAGE.to_owned(),
996 retryable: false,
997 }
998 }
999
1000 #[must_use]
1002 pub fn is_cancelled(&self) -> bool {
1003 self.message == Self::CANCELLED_MESSAGE
1004 }
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1008pub struct PromptSegment {
1010 pub id: PromptSegmentId,
1011 pub text: SharedStr,
1012 pub volatility: Volatility,
1013 pub cache_scope: CacheScope,
1014 pub content_hash: ContentHash,
1015 #[serde(default)]
1020 pub kind: PromptSegmentKind,
1021}
1022
1023#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1024#[serde(rename_all = "snake_case")]
1025pub enum PromptSegmentKind {
1027 #[default]
1029 System,
1030 Skill,
1032 Append,
1034}
1035
1036#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1037pub enum Volatility {
1039 Static,
1041 SessionStable,
1043 TurnDynamic,
1045 AlwaysDynamic,
1047}
1048
1049#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1050pub enum CacheScope {
1052 PrefixCacheable,
1054 Dynamic,
1056}
1057
1058#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1066pub struct CacheBreakpoints {
1067 pub after_system: bool,
1068 pub after_tools: bool,
1069 pub after_skills: bool,
1070 pub after_user_prompt: bool,
1071}
1072
1073impl CacheBreakpoints {
1074 #[must_use]
1078 pub fn all() -> Self {
1079 Self {
1080 after_system: true,
1081 after_tools: true,
1082 after_skills: true,
1083 after_user_prompt: true,
1084 }
1085 }
1086
1087 #[must_use]
1089 pub fn count_active(&self) -> usize {
1090 usize::from(self.after_system)
1091 + usize::from(self.after_tools)
1092 + usize::from(self.after_skills)
1093 + usize::from(self.after_user_prompt)
1094 }
1095}
1096
1097pub type FileViewCache = IndexMap<PathBuf, FileViewEntry>;
1099
1100#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1101pub struct FileViewEntry {
1103 pub path: PathBuf,
1104 pub full_hash: ContentHash,
1105 pub mtime: Timestamp,
1106 pub size: u64,
1107 pub viewed_ranges: Vec<ViewedRange>,
1108 pub last_shown_turn: TurnId,
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1112pub struct ViewedRange {
1114 pub start_line: u32,
1115 pub end_line: u32,
1116 pub line_anchors: Vec<LineAnchor>,
1117}
1118
1119#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1120pub struct LineAnchor {
1122 pub line: u32,
1123 pub anchor: [u8; 3],
1124}
1125
1126#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1127pub struct PendingToolCall {
1129 pub call: ToolCall,
1130 pub submitted_at: Timestamp,
1131}
1132
1133#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1134pub struct SummarySlice {
1136 pub id: String,
1137 pub text: String,
1138}
1139
1140#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1141pub struct TranscriptWindow {
1143 pub messages: Vec<Message>,
1144 pub elided_message_count: u64,
1145}
1146
1147#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1148#[serde(transparent)]
1149pub struct CompactedContext(pub Vec<Value>);
1151
1152impl CompactedContext {
1153 #[must_use]
1155 pub fn new(items: Vec<Value>) -> Self {
1156 Self(items)
1157 }
1158
1159 #[must_use]
1161 pub fn items(&self) -> &[Value] {
1162 &self.0
1163 }
1164
1165 #[must_use]
1167 pub fn into_items(self) -> Vec<Value> {
1168 self.0
1169 }
1170
1171 #[must_use]
1173 pub fn is_empty(&self) -> bool {
1174 self.0.is_empty()
1175 }
1176
1177 #[must_use]
1179 pub fn len(&self) -> usize {
1180 self.0.len()
1181 }
1182}
1183
1184impl From<Vec<Value>> for CompactedContext {
1185 fn from(value: Vec<Value>) -> Self {
1186 Self(value)
1187 }
1188}
1189
1190impl AsRef<[Value]> for CompactedContext {
1191 fn as_ref(&self) -> &[Value] {
1192 self.items()
1193 }
1194}
1195
1196#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1197pub struct CompactionWindow {
1199 pub eligible_messages: Vec<Message>,
1200 pub preserved_messages: Vec<Message>,
1201 pub reserved_response_block: bool,
1202}
1203
1204impl CompactionWindow {
1205 #[must_use]
1210 pub fn preserve_latest_assistant_response_block(messages: &[Message]) -> Self {
1211 let Some(last_assistant_index) = messages
1212 .iter()
1213 .rposition(|message| matches!(message, Message::Assistant(_)))
1214 else {
1215 return Self {
1216 eligible_messages: messages.to_vec(),
1217 preserved_messages: Vec::new(),
1218 reserved_response_block: false,
1219 };
1220 };
1221
1222 Self {
1223 eligible_messages: messages[..last_assistant_index].to_vec(),
1224 preserved_messages: messages[last_assistant_index..].to_vec(),
1225 reserved_response_block: true,
1226 }
1227 }
1228
1229 #[must_use]
1234 pub fn preserve_through_latest_user(messages: &[Message]) -> Self {
1235 let Some(last_user_index) = messages
1236 .iter()
1237 .rposition(|message| matches!(message, Message::User(_)))
1238 else {
1239 return Self {
1240 eligible_messages: Vec::new(),
1241 preserved_messages: messages.to_vec(),
1242 reserved_response_block: false,
1243 };
1244 };
1245 let pivot = last_user_index + 1;
1246 Self {
1247 eligible_messages: messages[pivot..].to_vec(),
1248 preserved_messages: messages[..pivot].to_vec(),
1249 reserved_response_block: false,
1250 }
1251 }
1252}
1253
1254#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1255pub struct FileViewSlice {
1257 pub path: PathBuf,
1258 pub full_hash: ContentHash,
1259 pub viewed_ranges: Vec<ViewedRange>,
1260 pub last_shown_turn: TurnId,
1261}
1262
1263#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1264pub struct ElisionMarker {
1266 pub kind: String,
1267 pub count: u64,
1268}
1269
1270#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1271pub struct MemoryItem {
1273 pub key: String,
1274 pub text: String,
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1278pub struct SubagentRef {
1280 pub session_id: SessionId,
1281 pub task: String,
1282}
1283
1284#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1285pub struct SessionBlueprint {
1287 pub session_id: SessionId,
1288 pub parent_session_id: Option<SessionId>,
1289 pub default_model: ModelId,
1290 pub subagent_model: ModelId,
1291 #[serde(default)]
1292 pub subagent_event_forwarding: SubagentEventForwarding,
1293 pub snapshot_revision: Revision,
1294 pub working_dir: PathBuf,
1295 pub system_prompt_seed: Vec<PromptSegment>,
1296 pub max_turns: Option<u32>,
1297 pub subagent_depth: u32,
1298}
1299
1300#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1301pub struct SessionState {
1303 pub messages: Vec<Message>,
1304 #[serde(default)]
1305 pub compacted_prefix: Vec<Value>,
1306 pub file_view_cache: FileViewCache,
1307 pub appended_prompt_segments: Vec<PromptSegment>,
1308 pub pending_tool_calls: IndexMap<ToolCallId, PendingToolCall>,
1309 pub usage_so_far: Usage,
1310 pub summaries: Vec<SummarySlice>,
1311 pub lineage: Vec<SubagentRef>,
1312 pub fired_hook_ids: Vec<String>,
1313 pub pending_session_start_source: Option<HookSessionStartSource>,
1314 pub pending_warning_messages: Vec<HookWarning>,
1315 #[serde(default, skip_serializing_if = "Option::is_none")]
1318 pub last_response_id: Option<String>,
1319 #[serde(default)]
1322 pub messages_seen_by_provider: usize,
1323}
1324
1325#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1326pub struct ObservedState {
1328 pub cwd: PathBuf,
1329 pub git_branch: Option<String>,
1330 pub git_dirty: Option<bool>,
1331 pub now_utc: Timestamp,
1332 pub env_facts: IndexMap<String, String>,
1333}
1334
1335#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1336pub struct InstructionFile {
1338 pub path: PathBuf,
1339 pub body: String,
1340}
1341
1342#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1343pub struct SkillDef {
1345 pub id: SkillId,
1346 pub name: String,
1347 pub description: String,
1348 pub body: String,
1349}
1350
1351#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1352pub struct AgentDef {
1354 pub id: AgentId,
1355 pub name: String,
1356 pub prompt: String,
1357}
1358
1359#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1360pub struct PluginManifest {
1362 pub name: String,
1363 pub version: String,
1364 pub skills: Vec<String>,
1365 pub agents: Vec<String>,
1366 pub hooks: Option<String>,
1367 pub mcp_servers: Option<String>,
1368 pub lsp_servers: Option<String>,
1369 pub allowed_http_hosts: Vec<String>,
1370 pub allowed_env_vars: Vec<String>,
1371}
1372
1373#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1374pub struct PromptRegistry {
1376 pub prompts: IndexMap<String, Vec<PromptSegment>>,
1377}
1378
1379#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1380pub struct ResourceSnapshot {
1382 pub revision: Revision,
1383 pub tools: IndexMap<ToolName, ToolSpec>,
1384 pub skills: IndexMap<SkillName, SkillDef>,
1385 pub agents: IndexMap<AgentName, AgentDef>,
1386 pub prompts: PromptRegistry,
1387 pub plugins: IndexMap<PluginId, PluginManifest>,
1388 pub instruction_files: Vec<InstructionFile>,
1389}
1390
1391impl ResourceSnapshot {
1392 #[must_use]
1394 pub fn empty() -> Self {
1395 Self {
1396 revision: Revision("empty".to_owned()),
1397 tools: IndexMap::new(),
1398 skills: IndexMap::new(),
1399 agents: IndexMap::new(),
1400 prompts: PromptRegistry::default(),
1401 plugins: IndexMap::new(),
1402 instruction_files: Vec::new(),
1403 }
1404 }
1405}
1406
1407#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1408pub struct ProviderCapabilities {
1410 pub supports_tools: bool,
1411 pub supports_streaming: bool,
1412 pub supports_reasoning: bool,
1413 pub supports_interleaved_reasoning: bool,
1414 pub supports_images: bool,
1415 pub supports_documents: bool,
1416 pub supports_prompt_cache: bool,
1417 pub supports_compaction: bool,
1418 #[serde(default)]
1422 pub compaction_strategy: Option<ProviderCompactionStrategy>,
1423 pub supports_tool_result_media: bool,
1424 pub requires_non_empty_assistant_content: bool,
1425 pub tool_call_id_policy: ToolCallIdPolicy,
1426 pub max_input_tokens: u64,
1427 pub max_output_tokens: u64,
1428}
1429
1430#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1431#[serde(rename_all = "snake_case")]
1432pub enum ProviderCompactionStrategy {
1433 Dedicated,
1438 Inline,
1444}
1445
1446impl Default for ProviderCapabilities {
1447 fn default() -> Self {
1448 Self {
1449 supports_tools: true,
1450 supports_streaming: true,
1451 supports_reasoning: false,
1452 supports_interleaved_reasoning: false,
1453 supports_images: false,
1454 supports_documents: false,
1455 supports_prompt_cache: false,
1456 supports_compaction: false,
1457 compaction_strategy: None,
1458 supports_tool_result_media: false,
1459 requires_non_empty_assistant_content: false,
1460 tool_call_id_policy: ToolCallIdPolicy::ProviderSupplied,
1461 max_input_tokens: 0,
1462 max_output_tokens: 0,
1463 }
1464 }
1465}
1466
1467#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1468#[serde(rename_all = "snake_case")]
1469pub enum ToolCallIdPolicy {
1471 ProviderSupplied,
1473 RuntimeSynthesized,
1475 StableReplayNormalized,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1480pub struct ResolvedModel {
1482 pub role: ModelRole,
1483 pub id: ModelId,
1484 pub provider: ProviderName,
1485 pub provider_kind: ProviderKind,
1486 pub api_kind: ApiKind,
1487 pub model: String,
1488 pub max_input_tokens: Option<u32>,
1489 pub max_output_tokens: Option<u32>,
1490 pub reasoning: Option<ReasoningEffort>,
1491 #[serde(default)]
1492 pub tokens_per_minute: Option<u64>,
1493}
1494
1495#[derive(
1496 Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord,
1497)]
1498#[serde(rename_all = "snake_case")]
1499pub enum MessageSignal {
1501 VeryLow = 0,
1503 Low = 1,
1505 Normal = 2,
1507 High = 3,
1509 VeryHigh = 4,
1511 Anchor = 5,
1513}
1514
1515#[derive(
1516 Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord,
1517)]
1518#[serde(rename_all = "snake_case")]
1519pub enum PruneSignalThreshold {
1521 VeryLow,
1522 Low,
1523 #[default]
1524 Normal,
1525 High,
1526}
1527
1528#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1529pub struct CompactionResult {
1531 pub compacted_count: usize,
1533 pub summary: String,
1535}
1536
1537#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1538pub struct ContextPlan {
1540 pub prompt_segments: Vec<PromptSegment>,
1541 pub transcript_window: TranscriptWindow,
1542 #[serde(default)]
1543 pub compacted_prefix: Vec<Value>,
1544 pub file_views: Vec<FileViewSlice>,
1545 pub carried_summaries: Vec<SummarySlice>,
1546 pub elided_tool_results: Vec<ElisionMarker>,
1547 pub memory_items: Vec<MemoryItem>,
1548 pub tool_specs: Vec<ToolSpec>,
1549 pub observed_state: ObservedState,
1550 pub projected_input_tokens: u64,
1551 pub cache_boundary_hash: ContentHash,
1552 pub messages: Vec<Message>,
1553 pub estimated_tokens: u64,
1554 pub compaction: Option<CompactionResult>,
1557 #[serde(default, skip_serializing_if = "Option::is_none")]
1559 pub previous_response_id: Option<String>,
1560 #[serde(default)]
1562 pub new_messages_start: usize,
1563}
1564
1565#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1566pub struct AssembledPrompt {
1568 pub segments: Vec<PromptSegment>,
1569 pub transcript: Vec<Message>,
1570 pub ordered_segments: Vec<PromptSegment>,
1571 pub prefix_cache_key: String,
1572 pub rendered_prefix: String,
1573 pub rendered_transcript: String,
1574 pub rendered: String,
1575 #[serde(default)]
1581 pub cache_breakpoints: CacheBreakpoints,
1582 #[serde(default)]
1585 pub system_segment_count: usize,
1586 #[serde(default)]
1589 pub skill_segment_count: usize,
1590}
1591
1592impl AssembledPrompt {
1593 #[must_use]
1595 pub fn system_segments(&self) -> &[PromptSegment] {
1596 let end = self.system_segment_count.min(self.ordered_segments.len());
1597 &self.ordered_segments[..end]
1598 }
1599
1600 #[must_use]
1602 pub fn skill_segments(&self) -> &[PromptSegment] {
1603 let start = self.system_segment_count.min(self.ordered_segments.len());
1604 let end = (start + self.skill_segment_count).min(self.ordered_segments.len());
1605 &self.ordered_segments[start..end]
1606 }
1607
1608 #[must_use]
1612 pub fn append_segments(&self) -> &[PromptSegment] {
1613 let start =
1614 (self.system_segment_count + self.skill_segment_count).min(self.ordered_segments.len());
1615 &self.ordered_segments[start..]
1616 }
1617}
1618
1619#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1620pub struct ProviderRequest {
1622 pub session_id: SessionId,
1623 pub turn_id: TurnId,
1624 pub model: ResolvedModel,
1625 pub prompt: AssembledPrompt,
1626 #[serde(default)]
1627 pub compacted_prefix: Vec<Value>,
1628 pub messages: Vec<Message>,
1629 pub tools: Vec<ToolSpec>,
1630 #[serde(default, skip_serializing_if = "Option::is_none")]
1634 pub previous_response_id: Option<String>,
1635 #[serde(default)]
1638 pub new_messages_start: usize,
1639}
1640
1641#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1642pub struct ProviderCompactionRequest {
1644 pub session_id: SessionId,
1645 pub model: ResolvedModel,
1646 #[serde(default)]
1647 pub compacted_prefix: Vec<Value>,
1648 pub messages: Vec<Message>,
1649 pub tools: Vec<ToolSpec>,
1650 pub instructions: String,
1651}
1652
1653#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1654pub struct ProviderCompactionResponse {
1656 pub output: Vec<Value>,
1657 pub usage: Usage,
1658}
1659
1660#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1661pub struct SubagentResult {
1663 pub session_id: SessionId,
1664 pub output: String,
1665 pub usage: Usage,
1666}
1667
1668#[cfg(test)]
1669mod tests {
1670 use bytes::Bytes;
1671
1672 use super::*;
1673
1674 #[test]
1675 fn message_roundtrip() {
1676 let message = Message::User(UserMessage::text("hello"));
1677 let encoded = serde_json::to_string(&message).expect("serialize message");
1678 let decoded: Message = serde_json::from_str(&encoded).expect("deserialize message");
1679 assert_eq!(decoded, message);
1680 }
1681
1682 #[test]
1683 fn session_event_roundtrip() {
1684 let event = SessionEvent::new_committed(
1685 SessionId::new(),
1686 1,
1687 Delivery::Lossless,
1688 SessionEventPayload::TurnCompleted {
1689 turn_id: TurnId::new(),
1690 usage: Usage {
1691 input_tokens: 10,
1692 output_tokens: 5,
1693 cache_creation_input_tokens: 0,
1694 cache_read_input_tokens: 0,
1695 },
1696 },
1697 );
1698 let encoded = serde_json::to_string(&event).expect("serialize event");
1699 let decoded: SessionEvent = serde_json::from_str(&encoded).expect("deserialize event");
1700 assert_eq!(decoded, event);
1701 }
1702
1703 #[test]
1704 fn pending_event_into_committed_preserves_fields() {
1705 let session_id = SessionId::from("session-42");
1706 let payload = SessionEventPayload::ContextCompacted {
1707 summary: "summary".to_owned(),
1708 };
1709 let pending = PendingEvent::new(session_id.clone(), Delivery::Lossless, payload.clone());
1710
1711 let committed = pending.clone().into_committed(7);
1712
1713 assert_eq!(committed.session_id, session_id);
1714 assert_eq!(committed.sequence(), 7);
1715 assert_eq!(committed.delivery, Delivery::Lossless);
1716 assert_eq!(committed.payload, payload);
1717
1718 let encoded = serde_json::to_string(&pending).expect("serialize pending");
1721 assert!(!encoded.contains("sequence"));
1722 }
1723
1724 #[test]
1725 fn turn_roundtrip_preserves_model_overrides() {
1726 let turn = Turn::user("hello")
1727 .with_default_model("default")
1728 .with_subagent_model("subagent");
1729
1730 let encoded = serde_json::to_string(&turn).expect("serialize turn");
1731 let decoded: Turn = serde_json::from_str(&encoded).expect("deserialize turn");
1732
1733 assert_eq!(decoded, turn);
1734 }
1735
1736 #[test]
1737 fn compacted_context_serializes_as_existing_prefix_array() {
1738 let context = CompactedContext::new(vec![
1739 serde_json::json!({"type": "reasoning", "encrypted_content": "summary"}),
1740 ]);
1741
1742 let encoded = serde_json::to_string(&context).expect("serialize compacted context");
1743 assert!(encoded.starts_with('['));
1744
1745 let decoded: CompactedContext =
1746 serde_json::from_str(&encoded).expect("deserialize compacted context");
1747 assert_eq!(decoded, context);
1748
1749 let state: SessionState = serde_json::from_value(serde_json::json!({
1750 "messages": [],
1751 "compacted_prefix": [
1752 {"type": "reasoning", "encrypted_content": "summary"}
1753 ],
1754 "file_view_cache": {},
1755 "appended_prompt_segments": [],
1756 "pending_tool_calls": {},
1757 "usage_so_far": {
1758 "input_tokens": 0,
1759 "output_tokens": 0,
1760 "cache_creation_input_tokens": 0,
1761 "cache_read_input_tokens": 0
1762 },
1763 "summaries": [],
1764 "lineage": [],
1765 "fired_hook_ids": [],
1766 "pending_session_start_source": null,
1767 "pending_warning_messages": [],
1768 "messages_seen_by_provider": 0
1769 }))
1770 .expect("deserialize existing session state");
1771 assert_eq!(state.compacted_prefix.len(), 1);
1772 }
1773
1774 #[test]
1775 fn compaction_window_preserves_latest_assistant_response_block() {
1776 let messages = vec![
1777 Message::User(UserMessage::text("first")),
1778 assistant_text("answer"),
1779 Message::User(UserMessage::text("follow up")),
1780 ];
1781
1782 let window = CompactionWindow::preserve_latest_assistant_response_block(&messages);
1783
1784 assert_eq!(window.eligible_messages.len(), 1);
1785 assert_eq!(window.preserved_messages.len(), 2);
1786 assert!(window.reserved_response_block);
1787 }
1788
1789 #[test]
1790 fn compaction_window_preserves_through_latest_user() {
1791 let messages = vec![
1792 Message::User(UserMessage::text("first")),
1793 assistant_text("answer"),
1794 Message::User(UserMessage::text("follow up")),
1795 assistant_text("tail"),
1796 ];
1797
1798 let window = CompactionWindow::preserve_through_latest_user(&messages);
1799
1800 assert_eq!(window.preserved_messages.len(), 3);
1801 assert!(matches!(
1802 window.preserved_messages.last(),
1803 Some(Message::User(_))
1804 ));
1805 assert_eq!(window.eligible_messages.len(), 1);
1806 assert!(!window.reserved_response_block);
1807 }
1808
1809 #[test]
1810 fn user_message_with_media_roundtrips() {
1811 let message = Message::User(UserMessage {
1812 id: MessageId::new(),
1813 created_at: Utc::now(),
1814 parts: vec![
1815 UserPart::Text {
1816 text: "hello".to_owned(),
1817 },
1818 UserPart::Image {
1819 media_type: "image/png".to_owned(),
1820 data: Bytes::from_static(b"png"),
1821 },
1822 UserPart::Document {
1823 media_type: "application/pdf".to_owned(),
1824 data: Bytes::from_static(b"pdf"),
1825 },
1826 ],
1827 });
1828
1829 let encoded = serde_json::to_string(&message).expect("serialize message");
1830 let decoded: Message = serde_json::from_str(&encoded).expect("deserialize message");
1831 assert_eq!(decoded, message);
1832 }
1833
1834 #[test]
1835 fn stream_event_with_signature_roundtrips() {
1836 let event = StreamEvent::ThinkingEnd {
1837 id: BlockId::new(),
1838 signature: Some("sig-123".to_owned()),
1839 };
1840
1841 let encoded = serde_json::to_string(&event).expect("serialize event");
1842 let decoded: StreamEvent = serde_json::from_str(&encoded).expect("deserialize event");
1843 assert_eq!(decoded, event);
1844 }
1845
1846 fn assistant_text(text: &str) -> Message {
1847 Message::Assistant(AssistantMessage {
1848 id: MessageId::new(),
1849 created_at: Utc::now(),
1850 parts: vec![AssistantPart::Text {
1851 text: text.to_owned(),
1852 }],
1853 stop_reason: None,
1854 usage: None,
1855 replay_meta: ReplayMeta::default(),
1856 })
1857 }
1858}