Skip to main content

twine_core/
memories.rs

1//! Discovery of local harness memories, instructions, and extraction records.
2//! Database reads use private snapshots; explicit edits are limited to Markdown files.
3mod database;
4mod discovery;
5mod editing;
6mod locations;
7
8pub use editing::save;
9
10use crate::FileContent;
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13use thiserror::Error;
14
15pub const SOURCE_LIMIT: usize = 1_024;
16
17#[derive(Clone, Debug, Default, Deserialize, Serialize)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19pub struct MemoryRequest {
20    pub folder: Option<PathBuf>,
21    pub source_id: Option<String>,
22    pub examples_root: Option<PathBuf>,
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub enum MemoryHarness {
28    Codex,
29    ClaudeCode,
30    Antigravity,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub enum MemoryScope {
36    Global,
37    Folder,
38    OtherFolder,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub enum MemoryKind {
44    Summary,
45    Durable,
46    RawMemory,
47    RolloutSummary,
48    Instructions,
49    Rule,
50    AgentMemory,
51    Skill,
52    Extension,
53    Artifact,
54    StoreStatus,
55    Configuration,
56}
57
58#[derive(Clone, Debug, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct MemorySource {
61    pub id: String,
62    pub title: String,
63    pub harness: MemoryHarness,
64    pub scope: MemoryScope,
65    pub kind: MemoryKind,
66    pub location: String,
67    pub group: String,
68    pub format: String,
69    pub modified_at: Option<u64>,
70    pub example: bool,
71    pub association: Option<String>,
72}
73
74#[derive(Clone, Debug, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct MemoryCatalog {
77    pub sources: Vec<MemorySource>,
78    pub diagnostics: Vec<String>,
79}
80
81#[derive(Clone, Debug, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct MemoryRead {
84    pub source: MemorySource,
85    pub text: Option<String>,
86    pub message: Option<String>,
87    #[serde(skip)]
88    pub file: Option<crate::FilePreview>,
89}
90
91#[derive(Debug, Error)]
92pub enum MemoryError {
93    #[error("Memory discovery requires absolute folder paths.")]
94    InvalidPath,
95    #[error("The source is no longer available. Refresh the source list.")]
96    MissingSource,
97    #[error("Only local Markdown files can be edited from Memories.")]
98    UnsupportedEdit,
99    #[error(transparent)]
100    File(#[from] crate::FileError),
101}
102
103/// Lists bounded local sources without generating or editing memory.
104///
105/// # Errors
106/// Returns an error when a requested folder or example root is not absolute.
107pub fn catalog(request: &MemoryRequest) -> Result<MemoryCatalog, MemoryError> {
108    let (sources, diagnostics) = discovery::discover(request)?;
109    Ok(MemoryCatalog {
110        sources: sources.into_iter().map(|e| e.source).collect(),
111        diagnostics,
112    })
113}
114
115/// Reads a source by a discovered opaque ID.
116///
117/// # Errors
118/// Returns an error for invalid paths or a source that is no longer discoverable.
119pub fn read(request: &MemoryRequest) -> Result<MemoryRead, MemoryError> {
120    let id = request
121        .source_id
122        .as_ref()
123        .ok_or(MemoryError::MissingSource)?;
124    let (entries, _) = discovery::discover(request)?;
125    let entry = entries
126        .into_iter()
127        .find(|e| &e.source.id == id)
128        .ok_or(MemoryError::MissingSource)?;
129    let mut file = None;
130    let content = match entry.backing {
131        discovery::Backing::File { root, path } => {
132            let preview = crate::files::read_preview(&root, &path);
133            let content = preview.content.clone();
134            file = Some(preview);
135            content
136        }
137        discovery::Backing::Text(text) => FileContent::Text(text),
138        discovery::Backing::Database {
139            root,
140            path,
141            thread,
142            column,
143        } => database::read_record(&root, &path, &thread, &column),
144    };
145    let (text, message) = match content {
146        FileContent::Text(text) => (Some(text), None),
147        FileContent::Binary => (
148            None,
149            Some("This source is binary, rather than UTF-8 text.".into()),
150        ),
151        FileContent::TooLarge => (
152            None,
153            Some("This source exceeds the 2 MiB reading limit.".into()),
154        ),
155        FileContent::Missing => (
156            None,
157            Some("This source was removed. Refresh the list.".into()),
158        ),
159        FileContent::Unsupported => (
160            None,
161            Some("Symbolic links and special files cannot be read.".into()),
162        ),
163        FileContent::Unavailable(message) => (None, Some(message)),
164    };
165    Ok(MemoryRead {
166        source: entry.source,
167        text,
168        message,
169        file,
170    })
171}