1use imp_llm::{ContentBlock, Message};
2
3pub fn should_nudge_learning(messages: &[Message], threshold: u32) -> bool {
9 if threshold == 0 {
10 return false;
11 }
12
13 let tool_call_count: u32 = messages
14 .iter()
15 .filter_map(|m| match m {
16 Message::Assistant(a) => Some(&a.content),
17 _ => None,
18 })
19 .flat_map(|blocks| blocks.iter())
20 .filter(|b| matches!(b, ContentBlock::ToolCall { .. }))
21 .count() as u32;
22
23 tool_call_count >= threshold
24}
25
26pub const LEARNING_NUDGE: &str = "\
28Before we finish — this was a complex session. Consider:
291. Is there anything worth capturing in mana facts, mana notes, or user profile context?
302. Should the approach be saved as a skill or extension for future reuse?
313. If you used a skill that was wrong or incomplete, patch it.";
32
33pub const LEARNING_INSTRUCTIONS: &str = "\
35You can author skills and should use mana for durable project knowledge. \
36Use them to save durable knowledge and reduce repeat work.";
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41 use imp_llm::{AssistantMessage, StopReason, UserMessage};
42
43 fn user_msg(text: &str) -> Message {
44 Message::User(UserMessage {
45 content: vec![ContentBlock::Text {
46 text: text.to_string(),
47 }],
48 timestamp: 0,
49 })
50 }
51
52 fn assistant_text(text: &str) -> Message {
53 Message::Assistant(AssistantMessage {
54 content: vec![ContentBlock::Text {
55 text: text.to_string(),
56 }],
57 usage: None,
58 stop_reason: StopReason::EndTurn,
59 timestamp: 0,
60 })
61 }
62
63 fn assistant_with_tool_calls(n: usize) -> Message {
64 let mut content = Vec::new();
65 for i in 0..n {
66 content.push(ContentBlock::ToolCall {
67 id: format!("call_{i}"),
68 name: "read".to_string(),
69 arguments: serde_json::json!({}),
70 });
71 }
72 Message::Assistant(AssistantMessage {
73 content,
74 usage: None,
75 stop_reason: StopReason::ToolUse,
76 timestamp: 0,
77 })
78 }
79
80 fn tool_result(call_id: &str) -> Message {
81 Message::ToolResult(imp_llm::ToolResultMessage {
82 tool_call_id: call_id.to_string(),
83 tool_name: "read".to_string(),
84 content: vec![ContentBlock::Text {
85 text: "ok".to_string(),
86 }],
87 is_error: false,
88 details: serde_json::Value::Null,
89 timestamp: 0,
90 })
91 }
92
93 #[test]
94 fn learning_nudge_below_threshold() {
95 let messages = vec![
96 user_msg("hello"),
97 assistant_with_tool_calls(2),
98 tool_result("call_0"),
99 tool_result("call_1"),
100 assistant_text("done"),
101 ];
102 assert!(!should_nudge_learning(&messages, 8));
103 }
104
105 #[test]
106 fn learning_nudge_at_threshold() {
107 let messages = vec![
109 user_msg("do stuff"),
110 assistant_with_tool_calls(4),
111 tool_result("call_0"),
112 assistant_with_tool_calls(4),
113 tool_result("call_0"),
114 assistant_text("done"),
115 ];
116 assert!(should_nudge_learning(&messages, 8));
117 }
118
119 #[test]
120 fn learning_nudge_above_threshold() {
121 let messages = vec![
122 user_msg("big task"),
123 assistant_with_tool_calls(10),
124 assistant_text("done"),
125 ];
126 assert!(should_nudge_learning(&messages, 8));
127 }
128
129 #[test]
130 fn learning_nudge_zero_threshold_never_nudges() {
131 let messages = vec![assistant_with_tool_calls(100)];
132 assert!(!should_nudge_learning(&messages, 0));
133 }
134
135 #[test]
136 fn learning_nudge_empty_messages() {
137 assert!(!should_nudge_learning(&[], 8));
138 }
139
140 #[test]
141 fn learning_nudge_only_text_messages() {
142 let messages = vec![user_msg("hello"), assistant_text("hi back")];
143 assert!(!should_nudge_learning(&messages, 1));
144 }
145}