harness_loop/experience/
guide.rs1use crate::experience::store::ExperienceStore;
9use async_trait::async_trait;
10use harness_core::{Block, Context, Execution, Guide, GuideError, GuideId, GuideScope, World};
11use std::sync::{Arc, OnceLock};
12
13const MARKER: &str = "[experience-recall]\n";
14
15pub struct ExperienceGuide {
17 store: Arc<ExperienceStore>,
18 top_k: usize,
19}
20
21static GUIDE_ID: OnceLock<GuideId> = OnceLock::new();
22static GUIDE_SCOPE: OnceLock<GuideScope> = OnceLock::new();
23
24impl ExperienceGuide {
25 pub fn new(store: Arc<ExperienceStore>) -> Self {
26 Self { store, top_k: 3 }
27 }
28
29 pub fn with_top_k(mut self, k: usize) -> Self {
32 self.top_k = k;
33 self
34 }
35
36 async fn block(&self, query: &str) -> Option<String> {
37 let eps = self.store.recall(query, self.top_k).await;
38 if eps.is_empty() {
39 return None;
40 }
41 let mut s = String::from(MARKER);
42 s.push_str("Similar past experience — how you handled comparable situations before:");
43 for (i, ep) in eps.iter().enumerate() {
44 let tools = if ep.tools.is_empty() {
45 "(none)".to_string()
46 } else {
47 ep.tools.join(", ")
48 };
49 s.push_str(&format!(
50 "\n {}. situation: {}\n tools used: {}\n outcome: {}",
51 i + 1,
52 ep.situation.trim(),
53 tools,
54 ep.outcome.trim(),
55 ));
56 }
57 Some(s)
58 }
59
60 fn remove_previous(ctx: &mut Context) {
61 ctx.guides
62 .retain(|b| !matches!(b, Block::Text(t) if t.starts_with(MARKER)));
63 }
64}
65
66fn last_user_text(ctx: &Context) -> Option<String> {
67 use harness_core::{Block as B, TurnRole};
68 for turn in ctx.history.iter().rev() {
69 if turn.role != TurnRole::User {
70 continue;
71 }
72 for block in turn.blocks.iter().rev() {
73 if let B::Text(t) = block
74 && !t.trim().is_empty()
75 {
76 return Some(t.clone());
77 }
78 }
79 }
80 None
81}
82
83#[async_trait]
84impl Guide for ExperienceGuide {
85 fn id(&self) -> &GuideId {
86 GUIDE_ID.get_or_init(|| "experience-recall".into())
87 }
88 fn kind(&self) -> Execution {
89 Execution::Computational
90 }
91 fn scope(&self) -> &GuideScope {
92 GUIDE_SCOPE.get_or_init(|| GuideScope::Always)
93 }
94 async fn apply(&self, ctx: &mut Context, _w: &World) -> Result<(), GuideError> {
95 Self::remove_previous(ctx);
96 if let Some(block) = self.block(&ctx.task.description).await {
97 ctx.guides.push(Block::Text(block));
98 }
99 Ok(())
100 }
101 async fn apply_before_iter(&self, ctx: &mut Context, _w: &World) -> Result<(), GuideError> {
102 let query = last_user_text(ctx).unwrap_or_else(|| ctx.task.description.clone());
103 Self::remove_previous(ctx);
104 if let Some(block) = self.block(&query).await {
105 ctx.guides.push(Block::Text(block));
106 }
107 Ok(())
108 }
109}