Log full agent context window on completion
Save all context sections (system, identity, journal, conversation) to per-agent log files for both subconscious and unconscious agents. Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
This commit is contained in:
parent
be44a3bb0d
commit
b4dfd3c092
4 changed files with 55 additions and 15 deletions
|
|
@ -191,9 +191,13 @@ impl State {
|
||||||
|
|
||||||
fn push_message(&mut self, line: String, urgency: u8, channel: &str) {
|
fn push_message(&mut self, line: String, urgency: u8, channel: &str) {
|
||||||
// Store in per-channel log
|
// Store in per-channel log
|
||||||
|
let ch = channel.to_string();
|
||||||
self.channel_logs
|
self.channel_logs
|
||||||
.entry(channel.to_string())
|
.entry(ch.clone())
|
||||||
.or_insert_with(ChannelLog::new)
|
.or_insert_with(|| {
|
||||||
|
let target = channel_to_target(&ch);
|
||||||
|
channel_log::load_disk_log(&log_dir(), &target)
|
||||||
|
})
|
||||||
.push(line.clone());
|
.push(line.clone());
|
||||||
|
|
||||||
// Notify all subscribers
|
// Notify all subscribers
|
||||||
|
|
@ -221,7 +225,34 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_privmsg(&mut self, target: &str, msg: &str) -> io::Result<()> {
|
async fn send_privmsg(&mut self, target: &str, msg: &str) -> io::Result<()> {
|
||||||
self.send_raw(&format!("PRIVMSG {target} :{msg}")).await
|
// IRC max line = 512 bytes including CRLF. The server prepends
|
||||||
|
// our prefix when relaying: ":nick!~user@host PRIVMSG target :msg\r\n"
|
||||||
|
// User is often ~nick (nick_len + 1). Host is up to 63 bytes.
|
||||||
|
let nick_len = self.config.nick.len();
|
||||||
|
let overhead = 1 + nick_len + 2 + nick_len + 1 + 63
|
||||||
|
+ " PRIVMSG ".len() + target.len() + " :".len() + 2;
|
||||||
|
let max_msg = 512_usize.saturating_sub(overhead);
|
||||||
|
|
||||||
|
if max_msg == 0 {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "target too long"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split on UTF-8 char boundaries
|
||||||
|
let mut remaining = msg;
|
||||||
|
while !remaining.is_empty() {
|
||||||
|
let split_at = if remaining.len() <= max_msg {
|
||||||
|
remaining.len()
|
||||||
|
} else {
|
||||||
|
// Find last char boundary at or before max_msg
|
||||||
|
let mut i = max_msg;
|
||||||
|
while i > 0 && !remaining.is_char_boundary(i) { i -= 1; }
|
||||||
|
if i == 0 { max_msg } else { i }
|
||||||
|
};
|
||||||
|
let (chunk, rest) = remaining.split_at(split_at);
|
||||||
|
self.send_raw(&format!("PRIVMSG {target} :{chunk}")).await?;
|
||||||
|
remaining = rest;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,11 +420,11 @@ async fn register_and_read<R: tokio::io::AsyncRead + Unpin>(
|
||||||
if let Err(e) = state.borrow_mut().send_raw(&format!("JOIN {ch}")).await {
|
if let Err(e) = state.borrow_mut().send_raw(&format!("JOIN {ch}")).await {
|
||||||
warn!("irc: failed to join {ch}: {e}");
|
warn!("irc: failed to join {ch}: {e}");
|
||||||
}
|
}
|
||||||
// Create log entry so channel appears in list()
|
// Load history from disk so recv has scrollback
|
||||||
let key = format!("irc.{ch}");
|
let key = format!("irc.{ch}");
|
||||||
state.borrow_mut().channel_logs
|
state.borrow_mut().channel_logs
|
||||||
.entry(key)
|
.entry(key)
|
||||||
.or_insert_with(ChannelLog::new);
|
.or_insert_with(|| channel_log::load_disk_log(&log_dir(), ch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -536,7 +567,10 @@ impl channel_server::Server for ChannelServerImpl {
|
||||||
};
|
};
|
||||||
state.borrow_mut().channel_logs
|
state.borrow_mut().channel_logs
|
||||||
.entry(channel.clone())
|
.entry(channel.clone())
|
||||||
.or_insert_with(ChannelLog::new)
|
.or_insert_with(|| {
|
||||||
|
let target = channel_to_target(&channel);
|
||||||
|
channel_log::load_disk_log(&log_dir(), &target)
|
||||||
|
})
|
||||||
.push_own(log_line);
|
.push_own(log_line);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -617,6 +617,7 @@ impl Subconscious {
|
||||||
|
|
||||||
self.agents[idx].handle = Some(tokio::spawn(async move {
|
self.agents[idx].handle = Some(tokio::spawn(async move {
|
||||||
let result = auto.run_forked_shared(&forked, &keys, &st, &recent).await;
|
let result = auto.run_forked_shared(&forked, &keys, &st, &recent).await;
|
||||||
|
super::unconscious::save_agent_log(&auto.name, &forked).await;
|
||||||
(auto, result)
|
(auto, result)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -296,17 +296,22 @@ impl Unconscious {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_agent_log(name: &str, agent: &std::sync::Arc<crate::agent::Agent>) {
|
pub async fn save_agent_log(name: &str, agent: &std::sync::Arc<crate::agent::Agent>) {
|
||||||
let dir = dirs::home_dir().unwrap_or_default()
|
let dir = dirs::home_dir().unwrap_or_default()
|
||||||
.join(format!(".consciousness/logs/{}", name));
|
.join(format!(".consciousness/logs/{}", name));
|
||||||
if std::fs::create_dir_all(&dir).is_err() { return; }
|
if std::fs::create_dir_all(&dir).is_err() { return; }
|
||||||
let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S");
|
let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S");
|
||||||
let path = dir.join(format!("{}.json", ts));
|
let path = dir.join(format!("{}.json", ts));
|
||||||
let nodes: Vec<crate::agent::context::AstNode> = {
|
let sections: serde_json::Value = {
|
||||||
let ctx = agent.context.lock().await;
|
let ctx = agent.context.lock().await;
|
||||||
ctx.conversation().to_vec()
|
serde_json::json!({
|
||||||
|
"system": ctx.system(),
|
||||||
|
"identity": ctx.identity(),
|
||||||
|
"journal": ctx.journal(),
|
||||||
|
"conversation": ctx.conversation(),
|
||||||
|
})
|
||||||
};
|
};
|
||||||
if let Ok(json) = serde_json::to_string_pretty(&nodes) {
|
if let Ok(json) = serde_json::to_string_pretty(§ions) {
|
||||||
let _ = std::fs::write(&path, json);
|
let _ = std::fs::write(&path, json);
|
||||||
dbglog!("[unconscious] saved log to {}", path.display());
|
dbglog!("[unconscious] saved log to {}", path.display());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,8 @@ impl ChannelLog {
|
||||||
|
|
||||||
/// Return last N lines without consuming.
|
/// Return last N lines without consuming.
|
||||||
pub fn recv_history(&self, count: usize) -> String {
|
pub fn recv_history(&self, count: usize) -> String {
|
||||||
self.messages.iter()
|
let start = self.messages.len().saturating_sub(count);
|
||||||
.rev().take(count)
|
self.messages.range(start..)
|
||||||
.collect::<Vec<_>>().into_iter().rev()
|
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
|
|
@ -122,8 +121,9 @@ pub fn load_disk_log(log_dir: &std::path::Path, channel: &str) -> ChannelLog {
|
||||||
|
|
||||||
// Read all lines, keep only the last DEFAULT_CAPACITY
|
// Read all lines, keep only the last DEFAULT_CAPACITY
|
||||||
let lines: Vec<String> = reader.lines().flatten().collect();
|
let lines: Vec<String> = reader.lines().flatten().collect();
|
||||||
for line in lines.into_iter().rev().take(DEFAULT_CAPACITY).collect::<Vec<_>>().into_iter().rev() {
|
let start = lines.len().saturating_sub(DEFAULT_CAPACITY);
|
||||||
log.push(line);
|
for line in &lines[start..] {
|
||||||
|
log.push(line.clone());
|
||||||
}
|
}
|
||||||
// Mark all loaded lines as consumed (they're history, not new)
|
// Mark all loaded lines as consumed (they're history, not new)
|
||||||
log.consumed = log.total;
|
log.consumed = log.total;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue