Skip to main content

imp_core/
learning.rs

1use imp_llm::{ContentBlock, Message};
2
3/// Check if the session was complex enough to warrant a learning nudge.
4///
5/// Counts tool calls across all assistant messages. Returns true if the count
6/// meets or exceeds the threshold, suggesting the agent should consider saving
7/// the approach as a skill or persisting something to memory.
8pub 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
26/// The nudge message injected after complex sessions.
27pub 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
33/// Learning instructions injected into Layer 1 of the system prompt.
34pub 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        // 8 tool calls spread across 2 assistant messages
108        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}