diff --git a/src/agent/memory.rs b/src/agent/memory.rs
new file mode 100644
index 0000000..57ab4a0
--- /dev/null
+++ b/src/agent/memory.rs
@@ -0,0 +1,146 @@
+// agent/memory.rs — Agent's live view of memory nodes
+//
+// MemoryNode is the agent's in-memory representation of a loaded
+// graph node. Unlike the store's Node (which has all the metadata),
+// this holds what the agent needs: the key, rendered content, and
+// links for navigation. The agent's context window tracks which
+// MemoryNodes are currently loaded.
+
+use crate::store::Store;
+
+/// A memory node loaded into the agent's working memory.
+#[derive(Debug, Clone)]
+pub struct MemoryNode {
+ pub key: String,
+ pub content: String,
+ pub links: Vec,
+ /// Version from the store — used for change detection.
+ pub version: u32,
+ /// Weight in the graph.
+ pub weight: f32,
+}
+
+/// A link to a neighbor node.
+#[derive(Debug, Clone)]
+pub struct Link {
+ pub target: String,
+ pub strength: f32,
+ /// Whether this link target is already referenced inline in the content.
+ pub inline: bool,
+}
+
+impl MemoryNode {
+ /// Load a node from the store by key. Returns None if not found.
+ pub fn load(key: &str) -> Option {
+ let store = Store::load().ok()?;
+ Self::from_store(&store, key)
+ }
+
+ /// Load from an already-open store.
+ pub fn from_store(store: &Store, key: &str) -> Option {
+ let node = store.nodes.get(key)?;
+
+ // Collect neighbor strengths
+ let mut neighbors: std::collections::HashMap<&str, f32> = std::collections::HashMap::new();
+ for r in &store.relations {
+ if r.deleted { continue; }
+ if r.source_key == key {
+ let e = neighbors.entry(&r.target_key).or_insert(0.0);
+ *e = e.max(r.strength);
+ } else if r.target_key == key {
+ let e = neighbors.entry(&r.source_key).or_insert(0.0);
+ *e = e.max(r.strength);
+ }
+ }
+
+ let mut links: Vec = neighbors.into_iter()
+ .map(|(target, strength)| Link {
+ inline: node.content.contains(target),
+ target: target.to_string(),
+ strength,
+ })
+ .collect();
+ links.sort_by(|a, b| b.strength.total_cmp(&a.strength));
+
+ Some(MemoryNode {
+ key: key.to_string(),
+ content: node.content.clone(),
+ links,
+ version: node.version,
+ weight: node.weight,
+ })
+ }
+
+ /// Render for inclusion in the context window.
+ pub fn render(&self) -> String {
+ let mut out = self.content.clone();
+
+ // Footer: links not already referenced inline
+ let footer_links: Vec<&Link> = self.links.iter()
+ .filter(|l| !l.inline)
+ .collect();
+
+ if !footer_links.is_empty() {
+ let total = footer_links.len();
+ out.push_str("\n\n---\nLinks:");
+ for link in footer_links.iter().take(15) {
+ out.push_str(&format!("\n ({:.2}) `poc-memory render {}`",
+ link.strength, link.target));
+ }
+ if total > 15 {
+ out.push_str(&format!("\n ... and {} more (`poc-memory graph link {}`)",
+ total - 15, self.key));
+ }
+ }
+ out
+ }
+
+ /// Write content to the store and return an updated MemoryNode.
+ pub fn write(key: &str, content: &str, provenance: Option<&str>) -> Result {
+ let prov = provenance.unwrap_or("manual");
+ let mut store = Store::load()?;
+ store.upsert_provenance(key, content, prov)?;
+ store.save()?;
+
+ Self::from_store(&store, key)
+ .ok_or_else(|| format!("wrote {} but failed to load back", key))
+ }
+
+ /// Search for nodes matching a query. Returns lightweight results.
+ pub fn search(query: &str) -> Result, String> {
+ let store = Store::load()?;
+ let results = crate::search::search(query, &store);
+
+ Ok(results.into_iter().take(20).map(|hit| SearchResult {
+ key: hit.key.clone(),
+ score: hit.activation as f32,
+ snippet: hit.snippet.unwrap_or_default(),
+ }).collect())
+ }
+
+ /// Mark a node as used (boosts weight).
+ pub fn mark_used(key: &str) -> Result {
+ let mut store = Store::load()?;
+ if !store.nodes.contains_key(key) {
+ return Err(format!("node not found: {}", key));
+ }
+ store.mark_used(key);
+ store.save()?;
+ Ok(format!("marked {} as used", key))
+ }
+}
+
+/// A search result — lightweight, not a full node load.
+#[derive(Debug, Clone)]
+pub struct SearchResult {
+ pub key: String,
+ pub score: f32,
+ pub snippet: String,
+}
+
+impl SearchResult {
+ /// Format for display.
+ pub fn render(&self) -> String {
+ format!("({:.2}) {} — {}", self.score, self.key, self.snippet)
+ }
+}
diff --git a/src/agent/mod.rs b/src/agent/mod.rs
index fee97de..c5b4960 100644
--- a/src/agent/mod.rs
+++ b/src/agent/mod.rs
@@ -28,6 +28,7 @@ pub mod tools;
pub mod ui_channel;
pub mod journal;
+pub mod memory;
pub mod runner;
pub mod cli;
pub mod context;