Initial commit: aether-tools-rs workspace

This commit is contained in:
gyt
2026-07-09 13:28:28 -04:00
commit 695725857d
17 changed files with 1912 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "aether-recall-mcp"
version.workspace = true
edition.workspace = true
[[bin]]
name = "aether-recall-mcp"
path = "src/main.rs"
[dependencies]
aether-atom = { path = "/home/cosmos/aether-atom" }
serde = { workspace = true }
serde_json = { workspace = true }
sha3 = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true }
tempfile = "3"
+311
View File
@@ -0,0 +1,311 @@
//! aether-recall-mcp — the entire aether-recall tool, refactored as a
//! thin MCP transport over the atom.
//!
//! What used to be 830 LOC across 5 modules is now this: 4 tool handlers
//! that call `aether_atom::{ask, remember, demote}`. The canonicalization,
//! fingerprinting, ledger, scope fallback, persistence, and TTL compaction
//! all come from the atom.
use aether_atom::{data_dir, fingerprint, rust_canonicalize, Lattice};
use serde_json::{json, Value};
use std::env;
use std::io::{BufRead, BufReader, Write};
use std::time::SystemTime;
fn now_ts() -> u64 {
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn ok(id: Value, result: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
fn err(id: Value, code: i64, message: &str) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
}
fn tool_result(content: Value, is_error: bool) -> Value {
json!({ "content": [{ "type": "text", "text": content.to_string() }], "isError": is_error })
}
struct Server {
lattice: Lattice,
}
impl Server {
fn new(lattice: Lattice) -> Self {
Self { lattice }
}
fn dispatch(&mut self, msg: Value) -> Option<Value> {
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let is_request = !id.is_null();
let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
let params = msg.get("params").cloned().unwrap_or_else(|| json!({}));
let respond = |result: Value| -> Option<Value> {
if is_request {
Some(ok(id.clone(), result))
} else {
None
}
};
match method {
"initialize" => respond(json!({
"protocolVersion": "2025-06-18",
"serverInfo": { "name": "aether-recall", "version": "0.1.0" },
"capabilities": { "tools": {} }
})),
"tools/list" => respond(tools_list()),
"tools/call" => respond(self.tool_call(&params)),
"ping" => respond(json!({})),
m if m.starts_with("notifications/") => None,
_ => {
if is_request {
Some(err(id, -32601, "method not found"))
} else {
None
}
}
}
}
fn tool_call(&mut self, params: &Value) -> Value {
let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or_else(|| json!({}));
match name {
"lookup_resolution" => self.tool_lookup(&args),
"record_outcome" => self.tool_record(&args),
"memory_stats" => self.tool_stats(),
"compact" => self.tool_compact(),
other => tool_result(
json!({ "error": format!("unknown tool: {}", other) }),
true,
),
}
}
/// `lookup_resolution(canonical_text)` — content-only lookup; returns
/// cached resolution if present.
fn tool_lookup(&mut self, args: &Value) -> Value {
let failure = match args.get("failure").and_then(|v| v.as_str()) {
Some(f) if !f.trim().is_empty() => f,
_ => {
return tool_result(
json!({
"error": "lookup_resolution: parameter 'failure' is required and must be a non-empty string",
"tool": "lookup_resolution",
"parameter": "failure",
"expected": "non-empty string"
}),
true,
);
}
};
let canon = rust_canonicalize(failure);
let sig = fingerprint(&canon);
match self.lattice.lookup(&sig, "global") {
Some(hit) => tool_result(
json!({
"hit": true,
"signature": sig.as_bytes().iter()
.map(|b| format!("{:02x}", b))
.collect::<String>(),
"value": String::from_utf8_lossy(hit.value.as_ref()).to_string(),
"value_hex": hit.value.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>(),
"worked": hit.worked,
"scope": hit.scope,
}),
false,
),
None => tool_result(
json!({
"hit": false,
"signature": sig.as_bytes().iter()
.map(|b| format!("{:02x}", b))
.collect::<String>(),
"note": "no verified resolution yet; call record_outcome after fixing"
}),
false,
),
}
}
/// `record_outcome(canonical_text, resolution, worked)` — direct
/// seeding. Use after a fresh fix has been verified.
fn tool_record(&mut self, args: &Value) -> Value {
let failure = match args.get("failure").and_then(|v| v.as_str()) {
Some(f) if !f.trim().is_empty() => f,
_ => {
return tool_result(
json!({
"error": "record_outcome: parameter 'failure' is required and must be a non-empty string",
"tool": "record_outcome",
"parameter": "failure",
"expected": "non-empty string"
}),
true,
);
}
};
let resolution = match args.get("resolution").and_then(|v| v.as_str()) {
Some(r) if !r.trim().is_empty() => r,
_ => {
return tool_result(
json!({
"error": "record_outcome: parameter 'resolution' is required and must be a non-empty string",
"tool": "record_outcome",
"parameter": "resolution",
"expected": "non-empty string"
}),
true,
);
}
};
let worked = args.get("worked").and_then(|v| v.as_bool()).unwrap_or(true);
let scope = args.get("scope").and_then(|v| v.as_str()).unwrap_or("global");
let canon = rust_canonicalize(failure);
let sig = fingerprint(&canon);
self.lattice.record(sig, scope, resolution.as_bytes(), Some(worked));
tool_result(
json!({
"recorded": true,
"signature": sig.as_bytes().iter()
.map(|b| format!("{:02x}", b))
.collect::<String>(),
"scope": scope,
"worked": worked,
"ts": now_ts(),
}),
false,
)
}
/// `memory_stats()` — recall ledger health.
fn tool_stats(&mut self) -> Value {
tool_result(
json!({
"records": self.lattice.record_count(),
"lookups": self.lattice.lookups,
"hits": self.lattice.hits,
"misses": self.lattice.misses,
"hit_rate": self.lattice.hit_rate(),
"outcomes": self.lattice.outcomes,
}),
false,
)
}
/// `compact(retention_days)` — drop lookup events older than N
/// days; default 30. Outcomes (durable facts) are preserved.
fn tool_compact(&mut self) -> Value {
let retention_secs = match env::var("AETHER_RECALL_LOOKUP_RETENTION_DAYS") {
Ok(s) => s.parse::<u64>().unwrap_or(30).saturating_mul(86_400),
Err(_) => 30 * 86_400,
};
match self.lattice.compact(retention_secs) {
Ok(dropped) => tool_result(
json!({
"compacted": true,
"dropped_lookups": dropped,
"retention_secs": retention_secs,
"retention_days": retention_secs / 86_400,
}),
false,
),
Err(e) => tool_result(
json!({ "error": format!("compaction failed: {}", e) }),
true,
),
}
}
}
fn tools_list() -> Value {
json!({
"tools": [
{
"name": "lookup_resolution",
"description": "Look up a previously-recorded resolution for a canonical failure text",
"inputSchema": {
"type": "object",
"properties": {
"failure": { "type": "string", "description": "Canonical form of the failure to look up" }
},
"required": ["failure"]
}
},
{
"name": "record_outcome",
"description": "Record an outcome: this canonical failure has this verified resolution",
"inputSchema": {
"type": "object",
"properties": {
"failure": { "type": "string" },
"resolution": { "type": "string" },
"worked": { "type": "boolean" },
"scope": { "type": "string", "default": "global" }
},
"required": ["failure", "resolution"]
}
},
{
"name": "memory_stats",
"description": "Return recall ledger health: hit_rate, lookups, outcomes, records",
"inputSchema": { "type": "object", "properties": {} }
},
{
"name": "compact",
"description": "Drop lookup events older than AETHER_RECALL_LOOKUP_RETENTION_DAYS",
"inputSchema": { "type": "object", "properties": {} }
}
]
})
}
fn main() {
let dir = data_dir("AETHER_RECALL_DIR");
let lattice = match Lattice::open(&dir) {
Ok(l) => l,
Err(e) => {
eprintln!("aether-recall-mcp: failed to open lattice at {:?}: {}", dir, e);
std::process::exit(1);
}
};
eprintln!("aether-recall-mcp ready (memory: {:?})", dir);
let mut server = Server::new(lattice);
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
for line in BufReader::new(stdin.lock()).lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
if line.trim().is_empty() {
continue;
}
let msg: Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(e) => {
eprintln!("aether-recall-mcp: parse error: {}", e);
continue;
}
};
if let Some(resp) = server.dispatch(msg) {
if let Ok(s) = serde_json::to_string(&resp) {
let _ = writeln!(stdout, "{}", s);
let _ = stdout.flush();
}
}
}
}
@@ -0,0 +1,159 @@
//! Cross-binary integration test.
//!
//! Spawns `aether-recall-mcp` and `aether-ask` against a shared data
//! directory. Issues a record via the MCP transport, then a query via
//! the HTTP transport. Asserts the second process sees what the
//! first wrote. If this fails, the system is not an integrated whole.
use serde_json::json;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Find the per-target binaries. We resolve via the per-target
/// `CARGO_BIN_EXE_<name>` for our own crate's bin, and via the
/// workspace target/release path for sibling crates' bins.
fn bin_mcp() -> PathBuf {
let env = format!("CARGO_BIN_EXE_{}", "aether-recall-mcp");
PathBuf::from(std::env::var(&env).expect("CARGO_BIN_EXE_aether-recall-mcp"))
}
fn bin_ask() -> PathBuf {
// Sibling-crate binaries are not exposed via CARGO_BIN_EXE_ in this
// crate's tests; they sit in workspace target/{debug,release}.
// We use the absolute workspace path because the test's CWD isn't
// guaranteed to be the workspace root.
// ../target/debug/aether-ask from this crate's tests/ dir at
// crates/aether-recall-mcp/tests/ is too fragile; instead use CARGO_MANIFEST_DIR.
// But we don't know which profile. Probe both.
let manifest = std::env::var("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR");
let crate_dir = PathBuf::from(manifest); // crates/aether-recall-mcp
let workspace_dir = crate_dir
.parent().and_then(|p| p.parent()).expect("workspace dir")
.to_path_buf();
for profile in ["release", "debug"] {
let p = workspace_dir.join("target").join(profile).join("aether-ask");
if p.exists() { return p; }
}
panic!("aether-ask binary not found under {workspace_dir:?}/target/{{release,debug}}");
}
struct McpClient {
child: Child,
}
impl McpClient {
fn spawn(data_dir: &Path) -> Self {
let mut cmd = Command::new(bin_mcp());
cmd.env("AETHER_RECALL_DIR", data_dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
Self { child: cmd.spawn().expect("spawn mcp") }
}
fn send(&mut self, req: serde_json::Value) -> serde_json::Value {
let stdin = self.child.stdin.as_mut().unwrap();
writeln!(stdin, "{}", serde_json::to_string(&req).unwrap()).unwrap();
stdin.flush().unwrap();
let stdout = self.child.stdout.as_mut().unwrap();
let mut buf = Vec::new();
let mut byte = [0u8; 1];
while stdout.read(&mut byte).unwrap() > 0 {
buf.push(byte[0]);
if byte[0] == b'\n' { break; }
}
serde_json::from_slice(&buf).unwrap()
}
}
impl Drop for McpClient {
fn drop(&mut self) { let _ = self.child.kill(); }
}
#[test]
fn mcp_record_then_mcp_lookup_returns_hit() {
let dir = tmp();
let mut m = McpClient::spawn(&dir);
m.send(json!({
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"record_outcome","arguments":{
"failure":"E0308 canonical test","resolution":"use .to_string()","worked":true
}}
}));
let r = m.send(json!({
"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"lookup_resolution","arguments":{"failure":"E0308 canonical test"}}
}));
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"hit\":true"), "got: {content}");
assert!(content.contains("use .to_string()"));
}
#[test]
fn ask_returns_value_after_mcp_recorded() {
let dir = tmp();
// 1. MCP records a fact in process A
{
let mut m = McpClient::spawn(&dir);
m.send(json!({
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"record_outcome","arguments":{
"failure":"cross-binary integration",
"resolution":"verified by separate processes",
"worked":true
}}
}));
} // process A drops → killed
// 2. ASK spins up in process B, hits the SAME data dir, asserts hit
let port = ephemeral_port();
let mut ask_child = Command::new(bin_ask())
.env("AETHER_RECALL_DIR", &dir)
.env("AETHER_ASK_PORT", port.to_string())
.env("OLLAMA_PORT", "1") // unreachable → fast neural-fail path
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn ask");
let addr = format!("127.0.0.1:{}", port);
for _ in 0..60 {
if TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(100)).is_ok() {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
s.set_read_timeout(Some(Duration::from_secs(3))).ok();
let body = json!({"question":"cross-binary integration"}).to_string();
let req = format!(
"POST /ask HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(), body
);
s.write_all(req.as_bytes()).unwrap();
let mut buf = Vec::new();
s.read_to_end(&mut buf).ok();
let text = String::from_utf8_lossy(&buf).to_string();
let (_head, body) = text.split_once("\r\n\r\n").unwrap_or(("", &text));
let v: serde_json::Value = serde_json::from_str(body).expect("json");
let _ = ask_child.kill();
assert_eq!(v["source"].as_str(), Some("deterministic"),
"ASK must hit the cached MCP record across processes; got: {v:?}");
assert_eq!(v["answer"].as_str(), Some("verified by separate processes"));
}
fn tmp() -> PathBuf {
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let p = std::env::temp_dir().join(format!("cross-bin-it-{stamp}"));
std::fs::create_dir_all(&p).unwrap();
p
}
fn ephemeral_port() -> u16 {
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
33_000 + ((stamp % 1000) as u16)
}
@@ -0,0 +1,130 @@
//! End-to-end integration tests for aether-recall-mcp.
//!
//! Spawns the actual binary, talks JSON-RPC over stdio, verifies
//! the round-trip and the canonical-form abstraction.
use serde_json::json;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
fn binary_path() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_aether-recall-mcp"))
}
struct Mcp {
child: Child,
}
impl Mcp {
fn spawn(data_dir: &PathBuf) -> Self {
let mut cmd = Command::new(binary_path());
cmd.env("AETHER_RECALL_DIR", data_dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child = cmd.spawn().expect("spawn aether-recall-mcp");
Self { child }
}
fn send(&mut self, req: serde_json::Value) -> serde_json::Value {
let stdin = self.child.stdin.as_mut().expect("stdin");
let s = serde_json::to_string(&req).unwrap();
writeln!(stdin, "{}", s).unwrap();
stdin.flush().unwrap();
let stdout = self.child.stdout.as_mut().expect("stdout");
let mut buf = Vec::new();
let mut byte = [0u8; 1];
while stdout.read(&mut byte).unwrap() > 0 {
buf.push(byte[0]);
if byte[0] == b'\n' { break; }
}
serde_json::from_slice(&buf).unwrap()
}
}
impl Drop for Mcp {
fn drop(&mut self) {
let _ = self.child.kill();
}
}
#[test]
fn full_lifecycle() {
let dir = tempdir_in("/tmp");
let mut m = Mcp::spawn(&dir);
// 1. initialize
let r = m.send(json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}));
assert_eq!(r["result"]["serverInfo"]["name"], "aether-recall");
// 2. cold lookup → miss
let r = m.send(json!({
"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"lookup_resolution","arguments":{"failure":"error[E0308]: expected i32, found &str"}}
}));
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"hit\":false"), "got: {content}");
// 3. record_outcome
let r = m.send(json!({
"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"record_outcome","arguments":{"failure":"error[E0308]: expected i32, found &str","resolution":"use .to_string()","worked":true}}
}));
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"recorded\":true"));
// 4. warm lookup → hit
let r = m.send(json!({
"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"lookup_resolution","arguments":{"failure":"error[E0308]: expected i32, found &str"}}
}));
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"hit\":true"), "got: {content}");
assert!(content.contains("use .to_string()"), "got: {content}");
// 5. path-equivalent surface form → hit (canonical abstraction)
let r = m.send(json!({
"jsonrpc":"2.0","id":5,"method":"tools/call",
"params":{"name":"lookup_resolution","arguments":{"failure":"error[E0308]: expected i32, found &str\n --> src/foo.rs:42:5"}}
}));
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"hit\":true"), "got: {content}");
// 6. memory_stats: 3 lookups, 2 hits, 1 miss
let r = m.send(json!({
"jsonrpc":"2.0","id":6,"method":"tools/call",
"params":{"name":"memory_stats","arguments":{}}
}));
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"lookups\":3"));
assert!(content.contains("\"hits\":2"));
assert!(content.contains("\"misses\":1"));
assert!(content.contains("\"hit_rate\":0.6"));
// 7. compact succeeds
let r = m.send(json!({
"jsonrpc":"2.0","id":7,"method":"tools/call",
"params":{"name":"compact","arguments":{}}
}));
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"compacted\":true"));
// 8. error: empty failure
let r = m.send(json!({
"jsonrpc":"2.0","id":8,"method":"tools/call",
"params":{"name":"lookup_resolution","arguments":{"failure":""}}
}));
assert_eq!(r["result"]["isError"], true);
let content = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"tool\":\"lookup_resolution\""));
}
fn tempdir_in(parent: &str) -> PathBuf {
use std::time::{SystemTime, UNIX_EPOCH};
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let p = PathBuf::from(parent).join(format!("aether-recall-mcp-it-{}", stamp));
std::fs::create_dir_all(&p).unwrap();
p
}