1use harness_core::{Block, Model, Tool, Turn, TurnRole};
8use std::sync::Arc;
9
10pub struct LearningConfig {
14 pub review_model: Arc<dyn Model>,
15 pub tools: Vec<Arc<dyn Tool>>,
16 pub review_prompt: String,
17 pub nudge_interval: u32,
18 pub max_iters: u32,
19}
20
21impl LearningConfig {
22 pub fn new(review_model: Arc<dyn Model>) -> Self {
23 Self {
24 review_model,
25 tools: Vec::new(),
26 review_prompt: DEFAULT_REVIEW_PROMPT.to_string(),
27 nudge_interval: 10,
28 max_iters: 6,
29 }
30 }
31 pub fn with_tool(mut self, t: Arc<dyn Tool>) -> Self { self.tools.push(t); self }
32 pub fn with_nudge_interval(mut self, n: u32) -> Self { self.nudge_interval = n; self }
33 pub fn with_review_prompt(mut self, p: impl Into<String>) -> Self { self.review_prompt = p.into(); self }
34 pub fn with_max_iters(mut self, n: u32) -> Self { self.max_iters = n; self }
35}
36
37pub const DEFAULT_REVIEW_PROMPT: &str = "\
39You are a BACKGROUND REVIEWER running after a session finished. Using ONLY the \
40tools provided (skill management + memory), update the skill library and memory \
41based on the conversation transcript below. Make at most a few focused changes.\n\n\
42Be active — most sessions that did real work produce at least one small update; a \
43pass that does nothing is a missed learning opportunity, not a neutral outcome. \
44But 'nothing to save' IS a valid result for a trivial session — if so, do nothing.\n\n\
45SKILLS (procedural memory): when a non-trivial technique, fix, workflow, or \
46correction emerged that a future session would reuse, capture it as a skill with \
47skill_manage. Prefer CLASS-LEVEL umbrella skills with a rich body (trigger \
48conditions, numbered steps with exact commands, a pitfalls section). The name must \
49be class-level (e.g. 'deploy-runbook'), NEVER a one-off ('fix-bug-1234'). If an \
50existing skill covers the territory, PATCH it (add a step or pitfall) instead of \
51creating a new one.\n\n\
52MEMORY (about the user): if the user revealed durable preferences, working style, \
53identity, or expectations about how you should behave ('stop doing X', 'always Y', \
54'remember Z'), save them with the memory tool so the next session starts knowing.\n\n\
55Make your changes, then stop.";
56
57pub fn render_transcript(history: &[Turn], max_chars: usize) -> String {
60 let mut out = String::new();
61 for turn in history {
62 let role = match turn.role {
63 TurnRole::User => "user",
64 TurnRole::Assistant => "assistant",
65 TurnRole::System => "system",
66 TurnRole::Tool => "tool",
67 _ => "unknown",
68 };
69 for b in &turn.blocks {
70 match b {
71 Block::Text(t) => out.push_str(&format!("{role}: {t}\n")),
72 Block::ToolResult { content, .. } => out.push_str(&format!("tool_result: {content}\n")),
73 _ => {} }
75 }
76 }
77 if out.len() > max_chars {
78 let start = out.len() - max_chars;
79 let start = (start..out.len()).find(|i| out.is_char_boundary(*i)).unwrap_or(out.len());
80 out = format!("…(transcript truncated)…\n{}", &out[start..]);
81 }
82 out
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn transcript_renders_roles_and_truncates_tail() {
91 let history = vec![
92 Turn { role: TurnRole::User, blocks: vec![Block::Text("hello there".into())] },
93 Turn { role: TurnRole::Assistant, blocks: vec![Block::Text("hi".into())] },
94 ];
95 let t = render_transcript(&history, 10_000);
96 assert!(t.contains("user: hello there"));
97 assert!(t.contains("assistant: hi"));
98
99 let big = vec![Turn { role: TurnRole::User, blocks: vec![Block::Text("x".repeat(50_000))] }];
100 let t = render_transcript(&big, 1_000);
101 assert!(t.len() < 1_200);
102 assert!(t.starts_with("…(transcript truncated)…"));
103 }
104}