返回 DeepSeek-TUI-2026
remember.rs
根目录 / crates / tui / src / tools / remember.rs
1 //! `remember` tool — model-callable bullet-add into the user memory file.
2 //!
3 //! Lets the model itself notice a durable preference, convention, or fact
4 //! worth keeping across sessions and write it to the user's `memory.md`.
5 //! The tool is auto-approved and side-effecting only on the user-owned
6 //! memory file (`~/.deepseek/memory.md` by default), so it doesn't get
7 //! gated behind the same approval flow as shell or arbitrary file writes.
8 //!
9 //! Only registered when `[memory] enabled = true` (or
10 //! `DEEPSEEK_MEMORY=on`). When disabled, the tool isn't surfaced to the
11 //! model at all, so prompts that mention `remember` simply fall through.
12
13 use async_trait::async_trait;
14 use serde_json::{Value, json};
15
16 use super::spec::{
17 ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec, required_str,
18 };
19
20 /// Tool that appends one bullet to the user memory file.
21 pub struct RememberTool;
22
23 #[async_trait]
24 impl ToolSpec for RememberTool {
25 fn name(&self) -> &'static str {
26 "remember"
27 }
28
29 fn description(&self) -> &'static str {
30 "Append a durable note to the user memory file so it surfaces in \
31 future sessions. Use this when the user states a preference, a \
32 convention they want enforced, or a fact about themselves or \
33 their workflow that you should not have to relearn next time. \
34 Keep notes terse (one sentence). Don't store secrets, transient \
35 tasks, or reasoning scratch — those belong in a checklist or in \
36 the conversation."
37 }
38
39 fn input_schema(&self) -> Value {
40 json!({
41 "type": "object",
42 "properties": {
43 "note": {
44 "type": "string",
45 "description": "The single-sentence durable note to remember."
46 }
47 },
48 "required": ["note"]
49 })
50 }
51
52 fn capabilities(&self) -> Vec<ToolCapability> {
53 vec![ToolCapability::WritesFiles]
54 }
55
56 fn approval_requirement(&self) -> ApprovalRequirement {
57 // Memory writes are scoped to the user's own memory file; gating
58 // them behind the standard shell/write approval would defeat the
59 // point of automatic memory.
60 ApprovalRequirement::Auto
61 }
62
63 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
64 let note = required_str(&input, "note")?;
65 let path = context.memory_path.as_ref().ok_or_else(|| {
66 ToolError::execution_failed(
67 "user memory is disabled — set `[memory] enabled = true` in config.toml or \
68 `DEEPSEEK_MEMORY=on` in the environment to enable",
69 )
70 })?;
71
72 crate::memory::append_entry(path, note).map_err(|err| {
73 ToolError::execution_failed(format!("failed to append to {}: {err}", path.display()))
74 })?;
75
76 Ok(ToolResult::success(format!(
77 "remembered: {}",
78 note.trim_start_matches('#').trim()
79 )))
80 }
81 }
82
83 #[cfg(test)]
84 mod tests {
85 use super::*;
86 use std::path::PathBuf;
87 use tempfile::tempdir;
88
89 fn ctx_with_memory(path: PathBuf) -> ToolContext {
90 let mut ctx = ToolContext::new(path.parent().unwrap_or_else(|| std::path::Path::new(".")));
91 ctx.memory_path = Some(path);
92 ctx
93 }
94
95 #[tokio::test]
96 async fn returns_error_when_memory_disabled() {
97 let tmp = tempdir().unwrap();
98 let mut ctx = ToolContext::new(tmp.path());
99 ctx.memory_path = None; // explicitly disabled
100
101 let tool = RememberTool;
102 let err = tool
103 .execute(json!({"note": "use 4 spaces for indentation"}), &ctx)
104 .await
105 .unwrap_err();
106 assert!(err.to_string().contains("memory is disabled"), "{err}");
107 }
108
109 #[tokio::test]
110 async fn appends_bullet_to_memory_file() {
111 let tmp = tempdir().unwrap();
112 let path = tmp.path().join("memory.md");
113 let ctx = ctx_with_memory(path.clone());
114
115 let tool = RememberTool;
116 let result = tool
117 .execute(json!({"note": "use 4 spaces for indentation"}), &ctx)
118 .await
119 .expect("ok");
120 assert!(result.success);
121 assert!(result.content.contains("4 spaces"));
122
123 let body = std::fs::read_to_string(&path).expect("read");
124 assert!(body.contains("4 spaces"));
125 assert!(body.starts_with("- ("), "{body}");
126 }
127
128 #[tokio::test]
129 async fn rejects_missing_note_field() {
130 let tmp = tempdir().unwrap();
131 let path = tmp.path().join("memory.md");
132 let ctx = ctx_with_memory(path);
133
134 let tool = RememberTool;
135 let err = tool.execute(json!({}), &ctx).await.unwrap_err();
136 assert!(err.to_string().to_lowercase().contains("note"), "{err}");
137 }
138 }
139
139 lines RUST