1use std::fmt;
4use std::path::PathBuf;
5
6use bytes::Bytes;
7use chrono::{DateTime, Utc};
8use indexmap::IndexMap;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use thiserror::Error;
13use uuid::Uuid;
14
15pub type SharedStr = String;
17
18pub const DEFAULT_TEMPERATURE: f32 = 0.7;
22pub type MediaType = String;
23pub type ReplaySignature = String;
24pub type ContentHash = String;
25pub type Timestamp = DateTime<Utc>;
26
27macro_rules! id_type {
28 ($name:ident) => {
29 #[derive(
30 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
31 )]
32 pub struct $name(pub String);
33
34 impl $name {
35 #[must_use]
36 pub fn new() -> Self {
37 Self(Uuid::new_v4().to_string())
38 }
39 }
40
41 impl Default for $name {
42 fn default() -> Self {
43 Self::new()
44 }
45 }
46
47 impl fmt::Display for $name {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(&self.0)
50 }
51 }
52
53 impl From<&str> for $name {
54 fn from(value: &str) -> Self {
55 Self(value.to_owned())
56 }
57 }
58
59 impl From<String> for $name {
60 fn from(value: String) -> Self {
61 Self(value)
62 }
63 }
64 };
65}
66
67macro_rules! string_wrapper {
68 ($name:ident) => {
69 #[derive(
70 Debug,
71 Clone,
72 PartialEq,
73 Eq,
74 PartialOrd,
75 Ord,
76 Hash,
77 Default,
78 Serialize,
79 Deserialize,
80 JsonSchema,
81 )]
82 pub struct $name(pub String);
83
84 impl fmt::Display for $name {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(&self.0)
87 }
88 }
89
90 impl From<&str> for $name {
91 fn from(value: &str) -> Self {
92 Self(value.to_owned())
93 }
94 }
95
96 impl From<String> for $name {
97 fn from(value: String) -> Self {
98 Self(value)
99 }
100 }
101 };
102}
103
104id_type!(MessageId);
105id_type!(BlockId);
106id_type!(ToolCallId);
107id_type!(PromptId);
108id_type!(PromptSegmentId);
109id_type!(SessionId);
110id_type!(TurnId);
111id_type!(SkillId);
112id_type!(PluginId);
113id_type!(AgentId);
114
115string_wrapper!(Revision);
116string_wrapper!(ModelId);
117string_wrapper!(ToolName);
118string_wrapper!(ToolAlias);
119string_wrapper!(SkillName);
120string_wrapper!(AgentName);
121string_wrapper!(ProviderName);
122
123#[derive(
124 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
125)]
126#[serde(rename_all = "snake_case")]
127pub enum ModelRole {
128 Default,
129 Plan,
130 Subagent,
131 Small,
132}
133
134impl ModelRole {
135 #[must_use]
136 pub const fn default_role() -> Self {
137 Self::Default
138 }
139
140 #[must_use]
141 pub const fn plan() -> Self {
142 Self::Plan
143 }
144
145 #[must_use]
146 pub const fn subagent() -> Self {
147 Self::Subagent
148 }
149
150 #[must_use]
151 pub const fn small() -> Self {
152 Self::Small
153 }
154
155 #[must_use]
156 pub const fn as_str(&self) -> &'static str {
157 match self {
158 Self::Default => "default",
159 Self::Plan => "plan",
160 Self::Subagent => "subagent",
161 Self::Small => "small",
162 }
163 }
164}
165
166impl Default for ModelRole {
167 fn default() -> Self {
168 Self::default_role()
169 }
170}
171
172impl std::str::FromStr for ModelRole {
173 type Err = String;
174
175 fn from_str(value: &str) -> Result<Self, Self::Err> {
176 match value {
177 "default" => Ok(Self::Default),
178 "plan" => Ok(Self::Plan),
179 "subagent" => Ok(Self::Subagent),
180 "small" => Ok(Self::Small),
181 other => Err(format!(
182 "unknown ModelRole '{other}'; expected one of: default, plan, subagent, small"
183 )),
184 }
185 }
186}
187
188impl fmt::Display for ModelRole {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 f.write_str(self.as_str())
191 }
192}
193
194#[derive(
195 Debug,
196 Clone,
197 Copy,
198 Default,
199 PartialEq,
200 Eq,
201 PartialOrd,
202 Ord,
203 Hash,
204 Serialize,
205 Deserialize,
206 JsonSchema,
207)]
208#[serde(rename_all = "snake_case")]
209pub enum SubagentEventForwarding {
210 #[default]
211 Off,
212 All,
213}
214
215impl SubagentEventForwarding {
216 #[must_use]
217 pub const fn is_enabled(self) -> bool {
218 matches!(self, Self::All)
219 }
220}
221
222#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
223#[serde(rename_all = "snake_case")]
224pub enum ProviderKind {
225 Anthropic,
226 OpenAi,
227 OpenRouter,
228 Fake,
229}
230
231#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
232#[serde(rename_all = "snake_case")]
233pub enum ApiKind {
234 AnthropicMessages,
235 OpenAiResponses,
236 OpenAiChat,
237 Fake,
238}
239
240#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
241#[serde(rename_all = "snake_case")]
242pub enum ReasoningEffort {
243 Low,
244 Medium,
245 High,
246 Xhigh,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
250pub struct Usage {
251 pub input_tokens: u64,
252 pub output_tokens: u64,
253 pub cache_creation_input_tokens: u64,
254 pub cache_read_input_tokens: u64,
255}
256
257#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
258#[serde(rename_all = "snake_case")]
259pub enum StopReason {
260 EndTurn,
261 ToolUse,
262 Interrupted,
263 MaxTokens,
264 Error,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
268pub struct ReplayMeta {
269 pub provider_name: Option<ProviderName>,
270 pub model: Option<ModelId>,
271}
272
273#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
274#[serde(rename_all = "snake_case")]
275pub enum HookWarningSeverity {
276 #[default]
277 Warning,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
281pub struct HookWarning {
282 pub severity: HookWarningSeverity,
283 pub category: SharedStr,
284 pub plugin_id: Option<PluginId>,
285 pub plugin_name: Option<SharedStr>,
286 pub source_path: Option<PathBuf>,
287 pub message: SharedStr,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
291pub struct SystemMessage {
292 pub id: MessageId,
293 pub created_at: Timestamp,
294 pub text: SharedStr,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
298pub struct UserMessage {
299 pub id: MessageId,
300 pub created_at: Timestamp,
301 pub parts: Vec<UserPart>,
302}
303
304impl UserMessage {
305 #[must_use]
306 pub fn text(text: impl Into<String>) -> Self {
307 Self {
308 id: MessageId::new(),
309 created_at: Utc::now(),
310 parts: vec![UserPart::Text { text: text.into() }],
311 }
312 }
313
314 #[must_use]
315 pub fn plain_text(&self) -> String {
316 self.parts
317 .iter()
318 .filter_map(|part| match part {
319 UserPart::Text { text } => Some(text.as_str()),
320 UserPart::Image { .. } | UserPart::Document { .. } => None,
321 })
322 .collect::<Vec<_>>()
323 .join("\n")
324 }
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
328#[serde(tag = "kind", rename_all = "snake_case")]
329pub enum UserPart {
330 Text {
331 text: SharedStr,
332 },
333 Image {
334 media_type: MediaType,
335 #[schemars(with = "Vec<u8>")]
336 data: Bytes,
337 },
338 Document {
339 media_type: MediaType,
340 #[schemars(with = "Vec<u8>")]
341 data: Bytes,
342 },
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
346pub struct AssistantMessage {
347 pub id: MessageId,
348 pub created_at: Timestamp,
349 pub parts: Vec<AssistantPart>,
350 pub stop_reason: Option<StopReason>,
351 pub usage: Option<Usage>,
352 pub replay_meta: ReplayMeta,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
356#[serde(tag = "kind", rename_all = "snake_case")]
357pub enum AssistantPart {
358 Text { text: SharedStr },
359 Thinking(ThinkingBlock),
360 ToolCall(ToolCall),
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
364pub struct ThinkingBlock {
365 pub text: SharedStr,
366 pub signature: Option<ReplaySignature>,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
370pub struct ToolCall {
371 pub id: ToolCallId,
372 pub name: ToolName,
373 pub arguments: Value,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
377pub struct ToolResultMessage {
378 pub id: MessageId,
379 pub call_id: ToolCallId,
380 pub content: ToolResult,
381 pub error: Option<ToolError>,
382 pub created_at: Timestamp,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
386#[serde(tag = "role", rename_all = "snake_case")]
387pub enum Message {
388 System(SystemMessage),
389 User(UserMessage),
390 Assistant(AssistantMessage),
391 Tool(ToolResultMessage),
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
395#[serde(tag = "kind", rename_all = "snake_case")]
396pub enum StreamEvent {
397 MessageStart {
398 id: MessageId,
399 },
400 TextStart {
401 id: BlockId,
402 },
403 TextDelta {
404 id: BlockId,
405 delta: SharedStr,
406 },
407 TextEnd {
408 id: BlockId,
409 },
410 ThinkingStart {
411 id: BlockId,
412 },
413 ThinkingDelta {
414 id: BlockId,
415 delta: SharedStr,
416 },
417 ThinkingEnd {
418 id: BlockId,
419 signature: Option<ReplaySignature>,
420 },
421 ToolCallStart {
422 id: BlockId,
423 tool_call_id: ToolCallId,
424 name: ToolName,
425 },
426 ToolArgsDelta {
427 id: BlockId,
428 delta: SharedStr,
429 },
430 ToolCallEnd {
431 id: BlockId,
432 },
433 UsageUpdate {
434 usage: Usage,
435 },
436 MessageEnd {
437 id: MessageId,
438 stop_reason: StopReason,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
441 response_id: Option<String>,
442 },
443 ProviderWarning {
444 message: SharedStr,
445 },
446 Error {
447 error: ProviderError,
448 },
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
452pub struct Turn {
453 pub id: TurnId,
454 pub user_message: UserMessage,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub default_model: Option<ModelId>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub subagent_model: Option<ModelId>,
459}
460
461impl Turn {
462 #[must_use]
463 pub fn user(text: impl Into<String>) -> Self {
464 Self {
465 id: TurnId::new(),
466 user_message: UserMessage::text(text),
467 default_model: None,
468 subagent_model: None,
469 }
470 }
471
472 #[must_use]
473 pub fn with_default_model(mut self, model: impl Into<ModelId>) -> Self {
474 self.default_model = Some(model.into());
475 self
476 }
477
478 #[must_use]
479 pub fn with_subagent_model(mut self, model: impl Into<ModelId>) -> Self {
480 self.subagent_model = Some(model.into());
481 self
482 }
483}
484
485#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
486#[serde(rename_all = "snake_case")]
487pub enum SubagentState {
488 Running,
489 Completed,
490 Failed,
491 Cancelled,
492 Closed,
493}
494
495impl SubagentState {
496 #[must_use]
497 pub fn is_terminal(self) -> bool {
498 matches!(
499 self,
500 Self::Completed | Self::Failed | Self::Cancelled | Self::Closed
501 )
502 }
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
506pub struct SpawnSubagentRequest {
507 pub message: String,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
509 pub agent_type: Option<AgentName>,
510 #[serde(default)]
511 pub fork_context: bool,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub model: Option<ModelId>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
517pub struct SendSubagentInputRequest {
518 pub target: AgentId,
519 pub message: String,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
523pub struct WaitSubagentRequest {
524 pub targets: Vec<AgentId>,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub timeout_ms: Option<u64>,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
530pub struct CloseSubagentRequest {
531 pub target: AgentId,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
535pub struct SubagentStatus {
536 pub agent_id: AgentId,
537 pub session_id: SessionId,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub agent_type: Option<AgentName>,
540 pub task: String,
541 pub state: SubagentState,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub last_message: Option<String>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub usage: Option<Usage>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub error: Option<String>,
548}
549
550impl SubagentStatus {
551 #[must_use]
552 pub fn is_terminal(&self) -> bool {
553 self.state.is_terminal()
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
558pub struct WaitSubagentResponse {
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub status: Option<SubagentStatus>,
561 pub timed_out: bool,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
565pub struct CloseSubagentResponse {
566 pub previous_status: SubagentStatus,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
570pub struct SubagentSpecWire {
571 pub role: Option<ModelRole>,
572 pub task: String,
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
576#[serde(tag = "kind", rename_all = "snake_case")]
577pub enum SessionCommand {
578 SubmitTurn { turn: Turn },
579 InterruptTurn,
580 AppendSystemPrompt { id: PromptId, text: SharedStr },
581 SetModelRole { role: ModelRole },
582 SetModel { model: ModelId },
583 SpawnSubagent { spec: SubagentSpecWire },
584 ReloadResources,
585 Shutdown,
586}
587
588#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
589pub enum Delivery {
590 Lossless,
591 BestEffort,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
595pub struct DeltaItem {
596 pub text: String,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
600pub struct ToolExecutionOutcome {
601 pub call: ToolCall,
602 pub result: Result<ToolResult, ToolError>,
603}
604
605#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
606#[serde(rename_all = "snake_case")]
607pub enum HookHandlerType {
608 Command,
609 Http,
610 Prompt,
611 Agent,
612 Callback,
613 Function,
614}
615
616#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
617#[serde(rename_all = "snake_case")]
618pub enum HookRunStatus {
619 Running,
620 Completed,
621 Failed,
622 Blocked,
623 Stopped,
624 Cancelled,
625}
626
627#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
628#[serde(rename_all = "snake_case")]
629pub enum HookOutputKind {
630 Warning,
631 Stop,
632 Feedback,
633 Context,
634 Error,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
638pub struct HookOutputEntry {
639 pub kind: HookOutputKind,
640 pub text: String,
641}
642
643#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
644#[serde(rename_all = "snake_case")]
645pub enum HookSessionStartSource {
646 Startup,
647 Resume,
648 Clear,
649 Compact,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
653pub struct HookRunSummary {
654 pub run_id: String,
655 pub event_name: String,
656 pub handler_type: HookHandlerType,
657 pub plugin_id: PluginId,
658 pub plugin_root: PathBuf,
659 pub status: HookRunStatus,
660 pub status_message: Option<String>,
661 pub started_at: Timestamp,
662 pub completed_at: Option<Timestamp>,
663 pub duration_ms: Option<u64>,
664 pub entries: Vec<HookOutputEntry>,
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
668#[serde(tag = "kind", rename_all = "snake_case")]
669pub enum SessionEventPayload {
670 SessionStarted,
671 Warning {
672 message: String,
673 },
674 TurnStarted {
675 turn_id: TurnId,
676 },
677 MessageItem {
678 message: Message,
679 },
680 DeltaItem {
681 delta: DeltaItem,
682 },
683 ToolExecutionStarted {
684 call: ToolCall,
685 },
686 ToolOutput {
687 call_id: ToolCallId,
688 tool_name: ToolName,
689 chunk: SharedStr,
690 },
691 HookStarted {
692 run: HookRunSummary,
693 },
694 HookCompleted {
695 run: HookRunSummary,
696 },
697 ToolExecutionCompleted {
698 outcome: ToolExecutionOutcome,
699 },
700 ApprovalRequested {
701 tool_name: ToolName,
702 reason: String,
703 },
704 ContextCompacted {
705 summary: String,
706 },
707 TurnCompleted {
708 turn_id: TurnId,
709 usage: Usage,
710 },
711 TurnFailed {
712 turn_id: TurnId,
713 error: String,
714 #[serde(default)]
716 cancelled: bool,
717 #[serde(default)]
721 retryable: bool,
722 },
723 Lagged {
724 dropped_events: u64,
725 },
726 SessionShutdownComplete,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
735pub struct SessionEvent {
736 pub session_id: SessionId,
737 pub(crate) sequence: u64,
738 pub delivery: Delivery,
739 pub payload: SessionEventPayload,
740}
741
742impl SessionEvent {
743 #[must_use]
747 pub fn new_committed(
748 session_id: SessionId,
749 sequence: u64,
750 delivery: Delivery,
751 payload: SessionEventPayload,
752 ) -> Self {
753 Self {
754 session_id,
755 sequence,
756 delivery,
757 payload,
758 }
759 }
760
761 #[must_use]
762 pub fn sequence(&self) -> u64 {
763 self.sequence
764 }
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
772pub struct PendingEvent {
773 pub session_id: SessionId,
774 pub delivery: Delivery,
775 pub payload: SessionEventPayload,
776}
777
778impl PendingEvent {
779 #[must_use]
780 pub fn new(session_id: SessionId, delivery: Delivery, payload: SessionEventPayload) -> Self {
781 Self {
782 session_id,
783 delivery,
784 payload,
785 }
786 }
787
788 #[must_use]
789 pub fn into_committed(self, sequence: u64) -> SessionEvent {
790 SessionEvent {
791 session_id: self.session_id,
792 sequence,
793 delivery: self.delivery,
794 payload: self.payload,
795 }
796 }
797}
798
799#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
800pub enum ToolConcurrency {
801 Exclusive,
802 ReadOnly,
803 ParallelSafe,
804}
805
806#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
807pub struct ToolCapabilities {
808 pub mutating: bool,
809 pub requires_approval: bool,
810 pub cancellable: bool,
811 pub long_running: bool,
812}
813
814#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
815pub struct ToolSpec {
816 pub name: ToolName,
817 pub description: SharedStr,
818 pub input_schema: Value,
819 pub concurrency: ToolConcurrency,
820 pub capabilities: ToolCapabilities,
821 pub provider_aliases: IndexMap<ProviderKind, ToolAlias>,
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
825#[serde(tag = "kind", rename_all = "snake_case")]
826pub enum ToolResult {
827 Empty,
828 Text { text: String },
829 Json { value: Value },
830}
831
832#[derive(Debug, Clone, Error, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
833#[error("{message}")]
834pub struct ToolError {
835 pub message: String,
836}
837
838impl ToolError {
839 #[must_use]
840 pub fn new(message: impl Into<String>) -> Self {
841 Self {
842 message: message.into(),
843 }
844 }
845}
846
847#[derive(Debug, Clone, Error, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
848#[error("{message}")]
849pub struct ProviderError {
850 pub message: String,
851 pub retryable: bool,
852}
853
854impl ProviderError {
855 pub const CANCELLED_MESSAGE: &str = "failed to execute provider request: request cancelled";
859
860 #[must_use]
861 pub fn new(message: impl Into<String>, retryable: bool) -> Self {
862 Self {
863 message: message.into(),
864 retryable,
865 }
866 }
867
868 #[must_use]
872 pub fn cancelled() -> Self {
873 Self {
874 message: Self::CANCELLED_MESSAGE.to_owned(),
875 retryable: false,
876 }
877 }
878
879 #[must_use]
880 pub fn is_cancelled(&self) -> bool {
881 self.message == Self::CANCELLED_MESSAGE
882 }
883}
884
885#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
886pub struct PromptSegment {
887 pub id: PromptSegmentId,
888 pub text: SharedStr,
889 pub volatility: Volatility,
890 pub cache_scope: CacheScope,
891 pub content_hash: ContentHash,
892 #[serde(default)]
897 pub kind: PromptSegmentKind,
898}
899
900#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
901#[serde(rename_all = "snake_case")]
902pub enum PromptSegmentKind {
903 #[default]
904 System,
905 Skill,
906 Append,
907}
908
909#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
910pub enum Volatility {
911 Static,
912 SessionStable,
913 TurnDynamic,
914 AlwaysDynamic,
915}
916
917#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
918pub enum CacheScope {
919 PrefixCacheable,
920 Dynamic,
921}
922
923#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
931pub struct CacheBreakpoints {
932 pub after_system: bool,
933 pub after_tools: bool,
934 pub after_skills: bool,
935 pub after_user_prompt: bool,
936}
937
938impl CacheBreakpoints {
939 #[must_use]
943 pub fn all() -> Self {
944 Self {
945 after_system: true,
946 after_tools: true,
947 after_skills: true,
948 after_user_prompt: true,
949 }
950 }
951
952 #[must_use]
953 pub fn count_active(&self) -> usize {
954 usize::from(self.after_system)
955 + usize::from(self.after_tools)
956 + usize::from(self.after_skills)
957 + usize::from(self.after_user_prompt)
958 }
959}
960
961pub type FileViewCache = IndexMap<PathBuf, FileViewEntry>;
962
963#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
964pub struct FileViewEntry {
965 pub path: PathBuf,
966 pub full_hash: ContentHash,
967 pub mtime: Timestamp,
968 pub size: u64,
969 pub viewed_ranges: Vec<ViewedRange>,
970 pub last_shown_turn: TurnId,
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
974pub struct ViewedRange {
975 pub start_line: u32,
976 pub end_line: u32,
977 pub line_anchors: Vec<LineAnchor>,
978}
979
980#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
981pub struct LineAnchor {
982 pub line: u32,
983 pub anchor: [u8; 3],
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
987pub struct PendingToolCall {
988 pub call: ToolCall,
989 pub submitted_at: Timestamp,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
993pub struct SummarySlice {
994 pub id: String,
995 pub text: String,
996}
997
998#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
999pub struct TranscriptWindow {
1000 pub messages: Vec<Message>,
1001 pub elided_message_count: u64,
1002}
1003
1004#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1005#[serde(transparent)]
1006pub struct CompactedContext(pub Vec<Value>);
1007
1008impl CompactedContext {
1009 #[must_use]
1010 pub fn new(items: Vec<Value>) -> Self {
1011 Self(items)
1012 }
1013
1014 #[must_use]
1015 pub fn items(&self) -> &[Value] {
1016 &self.0
1017 }
1018
1019 #[must_use]
1020 pub fn into_items(self) -> Vec<Value> {
1021 self.0
1022 }
1023
1024 #[must_use]
1025 pub fn is_empty(&self) -> bool {
1026 self.0.is_empty()
1027 }
1028
1029 #[must_use]
1030 pub fn len(&self) -> usize {
1031 self.0.len()
1032 }
1033}
1034
1035impl From<Vec<Value>> for CompactedContext {
1036 fn from(value: Vec<Value>) -> Self {
1037 Self(value)
1038 }
1039}
1040
1041impl AsRef<[Value]> for CompactedContext {
1042 fn as_ref(&self) -> &[Value] {
1043 self.items()
1044 }
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1048pub struct CompactionWindow {
1049 pub eligible_messages: Vec<Message>,
1050 pub preserved_messages: Vec<Message>,
1051 pub reserved_response_block: bool,
1052}
1053
1054impl CompactionWindow {
1055 #[must_use]
1060 pub fn preserve_latest_assistant_response_block(messages: &[Message]) -> Self {
1061 let Some(last_assistant_index) = messages
1062 .iter()
1063 .rposition(|message| matches!(message, Message::Assistant(_)))
1064 else {
1065 return Self {
1066 eligible_messages: messages.to_vec(),
1067 preserved_messages: Vec::new(),
1068 reserved_response_block: false,
1069 };
1070 };
1071
1072 Self {
1073 eligible_messages: messages[..last_assistant_index].to_vec(),
1074 preserved_messages: messages[last_assistant_index..].to_vec(),
1075 reserved_response_block: true,
1076 }
1077 }
1078
1079 #[must_use]
1084 pub fn preserve_through_latest_user(messages: &[Message]) -> Self {
1085 let Some(last_user_index) = messages
1086 .iter()
1087 .rposition(|message| matches!(message, Message::User(_)))
1088 else {
1089 return Self {
1090 eligible_messages: Vec::new(),
1091 preserved_messages: messages.to_vec(),
1092 reserved_response_block: false,
1093 };
1094 };
1095 let pivot = last_user_index + 1;
1096 Self {
1097 eligible_messages: messages[pivot..].to_vec(),
1098 preserved_messages: messages[..pivot].to_vec(),
1099 reserved_response_block: false,
1100 }
1101 }
1102}
1103
1104#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1105pub struct FileViewSlice {
1106 pub path: PathBuf,
1107 pub full_hash: ContentHash,
1108 pub viewed_ranges: Vec<ViewedRange>,
1109 pub last_shown_turn: TurnId,
1110}
1111
1112#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1113pub struct ElisionMarker {
1114 pub kind: String,
1115 pub count: u64,
1116}
1117
1118#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1119pub struct MemoryItem {
1120 pub key: String,
1121 pub text: String,
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1125pub struct SubagentRef {
1126 pub session_id: SessionId,
1127 pub task: String,
1128}
1129
1130#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1131pub struct SessionBlueprint {
1132 pub session_id: SessionId,
1133 pub parent_session_id: Option<SessionId>,
1134 pub default_model: ModelId,
1135 pub subagent_model: ModelId,
1136 #[serde(default)]
1137 pub subagent_event_forwarding: SubagentEventForwarding,
1138 pub snapshot_revision: Revision,
1139 pub working_dir: PathBuf,
1140 pub system_prompt_seed: Vec<PromptSegment>,
1141 pub max_turns: Option<u32>,
1142 pub subagent_depth: u32,
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1146pub struct SessionState {
1147 pub messages: Vec<Message>,
1148 #[serde(default)]
1149 pub compacted_prefix: Vec<Value>,
1150 pub file_view_cache: FileViewCache,
1151 pub appended_prompt_segments: Vec<PromptSegment>,
1152 pub pending_tool_calls: IndexMap<ToolCallId, PendingToolCall>,
1153 pub usage_so_far: Usage,
1154 pub summaries: Vec<SummarySlice>,
1155 pub lineage: Vec<SubagentRef>,
1156 pub fired_hook_ids: Vec<String>,
1157 pub pending_session_start_source: Option<HookSessionStartSource>,
1158 pub pending_warning_messages: Vec<HookWarning>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1162 pub last_response_id: Option<String>,
1163 #[serde(default)]
1166 pub messages_seen_by_provider: usize,
1167}
1168
1169#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1170pub struct ObservedState {
1171 pub cwd: PathBuf,
1172 pub git_branch: Option<String>,
1173 pub git_dirty: Option<bool>,
1174 pub now_utc: Timestamp,
1175 pub env_facts: IndexMap<String, String>,
1176}
1177
1178#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1179pub struct InstructionFile {
1180 pub path: PathBuf,
1181 pub body: String,
1182}
1183
1184#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1185pub struct SkillDef {
1186 pub id: SkillId,
1187 pub name: String,
1188 pub description: String,
1189 pub body: String,
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1193pub struct AgentDef {
1194 pub id: AgentId,
1195 pub name: String,
1196 pub prompt: String,
1197}
1198
1199#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1200pub struct PluginManifest {
1201 pub name: String,
1202 pub version: String,
1203 pub skills: Vec<String>,
1204 pub agents: Vec<String>,
1205 pub hooks: Option<String>,
1206 pub mcp_servers: Option<String>,
1207 pub lsp_servers: Option<String>,
1208 pub allowed_http_hosts: Vec<String>,
1209 pub allowed_env_vars: Vec<String>,
1210}
1211
1212#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1213pub struct PromptRegistry {
1214 pub prompts: IndexMap<String, Vec<PromptSegment>>,
1215}
1216
1217#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1218pub struct ResourceSnapshot {
1219 pub revision: Revision,
1220 pub tools: IndexMap<ToolName, ToolSpec>,
1221 pub skills: IndexMap<SkillName, SkillDef>,
1222 pub agents: IndexMap<AgentName, AgentDef>,
1223 pub prompts: PromptRegistry,
1224 pub plugins: IndexMap<PluginId, PluginManifest>,
1225 pub instruction_files: Vec<InstructionFile>,
1226}
1227
1228impl ResourceSnapshot {
1229 #[must_use]
1230 pub fn empty() -> Self {
1231 Self {
1232 revision: Revision("empty".to_owned()),
1233 tools: IndexMap::new(),
1234 skills: IndexMap::new(),
1235 agents: IndexMap::new(),
1236 prompts: PromptRegistry::default(),
1237 plugins: IndexMap::new(),
1238 instruction_files: Vec::new(),
1239 }
1240 }
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1244pub struct ProviderCapabilities {
1245 pub supports_tools: bool,
1246 pub supports_streaming: bool,
1247 pub supports_reasoning: bool,
1248 pub supports_interleaved_reasoning: bool,
1249 pub supports_images: bool,
1250 pub supports_documents: bool,
1251 pub supports_prompt_cache: bool,
1252 pub supports_compaction: bool,
1253 #[serde(default)]
1257 pub compaction_strategy: Option<ProviderCompactionStrategy>,
1258 pub supports_tool_result_media: bool,
1259 pub requires_non_empty_assistant_content: bool,
1260 pub tool_call_id_policy: ToolCallIdPolicy,
1261 pub max_input_tokens: u64,
1262 pub max_output_tokens: u64,
1263}
1264
1265#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1266#[serde(rename_all = "snake_case")]
1267pub enum ProviderCompactionStrategy {
1268 Dedicated,
1273 Inline,
1279}
1280
1281impl Default for ProviderCapabilities {
1282 fn default() -> Self {
1283 Self {
1284 supports_tools: true,
1285 supports_streaming: true,
1286 supports_reasoning: false,
1287 supports_interleaved_reasoning: false,
1288 supports_images: false,
1289 supports_documents: false,
1290 supports_prompt_cache: false,
1291 supports_compaction: false,
1292 compaction_strategy: None,
1293 supports_tool_result_media: false,
1294 requires_non_empty_assistant_content: false,
1295 tool_call_id_policy: ToolCallIdPolicy::ProviderSupplied,
1296 max_input_tokens: 0,
1297 max_output_tokens: 0,
1298 }
1299 }
1300}
1301
1302#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1303#[serde(rename_all = "snake_case")]
1304pub enum ToolCallIdPolicy {
1305 ProviderSupplied,
1306 RuntimeSynthesized,
1307 StableReplayNormalized,
1308}
1309
1310#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1311pub struct ResolvedModel {
1312 pub role: ModelRole,
1313 pub id: ModelId,
1314 pub provider: ProviderName,
1315 pub provider_kind: ProviderKind,
1316 pub api_kind: ApiKind,
1317 pub model: String,
1318 pub max_input_tokens: Option<u32>,
1319 pub max_output_tokens: Option<u32>,
1320 pub reasoning: Option<ReasoningEffort>,
1321 #[serde(default)]
1322 pub tokens_per_minute: Option<u64>,
1323}
1324
1325#[derive(
1326 Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord,
1327)]
1328#[serde(rename_all = "snake_case")]
1329pub enum MessageSignal {
1330 VeryLow = 0,
1332 Low = 1,
1334 Normal = 2,
1336 High = 3,
1338 VeryHigh = 4,
1340 Anchor = 5,
1342}
1343
1344#[derive(
1345 Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord,
1346)]
1347#[serde(rename_all = "snake_case")]
1348pub enum PruneSignalThreshold {
1349 VeryLow,
1350 Low,
1351 #[default]
1352 Normal,
1353 High,
1354}
1355
1356#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1357pub struct CompactionResult {
1358 pub compacted_count: usize,
1360 pub summary: String,
1362}
1363
1364#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1365pub struct ContextPlan {
1366 pub prompt_segments: Vec<PromptSegment>,
1367 pub transcript_window: TranscriptWindow,
1368 #[serde(default)]
1369 pub compacted_prefix: Vec<Value>,
1370 pub file_views: Vec<FileViewSlice>,
1371 pub carried_summaries: Vec<SummarySlice>,
1372 pub elided_tool_results: Vec<ElisionMarker>,
1373 pub memory_items: Vec<MemoryItem>,
1374 pub tool_specs: Vec<ToolSpec>,
1375 pub observed_state: ObservedState,
1376 pub projected_input_tokens: u64,
1377 pub cache_boundary_hash: ContentHash,
1378 pub messages: Vec<Message>,
1379 pub estimated_tokens: u64,
1380 pub compaction: Option<CompactionResult>,
1383 #[serde(default, skip_serializing_if = "Option::is_none")]
1385 pub previous_response_id: Option<String>,
1386 #[serde(default)]
1388 pub new_messages_start: usize,
1389}
1390
1391#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1392pub struct AssembledPrompt {
1393 pub segments: Vec<PromptSegment>,
1394 pub transcript: Vec<Message>,
1395 pub ordered_segments: Vec<PromptSegment>,
1396 pub prefix_cache_key: String,
1397 pub rendered_prefix: String,
1398 pub rendered_transcript: String,
1399 pub rendered: String,
1400 #[serde(default)]
1406 pub cache_breakpoints: CacheBreakpoints,
1407 #[serde(default)]
1410 pub system_segment_count: usize,
1411 #[serde(default)]
1414 pub skill_segment_count: usize,
1415}
1416
1417impl AssembledPrompt {
1418 #[must_use]
1420 pub fn system_segments(&self) -> &[PromptSegment] {
1421 let end = self.system_segment_count.min(self.ordered_segments.len());
1422 &self.ordered_segments[..end]
1423 }
1424
1425 #[must_use]
1427 pub fn skill_segments(&self) -> &[PromptSegment] {
1428 let start = self.system_segment_count.min(self.ordered_segments.len());
1429 let end = (start + self.skill_segment_count).min(self.ordered_segments.len());
1430 &self.ordered_segments[start..end]
1431 }
1432
1433 #[must_use]
1437 pub fn append_segments(&self) -> &[PromptSegment] {
1438 let start =
1439 (self.system_segment_count + self.skill_segment_count).min(self.ordered_segments.len());
1440 &self.ordered_segments[start..]
1441 }
1442}
1443
1444#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1445pub struct ProviderRequest {
1446 pub session_id: SessionId,
1447 pub turn_id: TurnId,
1448 pub model: ResolvedModel,
1449 pub prompt: AssembledPrompt,
1450 #[serde(default)]
1451 pub compacted_prefix: Vec<Value>,
1452 pub messages: Vec<Message>,
1453 pub tools: Vec<ToolSpec>,
1454 #[serde(default, skip_serializing_if = "Option::is_none")]
1458 pub previous_response_id: Option<String>,
1459 #[serde(default)]
1462 pub new_messages_start: usize,
1463}
1464
1465#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1466pub struct ProviderCompactionRequest {
1467 pub session_id: SessionId,
1468 pub model: ResolvedModel,
1469 #[serde(default)]
1470 pub compacted_prefix: Vec<Value>,
1471 pub messages: Vec<Message>,
1472 pub tools: Vec<ToolSpec>,
1473 pub instructions: String,
1474}
1475
1476#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1477pub struct ProviderCompactionResponse {
1478 pub output: Vec<Value>,
1479 pub usage: Usage,
1480}
1481
1482#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1483pub struct SubagentResult {
1484 pub session_id: SessionId,
1485 pub output: String,
1486 pub usage: Usage,
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use bytes::Bytes;
1492
1493 use super::*;
1494
1495 #[test]
1496 fn message_roundtrip() {
1497 let message = Message::User(UserMessage::text("hello"));
1498 let encoded = serde_json::to_string(&message).expect("serialize message");
1499 let decoded: Message = serde_json::from_str(&encoded).expect("deserialize message");
1500 assert_eq!(decoded, message);
1501 }
1502
1503 #[test]
1504 fn session_event_roundtrip() {
1505 let event = SessionEvent::new_committed(
1506 SessionId::new(),
1507 1,
1508 Delivery::Lossless,
1509 SessionEventPayload::TurnCompleted {
1510 turn_id: TurnId::new(),
1511 usage: Usage {
1512 input_tokens: 10,
1513 output_tokens: 5,
1514 cache_creation_input_tokens: 0,
1515 cache_read_input_tokens: 0,
1516 },
1517 },
1518 );
1519 let encoded = serde_json::to_string(&event).expect("serialize event");
1520 let decoded: SessionEvent = serde_json::from_str(&encoded).expect("deserialize event");
1521 assert_eq!(decoded, event);
1522 }
1523
1524 #[test]
1525 fn pending_event_into_committed_preserves_fields() {
1526 let session_id = SessionId::from("session-42");
1527 let payload = SessionEventPayload::ContextCompacted {
1528 summary: "summary".to_owned(),
1529 };
1530 let pending = PendingEvent::new(session_id.clone(), Delivery::Lossless, payload.clone());
1531
1532 let committed = pending.clone().into_committed(7);
1533
1534 assert_eq!(committed.session_id, session_id);
1535 assert_eq!(committed.sequence(), 7);
1536 assert_eq!(committed.delivery, Delivery::Lossless);
1537 assert_eq!(committed.payload, payload);
1538
1539 let encoded = serde_json::to_string(&pending).expect("serialize pending");
1542 assert!(!encoded.contains("sequence"));
1543 }
1544
1545 #[test]
1546 fn turn_roundtrip_preserves_model_overrides() {
1547 let turn = Turn::user("hello")
1548 .with_default_model("default")
1549 .with_subagent_model("subagent");
1550
1551 let encoded = serde_json::to_string(&turn).expect("serialize turn");
1552 let decoded: Turn = serde_json::from_str(&encoded).expect("deserialize turn");
1553
1554 assert_eq!(decoded, turn);
1555 }
1556
1557 #[test]
1558 fn compacted_context_serializes_as_existing_prefix_array() {
1559 let context = CompactedContext::new(vec![
1560 serde_json::json!({"type": "reasoning", "encrypted_content": "summary"}),
1561 ]);
1562
1563 let encoded = serde_json::to_string(&context).expect("serialize compacted context");
1564 assert!(encoded.starts_with('['));
1565
1566 let decoded: CompactedContext =
1567 serde_json::from_str(&encoded).expect("deserialize compacted context");
1568 assert_eq!(decoded, context);
1569
1570 let state: SessionState = serde_json::from_value(serde_json::json!({
1571 "messages": [],
1572 "compacted_prefix": [
1573 {"type": "reasoning", "encrypted_content": "summary"}
1574 ],
1575 "file_view_cache": {},
1576 "appended_prompt_segments": [],
1577 "pending_tool_calls": {},
1578 "usage_so_far": {
1579 "input_tokens": 0,
1580 "output_tokens": 0,
1581 "cache_creation_input_tokens": 0,
1582 "cache_read_input_tokens": 0
1583 },
1584 "summaries": [],
1585 "lineage": [],
1586 "fired_hook_ids": [],
1587 "pending_session_start_source": null,
1588 "pending_warning_messages": [],
1589 "messages_seen_by_provider": 0
1590 }))
1591 .expect("deserialize existing session state");
1592 assert_eq!(state.compacted_prefix.len(), 1);
1593 }
1594
1595 #[test]
1596 fn compaction_window_preserves_latest_assistant_response_block() {
1597 let messages = vec![
1598 Message::User(UserMessage::text("first")),
1599 assistant_text("answer"),
1600 Message::User(UserMessage::text("follow up")),
1601 ];
1602
1603 let window = CompactionWindow::preserve_latest_assistant_response_block(&messages);
1604
1605 assert_eq!(window.eligible_messages.len(), 1);
1606 assert_eq!(window.preserved_messages.len(), 2);
1607 assert!(window.reserved_response_block);
1608 }
1609
1610 #[test]
1611 fn compaction_window_preserves_through_latest_user() {
1612 let messages = vec![
1613 Message::User(UserMessage::text("first")),
1614 assistant_text("answer"),
1615 Message::User(UserMessage::text("follow up")),
1616 assistant_text("tail"),
1617 ];
1618
1619 let window = CompactionWindow::preserve_through_latest_user(&messages);
1620
1621 assert_eq!(window.preserved_messages.len(), 3);
1622 assert!(matches!(
1623 window.preserved_messages.last(),
1624 Some(Message::User(_))
1625 ));
1626 assert_eq!(window.eligible_messages.len(), 1);
1627 assert!(!window.reserved_response_block);
1628 }
1629
1630 #[test]
1631 fn user_message_with_media_roundtrips() {
1632 let message = Message::User(UserMessage {
1633 id: MessageId::new(),
1634 created_at: Utc::now(),
1635 parts: vec![
1636 UserPart::Text {
1637 text: "hello".to_owned(),
1638 },
1639 UserPart::Image {
1640 media_type: "image/png".to_owned(),
1641 data: Bytes::from_static(b"png"),
1642 },
1643 UserPart::Document {
1644 media_type: "application/pdf".to_owned(),
1645 data: Bytes::from_static(b"pdf"),
1646 },
1647 ],
1648 });
1649
1650 let encoded = serde_json::to_string(&message).expect("serialize message");
1651 let decoded: Message = serde_json::from_str(&encoded).expect("deserialize message");
1652 assert_eq!(decoded, message);
1653 }
1654
1655 #[test]
1656 fn stream_event_with_signature_roundtrips() {
1657 let event = StreamEvent::ThinkingEnd {
1658 id: BlockId::new(),
1659 signature: Some("sig-123".to_owned()),
1660 };
1661
1662 let encoded = serde_json::to_string(&event).expect("serialize event");
1663 let decoded: StreamEvent = serde_json::from_str(&encoded).expect("deserialize event");
1664 assert_eq!(decoded, event);
1665 }
1666
1667 fn assistant_text(text: &str) -> Message {
1668 Message::Assistant(AssistantMessage {
1669 id: MessageId::new(),
1670 created_at: Utc::now(),
1671 parts: vec![AssistantPart::Text {
1672 text: text.to_owned(),
1673 }],
1674 stop_reason: None,
1675 usage: None,
1676 replay_meta: ReplayMeta::default(),
1677 })
1678 }
1679}