Skip to main content

harness_core/
profile.rs

1//! User profile — ambient context every agent inherits.
2//!
3//! The framework keeps this deliberately small: the three things that almost
4//! every coding/scheduling/personal agent needs (name, timezone, locale) plus
5//! a free-form `extra` map for agent-specific preferences.
6//!
7//! Persistence is up to the runtime layer (see `harness_context::profile`).
8//! Tools read [`crate::World::profile`]; an opt-in `ProfileGuide` from
9//! `harness_loop` automatically renders it into the agent's system prompt.
10
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13
14/// Ambient information about who the agent is working for.
15///
16/// All fields are optional; an empty profile is the documented default.
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct UserProfile {
19    /// Display name, e.g. "李亮".
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub name: Option<String>,
22
23    /// IANA timezone identifier, e.g. "Asia/Shanghai", "Europe/Vienna".
24    /// When unset, agents should fall back to the system clock's local tz.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub tz: Option<String>,
27
28    /// BCP-47 locale, e.g. "zh-CN", "en-US". Affects reply language + date formatting.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub locale: Option<String>,
31
32    /// Free-form agent-specific preferences. Examples:
33    /// `default_meeting_duration_min: 60`, `preferred_linter: "clippy"`, …
34    ///
35    /// Keys should be namespaced when shared across agents (e.g.
36    /// `"scheduler.default_meeting_duration_min"`).
37    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
38    pub extra: BTreeMap<String, serde_json::Value>,
39}
40
41impl UserProfile {
42    /// Build a one-line prompt-friendly summary. Used by `ProfileGuide`.
43    pub fn summary_line(&self) -> String {
44        let mut parts = Vec::new();
45        if let Some(n) = &self.name {
46            parts.push(format!("name={n}"));
47        }
48        parts.push(match &self.tz {
49            Some(z) => format!("tz={z}"),
50            None => "tz=(system clock)".into(),
51        });
52        if let Some(l) = &self.locale {
53            parts.push(format!("locale={l}"));
54        }
55        parts.join(", ")
56    }
57
58    /// Read an `extra` key as a typed value.
59    pub fn extra<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
60        self.extra
61            .get(key)
62            .cloned()
63            .and_then(|v| serde_json::from_value(v).ok())
64    }
65
66    /// Set an `extra` key, replacing any existing value.
67    pub fn set_extra<T: Serialize>(&mut self, key: impl Into<String>, value: T) {
68        if let Ok(v) = serde_json::to_value(value) {
69            self.extra.insert(key.into(), v);
70        }
71    }
72}