Move poc-agent into workspace, improve agent prompts

Move poc-agent (substrate-independent AI agent framework) into the
memory workspace as a step toward using its API client for direct
LLM calls instead of shelling out to claude CLI.

Agent prompt improvements:
- distill: rewrite from hub-focused to knowledge-flow-focused.
  Now walks upward from seed nodes to find and refine topic nodes,
  instead of only maintaining high-degree hubs.
- distill: remove "don't touch journal entries" restriction
- memory-instructions-core: add "Make it alive" section — write
  with creativity and emotional texture, not spreadsheet summaries
- memory-instructions-core: add "Show your reasoning" section —
  agents must explain decisions, especially when they do nothing
- linker: already had emotional texture guidance (kept as-is)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-18 22:44:52 -04:00
parent 0a62832fe3
commit 57fcfb472a
89 changed files with 16389 additions and 51 deletions

126
poc-agent/src/log.rs Normal file
View file

@ -0,0 +1,126 @@
// log.rs — Persistent conversation log
//
// Append-only JSONL file that records every message in the conversation.
// This is the permanent record — never truncated, never compacted.
// The in-memory message array is a view into this log; compaction
// builds that view by mixing raw recent messages with journal
// summaries of older ones.
//
// Each line is a JSON-serialized Message with its timestamp.
// The log survives session restarts, compactions, and crashes.
use anyhow::{Context, Result};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::types::Message;
pub struct ConversationLog {
path: PathBuf,
}
impl ConversationLog {
pub fn new(path: PathBuf) -> Result<Self> {
// Ensure parent directory exists
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating log dir {}", parent.display()))?;
}
Ok(Self { path })
}
/// Append a single message to the log.
pub fn append(&self, msg: &Message) -> Result<()> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.with_context(|| format!("opening log {}", self.path.display()))?;
let line = serde_json::to_string(msg)
.context("serializing message for log")?;
writeln!(file, "{}", line)
.context("writing to conversation log")?;
Ok(())
}
/// Read the tail of the log (last `max_bytes` bytes).
/// Seeks to `file_len - max_bytes`, skips the first partial line,
/// then parses forward. For logs smaller than `max_bytes`, reads everything.
pub fn read_tail(&self, max_bytes: u64) -> Result<Vec<Message>> {
if !self.path.exists() {
return Ok(Vec::new());
}
let file = File::open(&self.path)
.with_context(|| format!("opening log {}", self.path.display()))?;
let file_len = file.metadata()?.len();
let mut reader = BufReader::new(file);
if file_len > max_bytes {
reader.seek(SeekFrom::Start(file_len - max_bytes))?;
// Skip partial first line
let mut discard = String::new();
reader.read_line(&mut discard)?;
}
let mut messages = Vec::new();
for line in reader.lines() {
let line = line.context("reading log tail")?;
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<Message>(line) {
Ok(msg) => messages.push(msg),
Err(_) => {} // skip corrupt/partial lines
}
}
Ok(messages)
}
/// Count messages in the log without loading content.
pub fn message_count(&self) -> Result<usize> {
if !self.path.exists() {
return Ok(0);
}
let file = File::open(&self.path)
.with_context(|| format!("opening log {}", self.path.display()))?;
let reader = BufReader::new(file);
Ok(reader.lines()
.filter(|l| l.as_ref().map_or(false, |s| !s.trim().is_empty()))
.count())
}
/// Read all messages from the log. Returns empty vec if log doesn't exist.
/// NOTE: Don't use this in hot paths — use read_tail() instead.
pub fn read_all(&self) -> Result<Vec<Message>> {
if !self.path.exists() {
return Ok(Vec::new());
}
let file = File::open(&self.path)
.with_context(|| format!("opening log {}", self.path.display()))?;
let reader = BufReader::new(file);
let mut messages = Vec::new();
for (i, line) in reader.lines().enumerate() {
let line = line.with_context(|| format!("reading log line {}", i))?;
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<Message>(line) {
Ok(msg) => messages.push(msg),
Err(e) => {
// Log corruption — skip bad lines rather than failing
eprintln!("warning: skipping corrupt log line {}: {}", i, e);
}
}
}
Ok(messages)
}
pub fn path(&self) -> &Path {
&self.path
}
}