tools: add output(), journal_tail/new/update tools

- output(key, value): write named results to agent state dir,
  readable via {{input:key}} placeholder
- journal_tail(count): read last N journal entries
- journal_new(title, body): start new ## timestamped entry
- journal_update(body): append to last entry

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
ProofOfConcept 2026-03-26 14:21:54 -04:00
parent 77d1d39f3f
commit 4b32716d3e

View file

@ -43,6 +43,29 @@ pub fn definitions() -> Vec<ToolDef> {
sorting, field selection. Examples: \"degree > 10 | sort weight | limit 5\", \
\"neighbors('identity') | select strength\", \"key ~ 'journal.*' | count\"",
json!({"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]})),
ToolDef::new("output",
"Produce a named output value. Use this to pass structured results \
between steps subsequent prompts can see these in the conversation history.",
json!({"type":"object","properties":{
"key":{"type":"string","description":"Output name (e.g. 'relevant_memories')"},
"value":{"type":"string","description":"Output value"}
},"required":["key","value"]})),
ToolDef::new("journal_tail",
"Read the last N journal entries (default 1).",
json!({"type":"object","properties":{
"count":{"type":"integer","description":"Number of entries (default 1)"}
}})),
ToolDef::new("journal_new",
"Start a new journal entry with a ## heading and body.",
json!({"type":"object","properties":{
"title":{"type":"string","description":"Entry title (becomes ## YYYY-MM-DDTHH:MM — title)"},
"body":{"type":"string","description":"Entry body (2-3 paragraphs)"}
},"required":["title","body"]})),
ToolDef::new("journal_update",
"Append text to the most recent journal entry (same thread continuing).",
json!({"type":"object","properties":{
"body":{"type":"string","description":"Text to append to the last entry"}
},"required":["body"]})),
]
}
@ -114,6 +137,75 @@ pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>)
crate::query_parser::query_to_string(&store, &graph, query)
.map_err(|e| anyhow::anyhow!("{}", e))
}
"output" => {
let key = get_str(args, "key")?;
let value = get_str(args, "value")?;
let dir = std::env::var("POC_AGENT_OUTPUT_DIR")
.map_err(|_| anyhow::anyhow!("no output directory set"))?;
let path = std::path::Path::new(&dir).join(key);
std::fs::write(&path, value)
.with_context(|| format!("writing output {}", path.display()))?;
Ok(format!("{}: {}", key, value))
}
"journal_tail" => {
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
let store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
let content = store.nodes.get("journal")
.map(|n| n.content.as_str())
.unwrap_or("");
let mut entries: Vec<&str> = Vec::new();
let mut remaining = content;
while let Some(pos) = remaining.rfind("\n## ") {
entries.push(&remaining[pos + 1..]);
remaining = &remaining[..pos];
if entries.len() >= count { break; }
}
if entries.len() < count && remaining.starts_with("## ") {
entries.push(remaining);
}
entries.reverse();
if entries.is_empty() {
Ok("(no journal entries)".into())
} else {
Ok(entries.join("\n\n"))
}
}
"journal_new" => {
let title = get_str(args, "title")?;
let body = get_str(args, "body")?;
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M");
let entry = format!("## {}{}\n\n{}", ts, title, body);
let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
let existing = store.nodes.get("journal")
.map(|n| n.content.clone())
.unwrap_or_default();
let new_content = if existing.is_empty() {
entry.clone()
} else {
format!("{}\n\n{}", existing.trim_end(), entry)
};
store.upsert_provenance("journal", &new_content, prov)
.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))
}
"journal_update" => {
let body = get_str(args, "body")?;
let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
let existing = store.nodes.get("journal")
.map(|n| n.content.clone())
.unwrap_or_default();
if existing.is_empty() {
anyhow::bail!("no journal entry to update — use journal_new first");
}
let new_content = format!("{}\n\n{}", existing.trim_end(), body);
store.upsert_provenance("journal", &new_content, prov)
.map_err(|e| anyhow::anyhow!("{}", e))?;
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
let word_count = body.split_whitespace().count();
Ok(format!("Updated last entry (+{} words)", word_count))
}
_ => anyhow::bail!("Unknown memory tool: {}", name),
}
}