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 <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 17:46:40 -04:00
parent 74f8952399
commit 9e49398689
4 changed files with 36 additions and 18 deletions

View file

@ -158,6 +158,8 @@ pub struct Agent {
pub pending_yield: bool, pub pending_yield: bool,
pub pending_model_switch: Option<String>, pub pending_model_switch: Option<String>,
pub pending_dmn_pause: bool, 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. /// Persistent conversation log — append-only record of all messages.
pub conversation_log: Option<ConversationLog>, pub conversation_log: Option<ConversationLog>,
/// BPE tokenizer for token counting (cl100k_base — close enough /// BPE tokenizer for token counting (cl100k_base — close enough
@ -214,6 +216,7 @@ impl Agent {
pending_yield: false, pending_yield: false,
pending_model_switch: None, pending_model_switch: None,
pending_dmn_pause: false, pending_dmn_pause: false,
provenance: "manual".to_string(),
conversation_log, conversation_log,
tokenizer, tokenizer,
context, context,
@ -253,6 +256,7 @@ impl Agent {
pending_yield: false, pending_yield: false,
pending_model_switch: None, pending_model_switch: None,
pending_dmn_pause: false, pending_dmn_pause: false,
provenance: self.provenance.clone(),
conversation_log: None, conversation_log: None,
tokenizer, tokenizer,
context: self.context.clone(), context: self.context.clone(),

View file

@ -355,7 +355,11 @@ impl AutoAgent {
} }
format!("{}: {}", key, value) format!("{}: {}", key, value)
} else { } 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()); dbglog!("[auto] {} result: {} chars", self.name, output.len());

View file

@ -23,7 +23,12 @@ async fn cached_store() -> Result<std::sync::Arc<tokio::sync::Mutex<Store>>> {
Store::cached().await.map_err(|e| anyhow::anyhow!("{}", e)) Store::cached().await.map_err(|e| anyhow::anyhow!("{}", e))
} }
fn provenance() -> &'static str { "manual" } async fn get_provenance(agent: &Option<std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>>) -> String {
match agent {
Some(a) => a.lock().await.provenance.clone(),
None => "manual".to_string(),
}
}
// ── Definitions ──────────────────────────────────────────────── // ── Definitions ────────────────────────────────────────────────
@ -35,7 +40,7 @@ pub fn memory_tools() -> [super::Tool; 12] {
handler: |_a, v| Box::pin(async move { render(&v) }) }, handler: |_a, v| Box::pin(async move { render(&v) }) },
Tool { name: "memory_write", description: "Create or update a memory node.", 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"]}"#, 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.", 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"]}"#, 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 }) }, 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 }) }, handler: |_a, v| Box::pin(async move { link_set(&v).await }) },
Tool { name: "memory_link_add", description: "Add a new link between two nodes.", 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"]}"#, 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).", 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"]}"#, parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#,
handler: |_a, v| Box::pin(async move { used(&v).await }) }, 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 }) }, 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).", 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"]}"#, 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.", 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"]}"#, parameters_json: r#"{"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]}"#,
handler: |_a, v| Box::pin(async move { query(&v).await }) }, 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 }) }, handler: |_a, v| Box::pin(async move { journal_tail(&v).await }) },
Tool { name: "journal_new", description: "Start a new journal entry.", 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"]}"#, 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.", 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"]}"#, 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<String> {
.render()) .render())
} }
async fn write(args: &serde_json::Value) -> Result<String> { async fn write(agent: &Option<std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>>, args: &serde_json::Value) -> Result<String> {
let key = get_str(args, "key")?; let key = get_str(args, "key")?;
let content = get_str(args, "content")?; let content = get_str(args, "content")?;
let prov = get_provenance(agent).await;
let arc = cached_store().await?; let arc = cached_store().await?;
let mut store = arc.lock().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))?; .map_err(|e| anyhow::anyhow!("{}", e))?;
store.save().map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(format!("{} '{}'", result, key)) Ok(format!("{} '{}'", result, key))
@ -161,12 +167,13 @@ async fn link_set(args: &serde_json::Value) -> Result<String> {
Ok(format!("{}{} strength {:.2}{:.2}", s, t, old, strength)) Ok(format!("{}{} strength {:.2}{:.2}", s, t, old, strength))
} }
async fn link_add(args: &serde_json::Value) -> Result<String> { async fn link_add(agent: &Option<std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>>, args: &serde_json::Value) -> Result<String> {
let arc = cached_store().await?; let arc = cached_store().await?;
let mut store = arc.lock().await; let mut store = arc.lock().await;
let s = store.resolve_key(get_str(args, "source")?).map_err(|e| anyhow::anyhow!("{}", e))?; 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 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))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(format!("linked {}{} (strength={:.2})", s, t, strength)) Ok(format!("linked {}{} (strength={:.2})", s, t, strength))
} }
@ -204,7 +211,7 @@ async fn rename(args: &serde_json::Value) -> Result<String> {
Ok(format!("Renamed '{}' → '{}'", resolved, new_key)) Ok(format!("Renamed '{}' → '{}'", resolved, new_key))
} }
async fn supersede(args: &serde_json::Value) -> Result<String> { async fn supersede(agent: &Option<std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>>, args: &serde_json::Value) -> Result<String> {
let old_key = get_str(args, "old_key")?; let old_key = get_str(args, "old_key")?;
let new_key = get_str(args, "new_key")?; let new_key = get_str(args, "new_key")?;
let reason = args.get("reason").and_then(|v| v.as_str()).unwrap_or("superseded"); 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<String> {
.ok_or_else(|| anyhow::anyhow!("node not found: {}", old_key))?; .ok_or_else(|| anyhow::anyhow!("node not found: {}", old_key))?;
let notice = format!("**SUPERSEDED** by `{}` — {}\n\n---\n\n{}", let notice = format!("**SUPERSEDED** by `{}` — {}\n\n---\n\n{}",
new_key, reason, content.trim()); new_key, reason, content.trim());
store.upsert_provenance(old_key, &notice, provenance()) let prov = get_provenance(agent).await;
store.upsert_provenance(old_key, &notice, &prov)
.map_err(|e| anyhow::anyhow!("{}", e))?; .map_err(|e| anyhow::anyhow!("{}", e))?;
store.set_weight(old_key, 0.01).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))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
@ -266,7 +274,7 @@ async fn journal_tail(args: &serde_json::Value) -> Result<String> {
} }
} }
async fn journal_new(args: &serde_json::Value) -> Result<String> { async fn journal_new(agent: &Option<std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>>, args: &serde_json::Value) -> Result<String> {
let name = get_str(args, "name")?; let name = get_str(args, "name")?;
let title = get_str(args, "title")?; let title = get_str(args, "title")?;
let body = get_str(args, "body")?; let body = get_str(args, "body")?;
@ -296,14 +304,14 @@ async fn journal_new(args: &serde_json::Value) -> Result<String> {
}; };
let mut node = crate::store::new_node(&key, &content); let mut node = crate::store::new_node(&key, &content);
node.node_type = crate::store::NodeType::EpisodicSession; 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.upsert_node(node).map_err(|e| anyhow::anyhow!("{}", e))?;
store.save().map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
let word_count = body.split_whitespace().count(); let word_count = body.split_whitespace().count();
Ok(format!("New entry '{}' ({} words)", title, word_count)) Ok(format!("New entry '{}' ({} words)", title, word_count))
} }
async fn journal_update(args: &serde_json::Value) -> Result<String> { async fn journal_update(agent: &Option<std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>>, args: &serde_json::Value) -> Result<String> {
let body = get_str(args, "body")?; let body = get_str(args, "body")?;
let arc = cached_store().await?; let arc = cached_store().await?;
let mut store = arc.lock().await; let mut store = arc.lock().await;
@ -316,7 +324,8 @@ async fn journal_update(args: &serde_json::Value) -> Result<String> {
}; };
let existing = store.nodes.get(&key).unwrap().content.clone(); let existing = store.nodes.get(&key).unwrap().content.clone();
let new_content = format!("{}\n\n{}", existing.trim_end(), body); 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))?; .map_err(|e| anyhow::anyhow!("{}", e))?;
store.save().map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
let word_count = body.split_whitespace().count(); let word_count = body.split_whitespace().count();

View file

@ -507,7 +507,8 @@ impl Subconscious {
for (idx, mut auto) in to_run { for (idx, mut auto) in to_run {
dbglog!("[subconscious] triggering {}", auto.name); 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 fork_point = forked.context.entries.len();
let shared_forked = Arc::new(tokio::sync::Mutex::new(forked)); let shared_forked = Arc::new(tokio::sync::Mutex::new(forked));