返回 CodeWhale
prompt_zones.rs
根目录 / crates / tui / src / prompt_zones.rs
1 //! Three-zone prompt contract types for prefix-cache stability (#2264).
2 //!
3 //! Divides every request into three rigid zones:
4 //!
5 //! ```text
6 //! ┌─────────────────────────────────────────┐
7 //! │ PinnedPrefix (frozen after construction) │ ← system prompt + tool catalog
8 //! │ combined_sha256 computed at freeze() │ cache hit candidate
9 //! ├─────────────────────────────────────────┤
10 //! │ AppendLog (append-only) │ ← conversation history
11 //! │ push() only, no insert / remove / edit │ preserves prefix of prior turns
12 //! ├─────────────────────────────────────────┤
13 //! │ TurnScratch (ephemeral) │ ← per-turn metadata
14 //! │ cleared at every turn boundary │ the only new content per request
15 //! └─────────────────────────────────────────┘
16 //! ```
17 //!
18 //! ## Status (Phase 1 foundation)
19 //!
20 //! `PinnedPrefix` / `FrozenPrefix` / `PrefixDrift` are ready for use.
21 //! `AppendLog` / `TurnScratch` / `ThreeZoneRequest` are type scaffolding
22 //! for future phases — not yet wired into the request path.
23
24 use codewhale_models::Role;
25 use codewhale_models::{Message, SystemPrompt, Tool};
26 use std::sync::Arc;
27 // ── helpers ────────────────────────────────────────────────────────────
28
29 fn sha256_hex(bytes: &[u8]) -> String {
30 crate::hashing::sha256_hex(bytes)
31 }
32
33 fn system_text(system: Option<&SystemPrompt>) -> String {
34 match system {
35 Some(SystemPrompt::Text(text)) => text.clone(),
36 Some(SystemPrompt::Blocks(blocks)) => {
37 let mut text = String::new();
38 for block in blocks {
39 text.push_str(&block.text);
40 text.push('\n');
41 }
42 text
43 }
44 None => String::new(),
45 }
46 }
47
48 /// Serialize tools to a deterministic, sorted JSON string for hashing.
49 fn tool_catalog_digest(tools: &[Tool]) -> String {
50 let mut serialized: Vec<String> = tools
51 .iter()
52 .filter_map(|t| serde_json::to_string(t).ok())
53 .collect();
54 serialized.sort();
55 serialized.join("\n")
56 }
57
58 fn combined_hash(system_text: &str, tools: &[Tool]) -> String {
59 let system_sha = sha256_hex(system_text.as_bytes());
60 let tools_digest = tool_catalog_digest(tools);
61 let tools_sha = sha256_hex(tools_digest.as_bytes());
62 let combined = format!("{system_sha}:{tools_sha}");
63 sha256_hex(combined.as_bytes())
64 }
65
66 // ── FrozenPrefix ───────────────────────────────────────────────────────
67
68 /// An immutable frozen prefix — system prompt text + tool catalog,
69 /// hashed at freeze time. The hash is stable as long as the system prompt
70 /// text and full tool definitions (name, description, schema) are unchanged.
71 ///
72 /// Use [`PinnedPrefix::freeze`] to produce one.
73 #[derive(Debug, Clone, PartialEq, Eq)]
74 pub struct FrozenPrefix {
75 pub(crate) system_text: String,
76 pub(crate) tool_catalog: String,
77 pub(crate) combined_sha256: String,
78 }
79
80 impl FrozenPrefix {
81 /// Verify that `current_system_text` and `current_tools` match the frozen
82 /// prefix. Returns `Ok(())` when stable, `Err(PrefixDrift)` on mismatch.
83 ///
84 /// Fast path: compares raw text before falling back to SHA-256.
85 pub fn verify(
86 &self,
87 current_system_text: &str,
88 current_tools: &[Tool],
89 ) -> Result<(), PrefixDrift> {
90 let system_changed = current_system_text != self.system_text;
91 let current_tool_catalog = tool_catalog_digest(current_tools);
92 let tools_changed = current_tool_catalog != self.tool_catalog;
93
94 if !system_changed && !tools_changed {
95 return Ok(());
96 }
97
98 let current_hash = combined_hash(current_system_text, current_tools);
99 Err(PrefixDrift {
100 system_changed,
101 tools_changed,
102 frozen_hash: self.combined_sha256.clone(),
103 current_hash,
104 })
105 }
106
107 /// Returns a short (12-char) human-readable id for display.
108 #[must_use]
109 pub fn short_id(&self) -> &str {
110 if self.combined_sha256.len() >= 12 {
111 &self.combined_sha256[..12]
112 } else {
113 &self.combined_sha256
114 }
115 }
116
117 /// Returns the full combined SHA-256.
118 #[must_use]
119 pub fn hash(&self) -> &str {
120 &self.combined_sha256
121 }
122 }
123
124 // ── PinnedPrefix ───────────────────────────────────────────────────────
125
126 /// A mutable prefix builder. Construct from the system prompt and tool
127 /// catalog, then call [`freeze`](Self::freeze) to produce a [`FrozenPrefix`].
128 #[derive(Debug, Clone)]
129 pub struct PinnedPrefix {
130 system_text: String,
131 tools: Vec<Tool>,
132 }
133
134 impl PinnedPrefix {
135 #[must_use]
136 pub fn new(system: Option<&SystemPrompt>, tools: Vec<Tool>) -> Self {
137 Self {
138 system_text: system_text(system),
139 tools,
140 }
141 }
142
143 /// Freeze this prefix into an immutable [`FrozenPrefix`].
144 #[must_use]
145 pub fn freeze(&self) -> FrozenPrefix {
146 let tool_catalog = tool_catalog_digest(&self.tools);
147 let combined_sha256 = combined_hash(&self.system_text, &self.tools);
148
149 FrozenPrefix {
150 system_text: self.system_text.clone(),
151 tool_catalog,
152 combined_sha256,
153 }
154 }
155 }
156
157 // ── PrefixDrift ────────────────────────────────────────────────────────
158
159 /// Describes how the current prefix differs from the frozen baseline.
160 #[derive(Debug, Clone, PartialEq, Eq)]
161 pub struct PrefixDrift {
162 pub system_changed: bool,
163 pub tools_changed: bool,
164 pub frozen_hash: String,
165 pub current_hash: String,
166 }
167
168 impl std::fmt::Display for PrefixDrift {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 let cause = match (self.system_changed, self.tools_changed) {
171 (true, true) => "system prompt and tool set",
172 (true, false) => "system prompt",
173 (false, true) => "tool set",
174 (false, false) => "unknown component",
175 };
176 write!(
177 f,
178 "prefix drift: {cause} changed (frozen={}, current={})",
179 &self.frozen_hash[..12.min(self.frozen_hash.len())],
180 &self.current_hash[..12.min(self.current_hash.len())]
181 )
182 }
183 }
184
185 // ── AppendLog ──────────────────────────────────────────────────────────
186
187 /// Append-only conversation history. Derefs to `&[Message]` via
188 /// [`Deref`](std::ops::Deref) for transparent read access; mutations go
189 /// through explicit methods (`push`, `truncate_to`, `trim_front`, `clear`)
190 /// whose names make cache impact obvious.
191 ///
192 /// Phase 4: backing store for `Session.messages` (#2264).
193 ///
194 /// The history is reference-counted (#6214 T2): snapshots hand out `Arc`
195 /// clones instead of deep-copying the transcript per event, and mutations
196 /// copy-on-write only while a snapshot is outstanding.
197 #[derive(Debug, Clone)]
198 pub struct AppendLog {
199 messages: Arc<Vec<Message>>,
200 }
201
202 impl AppendLog {
203 pub fn new() -> Self {
204 Self {
205 messages: Arc::new(Vec::new()),
206 }
207 }
208
209 pub fn from_messages(messages: Vec<Message>) -> Self {
210 Self {
211 messages: Arc::new(messages),
212 }
213 }
214
215 /// Share the current history without copying. The engine hands this to
216 /// `Event::SessionUpdated`; the `Arc` is immutable, so an outstanding
217 /// snapshot can never observe a later mutation.
218 #[must_use]
219 pub fn snapshot(&self) -> Arc<Vec<Message>> {
220 Arc::clone(&self.messages)
221 }
222
223 /// Append a message to the log. A single-message push is the cheapest
224 /// mutation for prefix-cache stability — it extends the byte sequence
225 /// without disturbing earlier turns.
226 pub fn push(&mut self, message: Message) {
227 Arc::make_mut(&mut self.messages).push(message);
228 }
229
230 /// Append multiple messages in one operation (fewer cache-line
231 /// invalidations than repeated `push`).
232 pub fn push_batch(&mut self, batch: Vec<Message>) {
233 Arc::make_mut(&mut self.messages).extend(batch);
234 }
235
236 /// Truncate to keep only the first `new_len` messages.
237 /// Discards newer messages (and their prefix-cache contribution)
238 /// from the tail.
239 pub fn truncate_to(&mut self, new_len: usize) {
240 Arc::make_mut(&mut self.messages).truncate(new_len);
241 }
242
243 /// Remove `count` messages from the front (oldest first).
244 /// Cache-destroying: drops the prefix that earlier turns share.
245 pub fn trim_front(&mut self, count: usize) {
246 let messages = Arc::make_mut(&mut self.messages);
247 if count >= messages.len() {
248 messages.clear();
249 } else {
250 messages.drain(0..count);
251 }
252 }
253
254 /// Remove all messages. Resets cache state completely.
255 pub fn clear(&mut self) {
256 Arc::make_mut(&mut self.messages).clear();
257 }
258
259 /// Return a mutable reference to the last message, if any.
260 /// Prefer this over `last_mut()` on the inner vec — the name signals
261 /// that only the most recent turn's content is being modified.
262 #[must_use]
263 pub fn last_mut(&mut self) -> Option<&mut Message> {
264 Arc::make_mut(&mut self.messages).last_mut()
265 }
266
267 /// Consume and return the inner `Vec<Message>`, copying only if a
268 /// snapshot still shares it.
269 #[must_use]
270 pub fn into_inner(self) -> Vec<Message> {
271 Arc::try_unwrap(self.messages).unwrap_or_else(|shared| (*shared).clone())
272 }
273 }
274
275 impl Default for AppendLog {
276 fn default() -> Self {
277 Self::new()
278 }
279 }
280
281 impl From<Vec<Message>> for AppendLog {
282 fn from(messages: Vec<Message>) -> Self {
283 Self {
284 messages: Arc::new(messages),
285 }
286 }
287 }
288
289 impl From<AppendLog> for Vec<Message> {
290 fn from(log: AppendLog) -> Self {
291 log.into_inner()
292 }
293 }
294
295 impl std::ops::Deref for AppendLog {
296 type Target = Vec<Message>;
297
298 fn deref(&self) -> &Self::Target {
299 &self.messages
300 }
301 }
302
303 // ── TurnScratch ────────────────────────────────────────────────────────
304
305 /// Per-turn ephemeral data. Cleared at every turn boundary.
306 ///
307 /// **Phase 1 scaffolding** — not yet wired into the engine request path.
308 #[cfg_attr(not(test), expect(dead_code))]
309 #[derive(Debug, Clone, Default)]
310 pub struct TurnScratch {
311 pub working_set: Vec<String>,
312 pub user_message: Option<Message>,
313 }
314
315 #[cfg_attr(not(test), expect(dead_code))]
316 impl TurnScratch {
317 pub fn new() -> Self {
318 Self::default()
319 }
320
321 pub fn clear(&mut self) {
322 self.working_set.clear();
323 self.user_message = None;
324 }
325
326 #[must_use]
327 pub fn is_empty(&self) -> bool {
328 self.working_set.is_empty() && self.user_message.is_none()
329 }
330 }
331
332 // ── ThreeZoneRequest ───────────────────────────────────────────────────
333
334 /// A composed three-zone request ready for DeepSeek API serialization.
335 ///
336 /// **Phase 1 scaffolding** — not yet wired into the engine request path.
337 /// Currently the engine continues to use `MessageRequest` directly.
338 #[expect(dead_code)]
339 #[derive(Debug, Clone)]
340 pub struct ThreeZoneRequest<'a> {
341 pub prefix: &'a FrozenPrefix,
342 pub log: &'a AppendLog,
343 pub scratch: TurnScratch,
344 pub model: String,
345 pub max_tokens: u32,
346 pub system: Option<SystemPrompt>,
347 pub tools: Option<Vec<Tool>>,
348 pub tool_choice: Option<serde_json::Value>,
349 pub reasoning_effort: Option<String>,
350 pub thinking: Option<serde_json::Value>,
351 pub stream: Option<bool>,
352 pub temperature: Option<f32>,
353 pub top_p: Option<f32>,
354 pub metadata: Option<serde_json::Value>,
355 }
356
357 #[cfg_attr(not(test), expect(dead_code))]
358 impl<'a> ThreeZoneRequest<'a> {
359 /// Build the full message list from system prompt, append-log messages,
360 /// and scratch user message. The returned vector is serialized as the
361 /// `messages` field in the DeepSeek chat-completion request.
362 #[must_use]
363 pub fn build_messages(&self) -> Vec<Message> {
364 let mut messages = Vec::with_capacity(self.message_count());
365
366 match self.system.as_ref() {
367 Some(SystemPrompt::Text(text)) => {
368 messages.push(Message {
369 role: Role::System,
370 content: vec![codewhale_models::ContentBlock::Text {
371 text: text.clone(),
372 cache_control: None,
373 }],
374 });
375 }
376 Some(SystemPrompt::Blocks(blocks)) => {
377 let content: Vec<codewhale_models::ContentBlock> = blocks
378 .iter()
379 .map(|block| codewhale_models::ContentBlock::Text {
380 text: block.text.clone(),
381 cache_control: block.cache_control.clone(),
382 })
383 .collect();
384 messages.push(Message {
385 role: Role::System,
386 content,
387 });
388 }
389 None => {}
390 }
391
392 for msg in self.log.iter() {
393 messages.push(msg.clone());
394 }
395
396 if let Some(ref user_msg) = self.scratch.user_message {
397 messages.push(user_msg.clone());
398 }
399
400 messages
401 }
402
403 #[must_use]
404 pub fn message_count(&self) -> usize {
405 let system_count = if self.system.is_some() { 1 } else { 0 };
406 let scratch_count = if self.scratch.user_message.is_some() {
407 1
408 } else {
409 0
410 };
411 system_count + self.log.len() + scratch_count
412 }
413 }
414
415 // ── tests ──────────────────────────────────────────────────────────────
416
417 #[cfg(test)]
418 mod tests {
419 use super::*;
420 use codewhale_models::ContentBlock;
421
422 fn make_tool(name: &str) -> Tool {
423 Tool {
424 name: name.to_string(),
425 description: String::new(),
426 input_schema: serde_json::Value::Null,
427 tool_type: None,
428 allowed_callers: None,
429 defer_loading: None,
430 input_examples: None,
431 strict: None,
432 cache_control: None,
433 }
434 }
435
436 fn make_message(role: &str, text: &str) -> Message {
437 Message {
438 role: Role::from(role),
439 content: vec![ContentBlock::Text {
440 text: text.to_string(),
441 cache_control: None,
442 }],
443 }
444 }
445
446 // ── AppendLog ────────────────────────────────────────────────
447
448 #[test]
449 fn append_log_snapshot_shares_and_mutation_detaches() {
450 let mut log = AppendLog::new();
451 log.push(make_message("user", "hello"));
452 let shared = log.snapshot();
453 // No copy: the snapshot aliases the live log.
454 assert!(Arc::ptr_eq(&shared, &log.snapshot()));
455 log.push(make_message("assistant", "hi"));
456 // Copy-on-write: the outstanding snapshot still sees one message.
457 assert_eq!(shared.len(), 1);
458 assert_eq!(log.len(), 2);
459 }
460
461 // ── FrozenPrefix / PinnedPrefix ────────────────────────────────
462
463 #[test]
464 fn freeze_produces_stable_hash() {
465 let tools = vec![make_tool("read"), make_tool("write")];
466 let sys = SystemPrompt::Text("hello world".to_string());
467
468 let a = PinnedPrefix::new(Some(&sys), tools.clone()).freeze();
469 let b = PinnedPrefix::new(Some(&sys), tools).freeze();
470
471 assert_eq!(a.combined_sha256, b.combined_sha256);
472 assert_eq!(a.hash(), b.hash());
473 assert_eq!(a.short_id(), b.short_id());
474 }
475
476 #[test]
477 fn freeze_tool_order_is_stable() {
478 let sys = SystemPrompt::Text("system".to_string());
479 let tools_a = vec![make_tool("b"), make_tool("a")];
480 let tools_b = vec![make_tool("a"), make_tool("b")];
481
482 let a = PinnedPrefix::new(Some(&sys), tools_a).freeze();
483 let b = PinnedPrefix::new(Some(&sys), tools_b).freeze();
484
485 assert_eq!(a.combined_sha256, b.combined_sha256);
486 }
487
488 #[test]
489 fn freeze_empty_tools() {
490 let sys = SystemPrompt::Text("system".to_string());
491 let frozen = PinnedPrefix::new(Some(&sys), vec![]).freeze();
492 assert!(frozen.tool_catalog.is_empty());
493 assert!(!frozen.combined_sha256.is_empty());
494 assert_eq!(frozen.short_id().len(), 12);
495 }
496
497 #[test]
498 fn freeze_no_system() {
499 let tools = vec![make_tool("t1")];
500 let frozen = PinnedPrefix::new(None, tools).freeze();
501 assert!(frozen.system_text.is_empty());
502 assert!(frozen.tool_catalog.contains("t1"));
503 }
504
505 #[test]
506 fn verify_passes_when_stable() {
507 let sys = SystemPrompt::Text("system".to_string());
508 let tools = vec![make_tool("a")];
509 let frozen = PinnedPrefix::new(Some(&sys), tools.clone()).freeze();
510
511 assert!(frozen.verify("system", &tools).is_ok());
512 }
513
514 #[test]
515 fn verify_detects_system_change() {
516 let sys = SystemPrompt::Text("old".to_string());
517 let tools = vec![make_tool("a")];
518 let frozen = PinnedPrefix::new(Some(&sys), tools.clone()).freeze();
519
520 let drift = frozen.verify("new", &tools).unwrap_err();
521 assert!(drift.system_changed);
522 assert!(!drift.tools_changed);
523 }
524
525 #[test]
526 fn verify_detects_tool_change() {
527 let sys = SystemPrompt::Text("system".to_string());
528 let tools_a = vec![make_tool("a")];
529 let frozen = PinnedPrefix::new(Some(&sys), tools_a).freeze();
530
531 let tools_b = vec![make_tool("b")];
532 let drift = frozen.verify("system", &tools_b).unwrap_err();
533 assert!(!drift.system_changed);
534 assert!(drift.tools_changed);
535 }
536
537 #[test]
538 fn verify_detects_both_changes() {
539 let sys = SystemPrompt::Text("old".to_string());
540 let tools = vec![make_tool("a")];
541 let frozen = PinnedPrefix::new(Some(&sys), tools).freeze();
542
543 let drift = frozen.verify("new", &[make_tool("b")]).unwrap_err();
544 assert!(drift.system_changed);
545 assert!(drift.tools_changed);
546 }
547
548 #[test]
549 fn verify_detects_schema_change() {
550 let sys = SystemPrompt::Text("system".to_string());
551 let tool_a = make_tool("a");
552 let mut tool_a_v2 = make_tool("a");
553 tool_a_v2.description = "updated desc".to_string();
554
555 let frozen = PinnedPrefix::new(Some(&sys), vec![tool_a]).freeze();
556 let drift = frozen.verify("system", &[tool_a_v2]).unwrap_err();
557 // Same name, different schema — should detect the change.
558 assert!(drift.tools_changed);
559 }
560
561 #[test]
562 fn prefix_drift_display_is_readable() {
563 let drift = PrefixDrift {
564 system_changed: true,
565 tools_changed: false,
566 frozen_hash: "a".repeat(64),
567 current_hash: "b".repeat(64),
568 };
569 let display = drift.to_string();
570 assert!(display.contains("system prompt"));
571 assert!(display.contains("aaaaaaaaaaaa"));
572 assert!(display.contains("bbbbbbbbbbbb"));
573 }
574
575 // ── AppendLog ─────────────────────────────────────────────────
576
577 #[test]
578 fn append_log_push_and_iter() {
579 let mut log = AppendLog::new();
580 assert!(log.is_empty());
581
582 log.push(make_message("user", "hello"));
583 log.push(make_message("assistant", "hi"));
584
585 assert_eq!(log.len(), 2);
586 assert!(!log.is_empty());
587
588 let messages: Vec<_> = log.iter().collect();
589 assert_eq!(messages.len(), 2);
590 }
591
592 #[test]
593 fn append_log_from_messages() {
594 let msgs = vec![make_message("user", "a"), make_message("assistant", "b")];
595 let log = AppendLog::from_messages(msgs);
596 assert_eq!(log.len(), 2);
597 assert_eq!(log.as_slice().len(), 2);
598 }
599
600 // ── TurnScratch ───────────────────────────────────────────────
601
602 #[test]
603 fn scratch_clear_empties_all_fields() {
604 let mut scratch = TurnScratch::new();
605 scratch.working_set.push("file.rs".to_string());
606 scratch.user_message = Some(make_message("user", "task"));
607
608 assert!(!scratch.is_empty());
609 scratch.clear();
610 assert!(scratch.is_empty());
611 assert!(scratch.working_set.is_empty());
612 assert!(scratch.user_message.is_none());
613 }
614
615 // ── ThreeZoneRequest ──────────────────────────────────────────
616
617 #[test]
618 fn build_messages_concatenates_zones() {
619 let sys = SystemPrompt::Text("you are helpful".to_string());
620 let tools = vec![make_tool("read")];
621 let prefix = PinnedPrefix::new(Some(&sys), tools).freeze();
622
623 let mut log = AppendLog::new();
624 log.push(make_message("user", "prev question"));
625 log.push(make_message("assistant", "prev answer"));
626
627 let scratch = TurnScratch {
628 working_set: vec!["main.rs".to_string()],
629 user_message: Some(make_message("user", "current task")),
630 };
631
632 let request = ThreeZoneRequest {
633 prefix: &prefix,
634 log: &log,
635 scratch,
636 model: "deepseek-v4-pro".to_string(),
637 max_tokens: 4096,
638 system: Some(sys),
639 tools: None,
640 tool_choice: None,
641 reasoning_effort: None,
642 thinking: None,
643 stream: None,
644 temperature: None,
645 top_p: None,
646 metadata: None,
647 };
648
649 let messages = request.build_messages();
650 assert_eq!(messages.len(), 4);
651 assert_eq!(messages[0].role, "system");
652 assert_eq!(messages[1].role, "user");
653 assert_eq!(messages[2].role, "assistant");
654 assert_eq!(messages[3].role, "user");
655 assert_eq!(request.message_count(), 4);
656 }
657
658 #[test]
659 fn build_messages_no_system_no_scratch() {
660 let prefix = PinnedPrefix::new(None, vec![]).freeze();
661
662 let mut log = AppendLog::new();
663 log.push(make_message("user", "hi"));
664
665 let request = ThreeZoneRequest {
666 prefix: &prefix,
667 log: &log,
668 scratch: TurnScratch::new(),
669 model: "x".to_string(),
670 max_tokens: 1,
671 system: None,
672 tools: None,
673 tool_choice: None,
674 reasoning_effort: None,
675 thinking: None,
676 stream: None,
677 temperature: None,
678 top_p: None,
679 metadata: None,
680 };
681
682 let messages = request.build_messages();
683 assert_eq!(messages.len(), 1);
684 assert_eq!(request.message_count(), 1);
685 }
686
687 #[test]
688 fn blocks_system_prompt_preserves_cache_control() {
689 use codewhale_models::{CacheControl, SystemBlock};
690 let cc = Some(CacheControl {
691 cache_type: "ephemeral".to_string(),
692 });
693 let blocks = SystemPrompt::Blocks(vec![SystemBlock {
694 block_type: "text".to_string(),
695 text: "hello".to_string(),
696 cache_control: cc.clone(),
697 }]);
698
699 let prefix = PinnedPrefix::new(Some(&blocks), vec![]).freeze();
700 let log = AppendLog::new();
701 let scratch = TurnScratch::new();
702 let request = ThreeZoneRequest {
703 prefix: &prefix,
704 log: &log,
705 scratch,
706 model: "x".to_string(),
707 max_tokens: 1,
708 system: Some(blocks),
709 tools: None,
710 tool_choice: None,
711 reasoning_effort: None,
712 thinking: None,
713 stream: None,
714 temperature: None,
715 top_p: None,
716 metadata: None,
717 };
718
719 let messages = request.build_messages();
720 assert_eq!(messages.len(), 1);
721 assert_eq!(messages[0].role, "system");
722 // cache_control should be preserved on the block.
723 if let ContentBlock::Text {
724 cache_control: actual_cc,
725 ..
726 } = &messages[0].content[0]
727 {
728 assert_eq!(
729 actual_cc.as_ref().map(|c| c.cache_type.as_str()),
730 Some("ephemeral")
731 );
732 } else {
733 panic!("expected Text content block");
734 }
735 }
736 }
737
737 lines RUST