//! Opted-in tool-output summaries (M5d spec, section 4.3). use crate::baseline::messages_with_carry; use crate::compact::cap_chars; use crate::config::Config; use crate::llama::{ChatMessage, ChatRequest, Client}; use crate::session::Session; use bxcore::sanitize::neutralise; use bxcore::{CallId, DataClass, LogRecord, Timestamp, ToolCall}; use std::collections::{HashMap, HashSet}; /// Append a `ToolSummary` for each opted-in `ToolResult` in this epoch that does not yet have /// one. Inference uses the background slot and no tools. pub fn missing(cfg: &Config, client: &Client, session: &mut Session) -> Result<(), String> { let path = cfg.baseline.system.with_file_name("summarise.md"); let prompt = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?; let mut names: HashMap = HashMap::new(); let mut done: HashSet = HashSet::new(); let mut pending: Vec<(CallId, String, String, DataClass, bool)> = Vec::new(); for record in session.records() { match record { LogRecord::Assistant { tool_calls, .. } => { for ToolCall { id, name, .. } in tool_calls { names.insert(id.clone(), name.clone()); } } LogRecord::ToolSummary { call, .. } => { done.insert(call.0); } LogRecord::ToolResult { call, tool_call_id, content, class, untrusted, .. } => { let Some(name) = names.get(tool_call_id) else { continue; }; if !cfg.compact.summarise.iter().any(|n| n == name) { continue; } pending.push(( *call, tool_call_id.clone(), content.clone(), *class, *untrusted, )); } _ => {} } } pending.retain(|(call, ..)| !done.contains(&call.0)); for (call, tool_call_id, content, class, untrusted) in pending { let mut messages = messages_with_carry(session.baseline(), session.records(), session.carry()); messages.push(ChatMessage::User { content: format!( "{}\n\n{}", prompt.trim_end(), cap_chars(&content, cfg.r#loop.tool_result_cap) ), }); let request = ChatRequest { slot: cfg.slots.background, messages, tools: Vec::new(), thinking: true, }; let text = match client.chat_with_retry(&request, &mut |_| {}) { Ok(completion) => completion.content.unwrap_or_default(), Err(e) => { eprintln!("loopd: tool summary: {e}"); continue; } }; let text = cap_chars(neutralise(&text).as_ref(), cfg.compact.summary_chars); session .append(LogRecord::ToolSummary { time: Timestamp::now(), call, tool_call_id, content: text, class, untrusted, }) .map_err(|e| e.to_string())?; } Ok(()) }