Skip to main content

harness_loop/
profile_guide.rs

1//! Opt-in `Guide` that renders [`harness_core::UserProfile`] into the agent's system prompt.
2//!
3//! Add to your loop when you want every model call to know who it's working for:
4//!
5//! ```ignore
6//! AgentLoop::new(model)
7//!     .with_guide(std::sync::Arc::new(harness_loop::ProfileGuide))
8//!     .run(task, &mut world).await?;
9//! ```
10//!
11//! The framework deliberately does NOT auto-attach this — `World.profile` is
12//! a slot the app fills however it wants (env, CLI, app-side config), and
13//! injection into the prompt is also the app's call.
14
15use async_trait::async_trait;
16use harness_core::{Block, Context, Execution, Guide, GuideError, GuideId, GuideScope, World};
17use std::sync::OnceLock;
18
19/// Renders `World.profile` as a one-line `User profile: …` block in the
20/// agent's `guides` section. No-op when the profile is empty.
21pub struct ProfileGuide;
22
23static PROFILE_GUIDE_ID: OnceLock<GuideId> = OnceLock::new();
24static PROFILE_GUIDE_SCOPE: OnceLock<GuideScope> = OnceLock::new();
25
26#[async_trait]
27impl Guide for ProfileGuide {
28    fn id(&self) -> &GuideId {
29        PROFILE_GUIDE_ID.get_or_init(|| "user-profile".into())
30    }
31    fn kind(&self) -> Execution {
32        Execution::Inferential
33    }
34    fn scope(&self) -> &GuideScope {
35        PROFILE_GUIDE_SCOPE.get_or_init(|| GuideScope::Always)
36    }
37    async fn apply(&self, ctx: &mut Context, w: &World) -> Result<(), GuideError> {
38        let p = &w.profile;
39        if p.name.is_none() && p.tz.is_none() && p.locale.is_none() && p.extra.is_empty() {
40            return Ok(()); // empty profile — don't pollute the prompt
41        }
42        ctx.guides
43            .push(Block::Text(format!("User profile: {}", p.summary_line())));
44        Ok(())
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use harness_core::UserProfile;
51
52    #[test]
53    fn empty_profile_is_empty() {
54        let p = UserProfile::default();
55        assert!(p.name.is_none() && p.tz.is_none() && p.locale.is_none() && p.extra.is_empty());
56    }
57
58    #[test]
59    fn populated_profile_renders_line() {
60        let p = UserProfile {
61            name: Some("李亮".into()),
62            tz: Some("Asia/Shanghai".into()),
63            locale: Some("zh-CN".into()),
64            ..Default::default()
65        };
66        let s = p.summary_line();
67        assert!(s.contains("李亮"));
68        assert!(s.contains("Asia/Shanghai"));
69        assert!(s.contains("zh-CN"));
70    }
71}