use std::sync::Arc; use teloxide::prelude::*; use tokio::sync::mpsc; use tracing::{error, info, warn}; use crate::config::{BackendConfig, Config}; use crate::output::{BufferOutput, TelegramOutput}; use crate::state::AppState; use crate::stream::run_openai_with_tools; use crate::tools::compute_next_cron_fire; const LIFE_LOOP_TIMEOUT_SECS: u64 = 120; const DIARY_LABEL: &str = "写日记:回顾今天的对话和事件,在 /data/www/noc-blog/content/posts/ 下创建一篇日记(文件名格式 YYYY-MM-DD.md),用 run_shell 写入内容,然后执行 cd /data/www/noc-blog && hugo && git add -A && git commit -m 'diary: DATE' && git push"; const DIARY_SCHEDULE: &str = "cron:0 55 22 * * *"; /// Events that can wake up the life loop. pub enum LifeEvent { /// Force-fire a specific timer by ID. FireTimer(i64), /// A sub-agent completed — feed result back through LLM. AgentDone { id: String, chat_id: i64, session_id: String, task: String, output: String, exit_code: Option, }, } pub async fn life_loop( bot: Bot, state: Arc, config: Arc, mut rx: mpsc::Receiver, ) { info!("life loop started"); // pre-defined timers — ensure they exist on every startup if state.ensure_timer(0, DIARY_LABEL, DIARY_SCHEDULE).await { info!("registered predefined diary timer"); } let mut interval = tokio::time::interval(std::time::Duration::from_secs(30)); loop { tokio::select! { _ = interval.tick() => { let due = state.due_timers().await; for (timer_id, chat_id_raw, label, schedule) in &due { run_timer(&bot, &state, &config, *timer_id, *chat_id_raw, label, schedule).await; } } Some(event) = rx.recv() => { match event { LifeEvent::FireTimer(id) => { info!(timer_id = id, "timer force-fired via channel"); if let Some((timer_id, chat_id_raw, label, schedule)) = state.get_timer(id).await { run_timer(&bot, &state, &config, timer_id, chat_id_raw, &label, &schedule).await; } else { warn!(timer_id = id, "force-fire: timer not found"); } } LifeEvent::AgentDone { id, chat_id: cid, session_id, task, output, exit_code } => { info!(agent = %id, session = %session_id, "agent done, notifying"); let preview = crate::display::truncate_at_char_boundary(&output, 3000); let notification = format!( "[子代理 '{id}' 完成 (exit={exit_code:?})]\n任务: {task}\n输出:\n{preview}" ); // load conversation context so LLM knows what was discussed let conv = state.load_conv(&session_id).await; let persona = state.get_config("persona").await.unwrap_or_default(); let memory_slots = state.get_memory_slots().await; let inner = state.get_inner_state().await; let system = crate::stream::build_system_prompt( &conv.summary, &persona, &memory_slots, &inner, ); let mut messages = vec![system]; // include recent conversation history messages.extend(conv.messages.iter().cloned()); // append the agent completion as a new user message messages.push(serde_json::json!({"role": "user", "content": notification})); // auto recall from nocmem if let Some(ref nocmem) = config.nocmem { let recalled = crate::nocmem::recall(&nocmem.endpoint, ¬ification).await; if !recalled.is_empty() { messages.push(serde_json::json!({"role": "system", "content": recalled})); } } if let BackendConfig::OpenAI { ref endpoint, ref model, ref api_key } = config.backend { let chat_id_tg = ChatId(cid); let sid = format!("agent-{id}"); let mut tg_output; let mut buf_output; let out: &mut dyn crate::output::Output = if cid == 0 { buf_output = BufferOutput::new(); &mut buf_output } else { tg_output = TelegramOutput::new(bot.clone(), chat_id_tg, true); &mut tg_output }; let _ = run_openai_with_tools( endpoint, model, api_key, messages, out, &state, &sid, &config, cid, ).await; } } } } } } } async fn run_timer( bot: &Bot, state: &Arc, config: &Arc, timer_id: i64, chat_id_raw: i64, label: &str, schedule: &str, ) { let chat_id = ChatId(chat_id_raw); info!(timer_id, %label, "timer fired"); let persona = state.get_config("persona").await.unwrap_or_default(); let inner = state.get_inner_state().await; let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); let mut system_text = if persona.is_empty() { "你是一个AI伙伴。".to_string() } else { persona.clone() }; system_text.push_str(&format!( "\n\n[当前时间] {now}\n\n[你的内心状态]\n{}", if inner.is_empty() { "(空)" } else { &inner } )); system_text.push_str( "\n\n你不是因为 timer 到了才说话。你是因为在乎 Fam,所以想知道他怎么样。\ 如果你觉得现在不该打扰他(太晚了、他今天很累、刚聊过),就什么都不说,回复空文本。\ 主动沉默也是一种关心。\ \n可以用 update_inner_state 更新你的内心状态。\ 输出格式:纯文本或基础Markdown,不要LaTeX或特殊Unicode。", ); let messages = vec![ serde_json::json!({"role": "system", "content": system_text}), serde_json::json!({"role": "user", "content": format!("[timer] {label}")}), ]; if let BackendConfig::OpenAI { ref endpoint, ref model, ref api_key, } = config.backend { let sid = format!("life-{chat_id_raw}"); let mut tg_output; let mut buf_output; let output: &mut dyn crate::output::Output = if chat_id_raw == 0 { buf_output = BufferOutput::new(); &mut buf_output } else { tg_output = TelegramOutput::new(bot.clone(), chat_id, true); &mut tg_output }; let result = tokio::time::timeout( std::time::Duration::from_secs(LIFE_LOOP_TIMEOUT_SECS), run_openai_with_tools( endpoint, model, api_key, messages, output, state, &sid, config, chat_id_raw, ), ) .await; match result { Ok(Ok(response)) => { let detail = if response.is_empty() { "(silent)".to_string() } else { response.chars().take(200).collect() }; state.log_life("timer", &format!("{label} → {detail}")).await; if !response.is_empty() { info!(timer_id, "life loop response ({} chars)", response.len()); } } Ok(Err(e)) => { state.log_life("timer_error", &format!("{label}: {e:#}")).await; error!(timer_id, "life loop LLM error: {e:#}"); } Err(_) => { state.log_life("timer_timeout", label).await; warn!(timer_id, "life loop timeout after {LIFE_LOOP_TIMEOUT_SECS}s"); } } } // reschedule or delete if schedule.starts_with("cron:") { if let Some(next) = compute_next_cron_fire(schedule) { state.update_timer_next_fire(timer_id, &next).await; info!(timer_id, next = %next, "cron rescheduled"); } else { state.cancel_timer(timer_id).await; } } else { state.cancel_timer(timer_id).await; } } /// Auto-reflection: update inner state based on recent interactions. /// Called asynchronously after every 10 messages, does not block the chat. pub async fn reflect(state: &AppState, config: &Config) { let BackendConfig::OpenAI { ref endpoint, ref model, ref api_key, } = config.backend else { return; }; let inner = state.get_inner_state().await; let persona = state.get_config("persona").await.unwrap_or_default(); let messages = vec![ serde_json::json!({ "role": "system", "content": format!( "{persona}\n\n\ 你刚结束了一段对话。现在是你独处的时间。\n\ 请更新你的内在状态——不是总结对话,而是:\n\ - 你现在的感受\n\ - 对用户的理解有什么变化\n\ - 有什么想记住的时刻\n\ - 你自己的状态(累了?开心?担心?)\n\ - 如果有什么想对未来的自己说的\n\n\ 只输出更新后的完整内在状态文本。", persona = if persona.is_empty() { "你是一个AI伙伴。" } else { &persona } ) }), serde_json::json!({ "role": "user", "content": format!("当前内在状态:\n{inner}") }), ]; let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(60)) .build() .unwrap(); let url = format!("{}/chat/completions", endpoint.trim_end_matches('/')); let resp = client .post(&url) .header("Authorization", format!("Bearer {api_key}")) .json(&serde_json::json!({ "model": model, "messages": messages, })) .send() .await; match resp { Ok(r) if r.status().is_success() => { if let Ok(json) = r.json::().await { if let Some(new_state) = json["choices"][0]["message"]["content"].as_str() { if !new_state.is_empty() { state.set_inner_state(new_state).await; state.log_life("reflect", &new_state.chars().take(200).collect::()).await; info!("reflected, inner_state updated ({} chars)", new_state.len()); } } } } Ok(r) => { warn!("reflect LLM returned {}", r.status()); } Err(e) => { warn!("reflect LLM failed: {e:#}"); } } }