rename agent: LLM-powered semantic key generation for memory nodes
New consolidation agent that reads node content and generates semantic 3-5 word kebab-case keys, replacing auto-generated slugs (5K+ journal entries with truncated first-line slugs, 2.5K mined transcripts with opaque UUIDs). Implementation: - prompts/rename.md: agent prompt template with naming conventions - prompts.rs: format_rename_candidates() selects nodes with long auto-generated keys, newest first - daemon.rs: job_rename_agent() parses RENAME actions from LLM output and applies them directly via store.rename_node() - Wired into RPC handler (run-agent rename) and TUI agent types - Fix epoch_to_local panic on invalid timestamps (fallback to UTC) Rename dramatically improves search: key-component matching on "journal#2026-02-28-violin-dream-room" makes the node findable by "violin", "dream", or "room" — the auto-slug was unsearchable.
This commit is contained in:
parent
ef760f0053
commit
4c973183c4
5 changed files with 219 additions and 5 deletions
|
|
@ -153,6 +153,86 @@ fn job_consolidation_agent(
|
|||
})
|
||||
}
|
||||
|
||||
/// Run the rename agent: generates renames via LLM, applies them directly.
|
||||
fn job_rename_agent(
|
||||
ctx: &ExecutionContext,
|
||||
batch_size: usize,
|
||||
) -> Result<(), TaskError> {
|
||||
run_job(ctx, "c-rename", || {
|
||||
ctx.log_line("loading store");
|
||||
let mut store = crate::store::Store::load()?;
|
||||
|
||||
let batch = if batch_size == 0 { 10 } else { batch_size };
|
||||
ctx.log_line(&format!("building prompt: rename (batch={})", batch));
|
||||
|
||||
let prompt = super::prompts::agent_prompt(&store, "rename", batch)?;
|
||||
ctx.log_line(&format!("prompt: {} chars, calling Sonnet", prompt.len()));
|
||||
|
||||
let response = super::llm::call_sonnet("consolidate", &prompt)?;
|
||||
|
||||
// Parse RENAME actions directly from response
|
||||
let mut applied = 0;
|
||||
let mut skipped = 0;
|
||||
for line in response.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.starts_with("RENAME ") { continue; }
|
||||
|
||||
let rest = &trimmed[7..];
|
||||
// Split on first space after the old key — tricky because keys contain spaces? No, they don't.
|
||||
// Keys are single tokens with hyphens/underscores/hashes.
|
||||
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
|
||||
if parts.len() != 2 { skipped += 1; continue; }
|
||||
|
||||
let old_key = parts[0].trim();
|
||||
let new_key = parts[1].trim();
|
||||
|
||||
if old_key.is_empty() || new_key.is_empty() { skipped += 1; continue; }
|
||||
|
||||
// Resolve old key (handles partial matches)
|
||||
let resolved = match store.resolve_key(old_key) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
ctx.log_line(&format!("skip: {} → {}: {}", old_key, new_key, e));
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Don't rename to something that already exists
|
||||
if store.nodes.contains_key(new_key) {
|
||||
ctx.log_line(&format!("skip: {} already exists", new_key));
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
match store.rename_node(&resolved, new_key) {
|
||||
Ok(()) => {
|
||||
ctx.log_line(&format!("renamed: {} → {}", resolved, new_key));
|
||||
applied += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.log_line(&format!("error: {} → {}: {}", resolved, new_key, e));
|
||||
skipped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if applied > 0 {
|
||||
store.save()?;
|
||||
}
|
||||
|
||||
// Also store the report for auditing
|
||||
let ts = crate::store::format_datetime(crate::store::now_epoch())
|
||||
.replace([':', '-', 'T'], "");
|
||||
let report_key = format!("_consolidation-rename-{}", ts);
|
||||
store.upsert_provenance(&report_key, &response,
|
||||
crate::store::Provenance::AgentConsolidate).ok();
|
||||
|
||||
ctx.log_line(&format!("done: {} applied, {} skipped", applied, skipped));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply consolidation actions from recent reports.
|
||||
fn job_consolidation_apply(ctx: &ExecutionContext) -> Result<(), TaskError> {
|
||||
run_job(ctx, "c-apply", || {
|
||||
|
|
@ -1062,6 +1142,7 @@ fn status_socket_loop(
|
|||
let mut spawned = 0;
|
||||
let mut remaining = count;
|
||||
|
||||
let is_rename = *agent_type == "rename";
|
||||
while remaining > 0 {
|
||||
let batch = remaining.min(batch_size);
|
||||
let agent = agent_type.to_string();
|
||||
|
|
@ -1070,7 +1151,11 @@ fn status_socket_loop(
|
|||
.resource(llm)
|
||||
.retries(1)
|
||||
.init(move |ctx| {
|
||||
job_consolidation_agent(ctx, &agent, batch)
|
||||
if is_rename {
|
||||
job_rename_agent(ctx, batch)
|
||||
} else {
|
||||
job_consolidation_agent(ctx, &agent, batch)
|
||||
}
|
||||
});
|
||||
if let Some(ref dep) = prev {
|
||||
builder.depend_on(dep);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue