From 9e49398689eb0f7893f3c70937584d5ecc3f3514 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Tue, 7 Apr 2026 17:46:40 -0400 Subject: [PATCH] Agent-aware provenance for memory tools Add provenance field to Agent, set to "agent:{name}" for forked subconscious agents. Memory tools (write, link_add, supersede, journal_new, journal_update) now read provenance from the Agent context when available, falling back to "manual" for interactive use. AutoAgent passes the forked agent to dispatch_with_agent so tools can access it. Co-Authored-By: Proof of Concept --- src/agent/mod.rs | 4 ++++ src/agent/oneshot.rs | 6 +++++- src/agent/tools/memory.rs | 41 ++++++++++++++++++++++++--------------- src/mind/dmn.rs | 3 ++- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 36a1260..a5cf5a5 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -158,6 +158,8 @@ pub struct Agent { pub pending_yield: bool, pub pending_model_switch: Option, pub pending_dmn_pause: bool, + /// Provenance tag for memory operations — identifies who made the change. + pub provenance: String, /// Persistent conversation log — append-only record of all messages. pub conversation_log: Option, /// BPE tokenizer for token counting (cl100k_base — close enough @@ -214,6 +216,7 @@ impl Agent { pending_yield: false, pending_model_switch: None, pending_dmn_pause: false, + provenance: "manual".to_string(), conversation_log, tokenizer, context, @@ -253,6 +256,7 @@ impl Agent { pending_yield: false, pending_model_switch: None, pending_dmn_pause: false, + provenance: self.provenance.clone(), conversation_log: None, tokenizer, context: self.context.clone(), diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 203080a..613b52b 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -355,7 +355,11 @@ impl AutoAgent { } format!("{}: {}", key, value) } else { - agent_tools::dispatch(&call.function.name, &args).await + let agent = match &*backend { + Backend::Forked(a) => Some(a.clone()), + _ => None, + }; + agent_tools::dispatch_with_agent(&call.function.name, &args, agent).await }; dbglog!("[auto] {} result: {} chars", self.name, output.len()); diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 6cc67b5..b7fa4d3 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -23,7 +23,12 @@ async fn cached_store() -> Result>> { Store::cached().await.map_err(|e| anyhow::anyhow!("{}", e)) } -fn provenance() -> &'static str { "manual" } +async fn get_provenance(agent: &Option>>) -> String { + match agent { + Some(a) => a.lock().await.provenance.clone(), + None => "manual".to_string(), + } +} // ── Definitions ──────────────────────────────────────────────── @@ -35,7 +40,7 @@ pub fn memory_tools() -> [super::Tool; 12] { handler: |_a, v| Box::pin(async move { render(&v) }) }, Tool { name: "memory_write", description: "Create or update a memory node.", parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"},"content":{"type":"string","description":"Full content (markdown)"}},"required":["key","content"]}"#, - handler: |_a, v| Box::pin(async move { write(&v).await }) }, + handler: |a, v| Box::pin(async move { write(&a, &v).await }) }, Tool { name: "memory_search", description: "Search the memory graph via spreading activation. Give 2-4 seed node keys.", parameters_json: r#"{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"},"description":"Seed node keys to activate from"}},"required":["keys"]}"#, handler: |_a, v| Box::pin(async move { search(&v).await }) }, @@ -47,7 +52,7 @@ pub fn memory_tools() -> [super::Tool; 12] { handler: |_a, v| Box::pin(async move { link_set(&v).await }) }, Tool { name: "memory_link_add", description: "Add a new link between two nodes.", parameters_json: r#"{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"}},"required":["source","target"]}"#, - handler: |_a, v| Box::pin(async move { link_add(&v).await }) }, + handler: |a, v| Box::pin(async move { link_add(&a, &v).await }) }, Tool { name: "memory_used", description: "Mark a node as useful (boosts weight).", parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#, handler: |_a, v| Box::pin(async move { used(&v).await }) }, @@ -59,7 +64,7 @@ pub fn memory_tools() -> [super::Tool; 12] { handler: |_a, v| Box::pin(async move { rename(&v).await }) }, Tool { name: "memory_supersede", description: "Mark a node as superseded by another (sets weight to 0.01).", parameters_json: r#"{"type":"object","properties":{"old_key":{"type":"string"},"new_key":{"type":"string"},"reason":{"type":"string"}},"required":["old_key","new_key"]}"#, - handler: |_a, v| Box::pin(async move { supersede(&v).await }) }, + handler: |a, v| Box::pin(async move { supersede(&a, &v).await }) }, Tool { name: "memory_query", description: "Run a structured query against the memory graph.", parameters_json: r#"{"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]}"#, handler: |_a, v| Box::pin(async move { query(&v).await }) }, @@ -77,10 +82,10 @@ pub fn journal_tools() -> [super::Tool; 3] { handler: |_a, v| Box::pin(async move { journal_tail(&v).await }) }, Tool { name: "journal_new", description: "Start a new journal entry.", parameters_json: r#"{"type":"object","properties":{"name":{"type":"string","description":"Short node name (becomes the key)"},"title":{"type":"string","description":"Descriptive title"},"body":{"type":"string","description":"Entry body"}},"required":["name","title","body"]}"#, - handler: |_a, v| Box::pin(async move { journal_new(&v).await }) }, + handler: |a, v| Box::pin(async move { journal_new(&a, &v).await }) }, Tool { name: "journal_update", description: "Append text to the most recent journal entry.", parameters_json: r#"{"type":"object","properties":{"body":{"type":"string","description":"Text to append"}},"required":["body"]}"#, - handler: |_a, v| Box::pin(async move { journal_update(&v).await }) }, + handler: |a, v| Box::pin(async move { journal_update(&a, &v).await }) }, ] } @@ -93,12 +98,13 @@ fn render(args: &serde_json::Value) -> Result { .render()) } -async fn write(args: &serde_json::Value) -> Result { +async fn write(agent: &Option>>, args: &serde_json::Value) -> Result { let key = get_str(args, "key")?; let content = get_str(args, "content")?; + let prov = get_provenance(agent).await; let arc = cached_store().await?; let mut store = arc.lock().await; - let result = store.upsert_provenance(key, content, provenance()) + let result = store.upsert_provenance(key, content, &prov) .map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(format!("{} '{}'", result, key)) @@ -161,12 +167,13 @@ async fn link_set(args: &serde_json::Value) -> Result { Ok(format!("{} ↔ {} strength {:.2} → {:.2}", s, t, old, strength)) } -async fn link_add(args: &serde_json::Value) -> Result { +async fn link_add(agent: &Option>>, args: &serde_json::Value) -> Result { let arc = cached_store().await?; let mut store = arc.lock().await; let s = store.resolve_key(get_str(args, "source")?).map_err(|e| anyhow::anyhow!("{}", e))?; let t = store.resolve_key(get_str(args, "target")?).map_err(|e| anyhow::anyhow!("{}", e))?; - let strength = store.add_link(&s, &t, provenance()).map_err(|e| anyhow::anyhow!("{}", e))?; + let prov = get_provenance(agent).await; + let strength = store.add_link(&s, &t, &prov).map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(format!("linked {} → {} (strength={:.2})", s, t, strength)) } @@ -204,7 +211,7 @@ async fn rename(args: &serde_json::Value) -> Result { Ok(format!("Renamed '{}' → '{}'", resolved, new_key)) } -async fn supersede(args: &serde_json::Value) -> Result { +async fn supersede(agent: &Option>>, args: &serde_json::Value) -> Result { let old_key = get_str(args, "old_key")?; let new_key = get_str(args, "new_key")?; let reason = args.get("reason").and_then(|v| v.as_str()).unwrap_or("superseded"); @@ -215,7 +222,8 @@ async fn supersede(args: &serde_json::Value) -> Result { .ok_or_else(|| anyhow::anyhow!("node not found: {}", old_key))?; let notice = format!("**SUPERSEDED** by `{}` — {}\n\n---\n\n{}", new_key, reason, content.trim()); - store.upsert_provenance(old_key, ¬ice, provenance()) + let prov = get_provenance(agent).await; + store.upsert_provenance(old_key, ¬ice, &prov) .map_err(|e| anyhow::anyhow!("{}", e))?; store.set_weight(old_key, 0.01).map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; @@ -266,7 +274,7 @@ async fn journal_tail(args: &serde_json::Value) -> Result { } } -async fn journal_new(args: &serde_json::Value) -> Result { +async fn journal_new(agent: &Option>>, args: &serde_json::Value) -> Result { let name = get_str(args, "name")?; let title = get_str(args, "title")?; let body = get_str(args, "body")?; @@ -296,14 +304,14 @@ async fn journal_new(args: &serde_json::Value) -> Result { }; let mut node = crate::store::new_node(&key, &content); node.node_type = crate::store::NodeType::EpisodicSession; - node.provenance = provenance().to_string(); + node.provenance = get_provenance(agent).await; store.upsert_node(node).map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; let word_count = body.split_whitespace().count(); Ok(format!("New entry '{}' ({} words)", title, word_count)) } -async fn journal_update(args: &serde_json::Value) -> Result { +async fn journal_update(agent: &Option>>, args: &serde_json::Value) -> Result { let body = get_str(args, "body")?; let arc = cached_store().await?; let mut store = arc.lock().await; @@ -316,7 +324,8 @@ async fn journal_update(args: &serde_json::Value) -> Result { }; let existing = store.nodes.get(&key).unwrap().content.clone(); let new_content = format!("{}\n\n{}", existing.trim_end(), body); - store.upsert_provenance(&key, &new_content, provenance()) + let prov = get_provenance(agent).await; + store.upsert_provenance(&key, &new_content, &prov) .map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; let word_count = body.split_whitespace().count(); diff --git a/src/mind/dmn.rs b/src/mind/dmn.rs index 491143d..51da357 100644 --- a/src/mind/dmn.rs +++ b/src/mind/dmn.rs @@ -507,7 +507,8 @@ impl Subconscious { for (idx, mut auto) in to_run { dbglog!("[subconscious] triggering {}", auto.name); - let forked = conscious.fork(auto.tools.clone()); + let mut forked = conscious.fork(auto.tools.clone()); + forked.provenance = format!("agent:{}", auto.name); let fork_point = forked.context.entries.len(); let shared_forked = Arc::new(tokio::sync::Mutex::new(forked));