返回 CodeWhale
auto_router.rs
根目录 / crates / tui / src / tui / auto_router.rs
1 //! Auto-routing helpers: deciding when to consult the auto-route flash
2 //! model, and building the small context window it sees.
3 //!
4 //! `dispatch_user_message` calls `model_routing::resolve_auto_route_with_inventory_for_session`
5 //! directly once per user turn when `app.auto_model` is set. The remaining
6 //! helpers here build the compact recent-context summary the router sees.
7
8 use crate::tui::app::App;
9 use codewhale_models::{ContentBlock, Message};
10
11 /// Whether the next turn should consult the auto-route flash model.
12 pub(super) fn should_resolve_auto_model_selection(app: &App) -> bool {
13 app.auto_model
14 }
15
16 /// Build a compact recent-context summary for the auto-route prompt.
17 ///
18 /// Walks `api_messages` from the most recent turn back, skipping the
19 /// final draft (which is what the router is being asked to classify),
20 /// collects up to six non-empty rows, and reverses them so the prompt
21 /// reads oldest-first. Each row is `<role>: <truncated content>` and
22 /// is capped at 900 characters.
23 pub(super) fn recent_auto_router_context(messages: &[Message]) -> String {
24 let mut rows = Vec::new();
25 for message in messages.iter().rev().skip(1) {
26 if rows.len() >= 6 {
27 break;
28 }
29 let text = content_blocks_text(&message.content);
30 let text = text.trim();
31 if text.is_empty() {
32 continue;
33 }
34 rows.push(format!(
35 "{}: {}",
36 message.role,
37 truncate_for_auto_router(text, 900)
38 ));
39 }
40 rows.reverse();
41 if rows.is_empty() {
42 "No prior context.".to_string()
43 } else {
44 rows.join("\n")
45 }
46 }
47
48 fn content_blocks_text(blocks: &[ContentBlock]) -> String {
49 let mut out = String::new();
50 for block in blocks {
51 match block {
52 ContentBlock::Text { text, .. } => {
53 append_router_text(&mut out, text);
54 }
55 ContentBlock::Thinking { .. } => {}
56 ContentBlock::ToolUse { name, .. } => {
57 append_router_text(&mut out, &format!("[tool call: {name}]"));
58 }
59 ContentBlock::ToolResult { content, .. } => {
60 append_router_text(&mut out, &format!("[tool result] {content}"));
61 }
62 _ => {}
63 }
64 }
65 out
66 }
67
68 fn append_router_text(out: &mut String, text: &str) {
69 if !out.is_empty() {
70 out.push('\n');
71 }
72 out.push_str(text);
73 }
74
75 fn truncate_for_auto_router(text: &str, max_chars: usize) -> String {
76 let mut chars = text.chars();
77 let truncated: String = chars.by_ref().take(max_chars).collect();
78 if chars.next().is_some() {
79 format!("{truncated}...")
80 } else {
81 truncated
82 }
83 }
84
85 #[cfg(test)]
86 mod tests {
87 use super::*;
88 use codewhale_models::ContentBlock;
89 use codewhale_models::Role;
90
91 fn make_msg(role: &str, text: &str) -> Message {
92 Message {
93 role: Role::from(role),
94 content: vec![ContentBlock::Text {
95 text: text.to_string(),
96 cache_control: None,
97 }],
98 }
99 }
100
101 #[test]
102 fn truncate_for_auto_router_honors_char_budget() {
103 let s = "abcdefghij";
104 assert_eq!(truncate_for_auto_router(s, 4), "abcd...");
105 assert_eq!(truncate_for_auto_router(s, 10), "abcdefghij");
106 assert_eq!(truncate_for_auto_router(s, 100), "abcdefghij");
107 }
108
109 #[test]
110 fn recent_auto_router_context_skips_final_message_and_caps_rows() {
111 // Eight messages; final one (the draft being routed) is skipped,
112 // so we expect at most six of the remaining seven.
113 let msgs: Vec<Message> = (0..8)
114 .map(|i| {
115 make_msg(
116 if i % 2 == 0 { "user" } else { "assistant" },
117 &format!("turn {i}"),
118 )
119 })
120 .collect();
121 let context = recent_auto_router_context(&msgs);
122 assert!(!context.contains("turn 7"), "final draft must be skipped");
123 let row_count = context.lines().count();
124 assert_eq!(row_count, 6);
125 // Output is oldest-first.
126 let first = context.lines().next().unwrap();
127 assert!(first.contains("turn 1"), "got: {context}");
128 }
129
130 #[test]
131 fn recent_auto_router_context_handles_empty_history() {
132 assert_eq!(recent_auto_router_context(&[]), "No prior context.");
133 }
134
135 #[test]
136 fn recent_auto_router_context_excludes_hidden_thinking() {
137 let msgs = vec![
138 Message {
139 role: Role::Assistant,
140 content: vec![
141 ContentBlock::Thinking {
142 signature: None,
143 state: None,
144 thinking: "The user seems to be asking me to classify myself.".to_string(),
145 },
146 ContentBlock::Text {
147 text: "Visible assistant answer.".to_string(),
148 cache_control: None,
149 },
150 ],
151 },
152 make_msg("user", "latest draft"),
153 ];
154
155 let context = recent_auto_router_context(&msgs);
156
157 assert!(context.contains("Visible assistant answer."));
158 assert!(!context.contains("The user seems"));
159 assert!(!context.contains("latest draft"));
160 }
161 }
162
162 lines RUST