atman_runtime/memory/
mod.rs1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use tokio::io::AsyncWriteExt;
5use uuid::Uuid;
6
7use crate::error::RuntimeError;
8
9pub mod confession;
10pub mod goal;
11pub mod plan;
12pub mod spec;
13pub mod todo;
14
15pub use confession::{Confession, ConfessionStore};
16pub use goal::GoalStore;
17pub use plan::{Plan, PlanStep, PlanStore};
18pub use spec::{
19 SpecDeviation, SpecEntry, SpecMaterializeResult, SpecReview, SpecStatus, SpecStore,
20};
21pub use todo::{Todo, TodoStatus, TodoStore};
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
24#[serde(transparent)]
25pub struct MemoryId(pub Uuid);
26
27impl MemoryId {
28 pub fn now() -> Self {
29 Self(Uuid::now_v7())
30 }
31
32 pub fn parse(s: &str) -> Result<Self, uuid::Error> {
33 Uuid::parse_str(s).map(Self)
34 }
35}
36
37impl std::fmt::Display for MemoryId {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 self.0.fmt(f)
40 }
41}
42
43pub(crate) async fn append_jsonl(path: &Path, value: &impl Serialize) -> Result<(), RuntimeError> {
44 if let Some(parent) = path.parent() {
45 tokio::fs::create_dir_all(parent)
46 .await
47 .map_err(|e| RuntimeError::ToolFailed(format!("mkdir {}: {e}", parent.display())))?;
48 }
49 let mut file = tokio::fs::OpenOptions::new()
50 .create(true)
51 .append(true)
52 .open(path)
53 .await
54 .map_err(|e| RuntimeError::ToolFailed(format!("open {}: {e}", path.display())))?;
55 let line = serde_json::to_string(value)
56 .map_err(|e| RuntimeError::ToolFailed(format!("encode: {e}")))?;
57 file.write_all(line.as_bytes())
58 .await
59 .map_err(|e| RuntimeError::ToolFailed(format!("write: {e}")))?;
60 file.write_all(b"\n")
61 .await
62 .map_err(|e| RuntimeError::ToolFailed(format!("write: {e}")))?;
63 file.flush()
64 .await
65 .map_err(|e| RuntimeError::ToolFailed(format!("flush: {e}")))?;
66 Ok(())
67}
68
69pub(crate) async fn read_jsonl<T: for<'de> Deserialize<'de>>(
70 path: &PathBuf,
71) -> Result<Vec<T>, RuntimeError> {
72 let contents = match tokio::fs::read_to_string(path).await {
73 Ok(c) => c,
74 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
75 Err(e) => {
76 return Err(RuntimeError::ToolFailed(format!(
77 "read {}: {e}",
78 path.display()
79 )));
80 }
81 };
82 let mut out = Vec::new();
83 for (i, line) in contents.lines().enumerate() {
84 if line.trim().is_empty() {
85 continue;
86 }
87 match serde_json::from_str::<T>(line) {
88 Ok(v) => out.push(v),
89 Err(e) => {
90 let key = format!("jsonl.malformed:{}", path.display());
91 crate::notify!(
92 warn,
93 location = Log,
94 stack = merge_count(key, 60_000),
95 "skipping malformed jsonl line {}:{}: {e}",
96 path.display(),
97 i + 1
98 );
99 }
100 }
101 }
102 Ok(out)
103}