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}