use std::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::app::events::BackgroundKind;
use crate::entities::profile::ToolId;
use crate::entities::sampling::SamplingConfig;
use crate::entities::self_model::SelfModelParams;
use crate::features::tools::{notes, self_model};
use crate::shared::api::{ApiMessage, ChatRequest};
use super::Orchestrator;
use super::request::last_user_message_at;
use super::tool_loop;
const SELF_CONSOLIDATE_MAX_TOKENS: usize = 2048;
const SELF_CONSOLIDATE_MAX_ROUNDS: u32 = 8;
const SELF_CONSOLIDATE_TIMEOUT: Duration = Duration::from_secs(180);
const SELF_CONSOLIDATE_TOOL_IDS: &[&str] = &[
self_model::GET_SELF_MODEL_ID,
self_model::UPDATE_SELF_MODEL_ID,
self_model::UPDATE_USER_MODEL_ID,
notes::NOTE_REVISE_ID,
notes::NOTE_SUPERSEDE_ID,
notes::NOTE_MERGE_ID,
notes::NOTE_LINK_ID,
notes::NOTE_NEIGHBORS_ID,
];
fn self_consolidate_system_message(loc: &crate::shared::i18n::Locale) -> String {
loc.tf(
"prompt.self_consolidate.system",
&[("core", self_model::policy_core(loc))],
)
}
impl Orchestrator {
pub(super) fn maybe_auto_self_consolidate(&mut self, chat_id: uuid::Uuid) {
let every = self.config.self_model.auto_consolidate_every;
if every == 0 {
return;
}
let profile_id;
let lang; let system_message;
let last_user;
let allowed: Vec<ToolId>;
{
let Some(chat) = self.chats.iter().find(|c| c.id == chat_id) else {
return;
};
profile_id = chat.profile_id;
let Some(profile) = self.profiles.iter().find(|p| p.id == profile_id) else {
return;
};
lang = profile.language;
if !profile
.enabled_tools
.iter()
.any(|t| t == self_model::GET_SELF_MODEL_ID)
{
return;
}
allowed = SELF_CONSOLIDATE_TOOL_IDS
.iter()
.filter(|id| profile.enabled_tools.iter().any(|t| t == **id))
.map(ToString::to_string)
.collect();
system_message = chat.system_message.clone();
last_user = last_user_message_at(chat);
}
{
let count = self.self_consolidate_counts.entry(chat_id).or_insert(0);
*count += 1;
if !tool_loop::due(*count, every) {
return;
}
}
if self.bg_running(BackgroundKind::SelfConsolidation) {
return; }
let loc = crate::shared::i18n::locale(lang);
let params = SelfModelParams::from_settings(&self.config.self_model);
let obs_count = self
.storage
.db()
.note_list(profile_id, None, &[notes::SELF_NOTE_TAG.to_string()], None)
.unwrap_or_default()
.len();
let summary_hint = self
.storage
.db()
.self_model_get(profile_id)
.ok()
.flatten()
.and_then(|m| m.summary_fill_hint(params.summary_target_chars, loc));
if obs_count < 2 && summary_hint.is_none() {
return;
}
let Ok(backend) = self.engines.backend_if_ready(self.ui_locale()) else {
return;
};
let count = self.self_consolidate_counts.insert(chat_id, 0).unwrap_or(0);
let window = Some(super::background::Window::Counter {
chat: chat_id,
count,
});
let overview = notes::build_self_consolidation_overview(&self.storage, profile_id, loc);
let digest = match (overview, summary_hint) {
(Some(o), Some(h)) => format!("{o}\n\n{h}"),
(Some(o), None) => o,
(None, Some(h)) => h,
(None, None) => return,
};
let cancel = CancellationToken::new();
let sessions = self.session_budget();
let ctx = self.background_tool_ctx(
backend.clone(),
sessions,
profile_id,
chat_id,
system_message,
last_user,
lang,
cancel.clone(),
);
let sampling = SamplingConfig {
max_tokens: Some(SELF_CONSOLIDATE_MAX_TOKENS),
temperature: Some(0.3),
..Default::default()
};
let request = ChatRequest {
continue_final: false,
system: Some(self_consolidate_system_message(loc)),
messages: vec![ApiMessage::user(digest)],
sampling,
tools: self.registry.schemas_for(&allowed, loc),
};
let acted = std::sync::Arc::new(super::background::Acted::default());
tool_loop::spawn_silent_loop(tool_loop::SilentLoop {
backend,
registry: self.registry.clone(),
ctx,
request,
allowed,
cancel: cancel.clone(),
max_rounds: SELF_CONSOLIDATE_MAX_ROUNDS,
timeout: SELF_CONSOLIDATE_TIMEOUT,
label: "auto self-consolidation",
profile_id,
kind: BackgroundKind::SelfConsolidation,
done_tx: self.bg_done_tx.clone(),
acted: acted.clone(),
summary_semantics: Some(tool_loop::SummarySemantics {
embedder: self.engines.embedder(),
storage: self.storage.clone(),
profile_id,
loc,
}),
});
self.begin_bg(
BackgroundKind::SelfConsolidation,
cancel,
window.map(|window| super::background::Refund { window, acted }),
);
}
}
#[cfg(test)]
mod tests {
use super::self_consolidate_system_message;
fn ru() -> &'static crate::shared::i18n::Locale {
crate::shared::i18n::locale(crate::shared::i18n::Lang::Ru)
}
#[test]
fn self_consolidate_system_message_composes_from_policy_core() {
let msg = self_consolidate_system_message(ru());
assert!(msg.contains(crate::features::tools::self_model::policy_core(ru())));
assert!(msg.contains("get_self_model"));
assert!(msg.contains("note_merge"));
assert!(msg.contains("update_self_model"));
}
#[test]
fn self_consolidate_system_message_localized_for_all_langs() {
for &lang in crate::shared::i18n::Lang::ALL {
let l = crate::shared::i18n::locale(lang);
let msg = self_consolidate_system_message(l);
assert!(
msg.contains(crate::features::tools::self_model::policy_core(l)),
"{lang:?}: policy_core is not embedded"
);
assert!(
!msg.contains("{core}"),
"{lang:?}: the placeholder wasn't substituted"
);
assert!(
msg.contains("get_self_model") && msg.contains("note_merge"),
"{lang:?}"
);
}
}
#[test]
fn self_consolidate_tools_cover_observations_and_summary() {
use super::SELF_CONSOLIDATE_TOOL_IDS;
use crate::features::tools::{notes, self_model};
assert!(SELF_CONSOLIDATE_TOOL_IDS.contains(¬es::NOTE_MERGE_ID));
assert!(SELF_CONSOLIDATE_TOOL_IDS.contains(¬es::NOTE_LINK_ID));
assert!(SELF_CONSOLIDATE_TOOL_IDS.contains(&self_model::UPDATE_SELF_MODEL_ID));
assert!(!SELF_CONSOLIDATE_TOOL_IDS.contains(¬es::NOTE_RECALL_ID));
}
}