Skip to main content

microclaw_engine/config/
autonomy.rs

1use super::*;
2
3pub(crate) fn default_sleep_time_idle_hours() -> u64 {
4    6
5}
6
7pub(crate) fn default_sleep_time_min_interval_hours() -> u64 {
8    24
9}
10
11pub(crate) fn default_sleep_time_similarity_threshold() -> f64 {
12    0.82
13}
14
15pub(crate) fn default_sleep_time_max_archived_per_pass() -> usize {
16    20
17}
18
19/// "Sleep-time" memory consolidation: when a chat has been idle for a while, run a
20/// background, deterministic (no-LLM) pass that archives near-duplicate memories so
21/// the store stops accumulating redundancy between reflector runs. PROFILE memories
22/// are never touched, archiving is reversible, and the pass is capped and throttled.
23/// OFF by default. First slice of the v0.3.0 "Self-Improving Runtime" sleep-time
24/// consolidation (Pillar 1c).
25#[derive(Clone, Debug, Serialize, Deserialize)]
26pub struct SleepTimeConfig {
27    #[serde(default)]
28    pub enabled: bool,
29    /// Only consolidate a chat after this many hours with no messages.
30    #[serde(default = "default_sleep_time_idle_hours")]
31    pub idle_hours: u64,
32    /// At most one consolidation pass per chat per this many hours.
33    #[serde(default = "default_sleep_time_min_interval_hours")]
34    pub min_interval_hours: u64,
35    /// Jaccard similarity at/above which two same-category memories are treated as
36    /// duplicates (the lower-confidence one is archived). Clamped to [0.5, 1.0].
37    #[serde(default = "default_sleep_time_similarity_threshold")]
38    pub similarity_threshold: f64,
39    /// Safety cap on how many memories a single pass may archive per chat.
40    #[serde(default = "default_sleep_time_max_archived_per_pass")]
41    pub max_archived_per_pass: usize,
42}
43
44impl Default for SleepTimeConfig {
45    fn default() -> Self {
46        Self {
47            enabled: false,
48            idle_hours: default_sleep_time_idle_hours(),
49            min_interval_hours: default_sleep_time_min_interval_hours(),
50            similarity_threshold: default_sleep_time_similarity_threshold(),
51            max_archived_per_pass: default_sleep_time_max_archived_per_pass(),
52        }
53    }
54}
55
56pub(crate) fn default_heartbeat_interval_mins() -> u64 {
57    30
58}
59
60pub(crate) fn default_heartbeat_max_chars() -> usize {
61    8000
62}
63
64/// OpenClaw-style proactive heartbeat. When enabled, every `interval_mins`
65/// the bot reads each chat's `runtime/groups/<chat_id>/HEARTBEAT.md` checklist
66/// and runs an agent turn over it; the agent messages the chat only when
67/// something on the list genuinely needs attention, otherwise stays silent.
68/// OFF by default — and chats without a HEARTBEAT.md file are never touched.
69#[derive(Clone, Debug, Serialize, Deserialize)]
70pub struct HeartbeatConfig {
71    #[serde(default)]
72    pub enabled: bool,
73    /// Minutes between heartbeat sweeps. Default: 30.
74    #[serde(default = "default_heartbeat_interval_mins")]
75    pub interval_mins: u64,
76    /// Max characters of HEARTBEAT.md injected into the prompt. Default: 8000.
77    #[serde(default = "default_heartbeat_max_chars")]
78    pub max_chars: usize,
79}
80
81impl Default for HeartbeatConfig {
82    fn default() -> Self {
83        Self {
84            enabled: false,
85            interval_mins: default_heartbeat_interval_mins(),
86            max_chars: default_heartbeat_max_chars(),
87        }
88    }
89}
90
91pub(crate) fn default_idle_checkin_idle_hours() -> u64 {
92    24
93}
94
95pub(crate) fn default_idle_checkin_min_interval_hours() -> u64 {
96    24
97}
98
99/// Proactive "long-silence" check-in: after a chat has been quiet for a while,
100/// optionally let the bot reach out IF it has something genuinely useful to say
101/// (a pending follow-up, a due reminder). OFF by default — it is outward-facing
102/// and uses an LLM call per idle chat.
103#[derive(Clone, Debug, Serialize, Deserialize)]
104pub struct IdleCheckinConfig {
105    #[serde(default)]
106    pub enabled: bool,
107    /// Only consider a chat idle after this many hours with no messages.
108    #[serde(default = "default_idle_checkin_idle_hours")]
109    pub idle_hours: u64,
110    /// At most one check-in per chat per this many hours.
111    #[serde(default = "default_idle_checkin_min_interval_hours")]
112    pub min_interval_hours: u64,
113}
114
115impl Default for IdleCheckinConfig {
116    fn default() -> Self {
117        Self {
118            enabled: false,
119            idle_hours: default_idle_checkin_idle_hours(),
120            min_interval_hours: default_idle_checkin_min_interval_hours(),
121        }
122    }
123}
124
125pub(crate) fn default_interjection_min_interval_secs() -> u64 {
126    900
127}
128
129pub(crate) fn default_interjection_lookback_mins() -> u64 {
130    10
131}
132
133/// "Inner thoughts" interjection: in an active group chat where the bot was NOT
134/// addressed, occasionally evaluate whether it has a genuinely valuable thing to
135/// say and, if so, chime in once. OFF by default — it speaks unprompted in
136/// group conversations and uses an LLM call per evaluation.
137#[derive(Clone, Debug, Serialize, Deserialize)]
138pub struct InterjectionConfig {
139    #[serde(default)]
140    pub enabled: bool,
141    /// Minimum seconds between unprompted interjections per chat.
142    #[serde(default = "default_interjection_min_interval_secs")]
143    pub min_interval_secs: u64,
144    /// Only consider chats with non-bot messages in the last this-many minutes.
145    #[serde(default = "default_interjection_lookback_mins")]
146    pub lookback_mins: u64,
147}
148
149impl Default for InterjectionConfig {
150    fn default() -> Self {
151        Self {
152            enabled: false,
153            min_interval_secs: default_interjection_min_interval_secs(),
154            lookback_mins: default_interjection_lookback_mins(),
155        }
156    }
157}