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