Skip to main content

atman_runtime/tools/
memory.rs

1use std::sync::Arc;
2
3use crate::error::RuntimeError;
4use crate::memory::MemoryId;
5use crate::memory::confession::{Confession, ConfessionStore};
6use crate::memory::goal::GoalStore;
7use crate::memory::spec::SpecStore;
8use crate::memory::todo::{Todo, TodoStatus, TodoStore};
9use crate::tool::{BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
10use crate::value::Value;
11
12pub struct MemoryGoalGet {
13    pub store: Arc<GoalStore>,
14}
15
16impl Tool for MemoryGoalGet {
17    fn name(&self) -> &str {
18        "memory.goal.get"
19    }
20
21    fn tier(&self) -> Tier {
22        Tier::Zero
23    }
24
25    fn description(&self) -> Option<&str> {
26        Some(
27            "Return the current session goal (persistent, auto-injected as system prefix). Empty string when unset.",
28        )
29    }
30
31    fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
32        Box::pin(async move {
33            let text = self
34                .store
35                .get()
36                .map_err(|e| RuntimeError::ToolFailed(format!("goal.get: {e}")))?;
37            Ok(Value::Str(text))
38        })
39    }
40}
41
42pub struct MemoryGoalSet {
43    pub store: Arc<GoalStore>,
44}
45
46impl Tool for MemoryGoalSet {
47    fn name(&self) -> &str {
48        "memory.goal.set"
49    }
50
51    fn tier(&self) -> Tier {
52        Tier::One
53    }
54
55    fn description(&self) -> Option<&str> {
56        Some(
57            "Set the session goal — a short directive (1-2 sentences) that atman injects \
58             as a system-prompt prefix on every LLM call. It persists across turns, never \
59             enters message history, and is never compacted.\n\n\
60             Best practice: set the goal early (right after understanding the user's request), \
61             keep it concise and actionable. Update it if the user's intent changes. Clear it \
62             when the task is complete. Example: 'Fix the login bug in auth.rs and add a \
63             regression test.'",
64        )
65    }
66
67    fn input_schema(&self) -> serde_json::Value {
68        serde_json::json!({
69            "type": "object",
70            "properties": {
71                "text": {
72                    "type": "string",
73                    "description": "The goal text, 1-2 sentences. Be specific: what to do, where, and what 'done' looks like."
74                }
75            },
76            "required": ["text"]
77        })
78    }
79
80    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
81        Box::pin(async move {
82            let text = required_string(&args, "text")?;
83            self.store
84                .set(&text)
85                .map_err(|e| RuntimeError::ToolFailed(format!("goal.set: {e}")))?;
86            Ok(Value::Unit)
87        })
88    }
89}
90
91pub struct MemoryRecentTurns;
92
93impl Tool for MemoryRecentTurns {
94    fn name(&self) -> &str {
95        "memory.recent_turns"
96    }
97
98    fn tier(&self) -> Tier {
99        Tier::Zero
100    }
101
102    fn description(&self) -> Option<&str> {
103        Some(
104            "Return the last N Message values (user + assistant + tool_result) from the \
105             current session's event log so a flow can hand the code agent a sliding \
106             history window. Reads from disk; cost O(events file size).",
107        )
108    }
109
110    fn input_schema(&self) -> serde_json::Value {
111        serde_json::json!({
112            "type": "object",
113            "properties": {
114                "n": {"type": "integer", "description": "Max message count to return (default 10)"}
115            }
116        })
117    }
118
119    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
120        Box::pin(async move {
121            let n = match args.named("n").or_else(|| args.positional(0).ok()) {
122                Some(Value::Int(k)) if *k >= 0 => *k as usize,
123                Some(other) => {
124                    return Err(RuntimeError::TypeMismatch {
125                        expected: "non-negative int".into(),
126                        actual: other.kind_name().into(),
127                    });
128                }
129                None => 10,
130            };
131            if n == 0 {
132                if let Some(cb) = &ctx.on_memory_recent {
133                    cb(0);
134                }
135                return Ok(Value::Struct(vec![
136                    ("total_message_count".into(), Value::Int(0)),
137                    ("items".into(), Value::List(Vec::new())),
138                ]));
139            }
140            // Sub-agent path: session_messages has the child's local message list.
141            if let Some(msgs) = ctx.session_messages.as_ref() {
142                let total = msgs.len() as u64;
143                let start = msgs.len().saturating_sub(n);
144                let out: Vec<Value> = msgs[start..].iter().cloned().map(Value::Message).collect();
145                if let Some(cb) = &ctx.on_memory_recent {
146                    cb(out.len() as u16);
147                }
148                return Ok(Value::Struct(vec![
149                    ("total_message_count".into(), Value::Int(total as i64)),
150                    ("items".into(), Value::List(out)),
151                ]));
152            }
153            // Main-agent path: delegate to HistoryStore.
154            let Some(store) = ctx.history_store.clone() else {
155                return Err(RuntimeError::ToolFailed(
156                    "memory.recent_turns: no history store on context".into(),
157                ));
158            };
159            let (total, msgs) = tokio::task::spawn_blocking(move || store.recent(n))
160                .await
161                .map_err(|e| RuntimeError::ToolFailed(format!("recent_turns: {e}")))??;
162            if let Some(cb) = &ctx.on_memory_recent {
163                cb(msgs.len() as u16);
164            }
165            let items: Vec<Value> = msgs.into_iter().map(Value::Message).collect();
166            Ok(Value::Struct(vec![
167                ("total_message_count".into(), Value::Int(total as i64)),
168                ("items".into(), Value::List(items)),
169            ]))
170        })
171    }
172}
173
174pub struct MemoryGoalClear {
175    pub store: Arc<GoalStore>,
176}
177
178impl Tool for MemoryGoalClear {
179    fn name(&self) -> &str {
180        "memory.goal.clear"
181    }
182
183    fn tier(&self) -> Tier {
184        Tier::One
185    }
186
187    fn description(&self) -> Option<&str> {
188        Some(
189            "Clear the session goal. Call this when the task is complete or the user \
190             changes direction entirely. Returns nothing.",
191        )
192    }
193
194    fn input_schema(&self) -> serde_json::Value {
195        serde_json::json!({"type": "object"})
196    }
197
198    fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
199        Box::pin(async move {
200            self.store
201                .clear()
202                .map_err(|e| RuntimeError::ToolFailed(format!("goal.clear: {e}")))?;
203            Ok(Value::Unit)
204        })
205    }
206}
207
208pub struct MemoryTodoSet {
209    pub store: Arc<TodoStore>,
210}
211
212impl Tool for MemoryTodoSet {
213    fn name(&self) -> &str {
214        "memory.todo.set"
215    }
216
217    fn tier(&self) -> Tier {
218        Tier::One
219    }
220
221    fn description(&self) -> Option<&str> {
222        Some(
223            "Create a concrete execution todo. Returns the todo id (UUID string) — \
224             save it for memory.todo.done / memory.todo.cancel / memory.todo.delete.\n\n\
225             Todos are for short, trackable work items, usually inside the current \
226             plan step. Use plan.write/read/tick for the high-level ordered route \
227             through a multi-step task. Do not create todos that simply mirror plan \
228             steps; do not create a todo when one plan step is enough.\n\n\
229             Best practice: create a todo for each discrete execution item that \
230             should stay visible while you work. Keep `where` specific (file path \
231             or module), `why` one sentence, `how` a brief approach, \
232             `expected_result` the verification criteria. Don't create todos for \
233             trivial steps — only for things the user would want to track.\n\n\
234             To modify an existing todo, cancel the old one then create a new one. \
235             There is no update tool.",
236        )
237    }
238
239    fn input_schema(&self) -> serde_json::Value {
240        serde_json::json!({
241            "type": "object",
242            "properties": {
243                "where": {"type": "string", "description": "Where to do it (file path, module, etc.)"},
244                "why": {"type": "string", "description": "Why this needs doing"},
245                "how": {"type": "string", "description": "How to do it (brief approach)"},
246                "expected_result": {"type": "string", "description": "What success looks like"}
247            },
248            "required": ["where", "why", "how", "expected_result"]
249        })
250    }
251
252    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
253        Box::pin(async move {
254            let where_ = required_string(&args, "where")?;
255            let why = required_string(&args, "why")?;
256            let how = required_string(&args, "how")?;
257            let expected_result = required_string(&args, "expected_result")?;
258            let todo = Todo {
259                id: MemoryId::now(),
260                where_,
261                why,
262                how,
263                expected_result,
264                status: TodoStatus::Pending,
265            };
266            let id = self.store.add(todo).await?;
267            Ok(Value::Str(id.to_string()))
268        })
269    }
270}
271
272pub struct MemoryTodoDone {
273    pub store: Arc<TodoStore>,
274}
275
276impl Tool for MemoryTodoDone {
277    fn name(&self) -> &str {
278        "memory.todo.done"
279    }
280
281    fn tier(&self) -> Tier {
282        Tier::One
283    }
284
285    fn description(&self) -> Option<&str> {
286        Some(
287            "Mark a todo as done. Once done, a todo cannot be un-done. \
288             The id must be the UUID string returned by memory.todo.set. \
289             Returns \"ok\" on success (including if already done).",
290        )
291    }
292
293    fn input_schema(&self) -> serde_json::Value {
294        serde_json::json!({
295            "type": "object",
296            "properties": {
297                "id": {"type": "string", "description": "The UUID returned by memory.todo.set (e.g. \"019f5500-9a53-7800-8083-b608fdc4124a\")"}
298            },
299            "required": ["id"]
300        })
301    }
302
303    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
304        Box::pin(async move {
305            let id = required_string(&args, "id")?;
306            let uuid = uuid::Uuid::parse_str(&id).map_err(|e| {
307                RuntimeError::ToolFailed(format!(
308                    "bad todo id: {e}. The id must be the UUID returned by memory.todo.set."
309                ))
310            })?;
311            self.store
312                .set_status(&MemoryId(uuid), TodoStatus::Done)
313                .await?;
314            Ok(Value::Str("ok".into()))
315        })
316    }
317}
318
319pub struct MemoryTodoCancel {
320    pub store: Arc<TodoStore>,
321}
322
323impl Tool for MemoryTodoCancel {
324    fn name(&self) -> &str {
325        "memory.todo.cancel"
326    }
327
328    fn tier(&self) -> Tier {
329        Tier::One
330    }
331
332    fn description(&self) -> Option<&str> {
333        Some(
334            "Cancel a todo. Once cancelled, a todo cannot be re-activated. \
335             The id must be the UUID string returned by memory.todo.set. \
336             Returns \"ok\" on success (including if already cancelled).",
337        )
338    }
339
340    fn input_schema(&self) -> serde_json::Value {
341        serde_json::json!({
342            "type": "object",
343            "properties": {
344                "id": {"type": "string", "description": "The UUID returned by memory.todo.set"}
345            },
346            "required": ["id"]
347        })
348    }
349
350    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
351        Box::pin(async move {
352            let id = required_string(&args, "id")?;
353            let uuid = uuid::Uuid::parse_str(&id).map_err(|e| {
354                RuntimeError::ToolFailed(format!(
355                    "bad todo id: {e}. The id must be the UUID returned by memory.todo.set."
356                ))
357            })?;
358            self.store
359                .set_status(&MemoryId(uuid), TodoStatus::Cancelled)
360                .await?;
361            Ok(Value::Str("ok".into()))
362        })
363    }
364}
365
366pub struct MemoryTodoDelete {
367    pub store: Arc<TodoStore>,
368}
369
370impl Tool for MemoryTodoDelete {
371    fn name(&self) -> &str {
372        "memory.todo.delete"
373    }
374
375    fn tier(&self) -> Tier {
376        Tier::One
377    }
378
379    fn description(&self) -> Option<&str> {
380        Some(
381            "Permanently delete a todo. Unlike done/cancel, the todo is removed \
382             entirely from the list. Use for todos created by mistake. \
383             The id must be the UUID string returned by memory.todo.set.",
384        )
385    }
386
387    fn input_schema(&self) -> serde_json::Value {
388        serde_json::json!({
389            "type": "object",
390            "properties": {
391                "id": {"type": "string", "description": "The UUID returned by memory.todo.set"}
392            },
393            "required": ["id"]
394        })
395    }
396
397    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
398        Box::pin(async move {
399            let id = required_string(&args, "id")?;
400            let uuid = uuid::Uuid::parse_str(&id).map_err(|e| {
401                RuntimeError::ToolFailed(format!(
402                    "bad todo id: {e}. The id must be the UUID returned by memory.todo.set."
403                ))
404            })?;
405            self.store.delete(&MemoryId(uuid)).await?;
406            Ok(Value::Str("ok".into()))
407        })
408    }
409}
410
411pub struct MemoryTodoList {
412    pub store: Arc<TodoStore>,
413}
414
415impl Tool for MemoryTodoList {
416    fn name(&self) -> &str {
417        "memory.todo.list"
418    }
419
420    fn tier(&self) -> Tier {
421        Tier::Zero
422    }
423
424    fn description(&self) -> Option<&str> {
425        Some(
426            "List all todos in the current session. Returns an array of \
427             {id, where, why, how, expected_result, status}. \
428             status is one of: pending, done, cancelled. \
429             Call this to check concrete work items before starting or resuming a \
430             plan step.",
431        )
432    }
433
434    fn input_schema(&self) -> serde_json::Value {
435        serde_json::json!({"type": "object"})
436    }
437
438    fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
439        Box::pin(async move {
440            let todos = self.store.list().await?;
441            let items: Vec<Value> = todos
442                .into_iter()
443                .map(|t| {
444                    Value::Struct(vec![
445                        ("id".into(), Value::Str(t.id.to_string())),
446                        ("where".into(), Value::Str(t.where_)),
447                        ("why".into(), Value::Str(t.why)),
448                        ("how".into(), Value::Str(t.how)),
449                        ("expected_result".into(), Value::Str(t.expected_result)),
450                        (
451                            "status".into(),
452                            Value::Str(format!("{:?}", t.status).to_lowercase()),
453                        ),
454                    ])
455                })
456                .collect();
457            Ok(Value::List(items))
458        })
459    }
460}
461
462pub struct MemoryConfess {
463    pub store: Arc<ConfessionStore>,
464}
465
466impl Tool for MemoryConfess {
467    fn name(&self) -> &str {
468        "memory.confess"
469    }
470
471    fn tier(&self) -> Tier {
472        Tier::One
473    }
474
475    fn description(&self) -> Option<&str> {
476        Some(
477            "Record a confession when the agent broke a rule. Anchors are auto-filled from \
478             the current turn / flow_run / event_seq. Returns the new confession id.",
479        )
480    }
481
482    fn input_schema(&self) -> serde_json::Value {
483        serde_json::json!({
484            "type": "object",
485            "properties": {
486                "trigger": {"type": "string", "description": "What the user or watcher noticed."},
487                "rule_violated": {"type": "string", "description": "Name of the red-line rule."},
488                "what_i_did": {"type": "string", "description": "The concrete mistake."},
489                "why": {"type": "string", "description": "The reasoning that led there."},
490                "mitigation": {"type": "string", "description": "What will prevent recurrence."},
491                "anchors": {
492                    "type": "array",
493                    "items": {"type": "string"},
494                    "description": "Optional extra anchor strings (auto-filled ones stay)."
495                }
496            },
497            "required": ["trigger", "rule_violated", "what_i_did", "why", "mitigation"]
498        })
499    }
500
501    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
502        let anchors = collect_anchors(&args, ctx);
503        Box::pin(async move {
504            let trigger = required_string(&args, "trigger")?;
505            let rule_violated = required_string(&args, "rule_violated")?;
506            let what_i_did = required_string(&args, "what_i_did")?;
507            let why = required_string(&args, "why")?;
508            let mitigation = required_string(&args, "mitigation")?;
509            let confession = Confession {
510                id: MemoryId::now(),
511                trigger,
512                rule_violated,
513                what_i_did,
514                why,
515                mitigation,
516                anchors,
517                created_at: chrono::Utc::now(),
518            };
519            let id = self.store.append(confession).await?;
520            Ok(Value::Str(id.to_string()))
521        })
522    }
523}
524
525fn collect_anchors(args: &ToolArgs, ctx: &ToolCtx) -> Vec<String> {
526    let mut out = Vec::new();
527    if let Some(flow_run) = &ctx.flow_run_id {
528        out.push(format!("flow_run:{flow_run}"));
529    }
530    if let Some(turn) = &ctx.turn_id {
531        out.push(format!("turn:{turn}"));
532    }
533    if let Some(seq) = ctx.event_seq {
534        out.push(format!("event_seq:{seq}"));
535    }
536    if let Some(Value::List(items)) = args.named("anchors") {
537        for item in items {
538            if let Value::Str(s) = item {
539                out.push(s.clone());
540            }
541        }
542    }
543    out
544}
545
546pub struct MemorySpecStatus {
547    pub store: Arc<SpecStore>,
548}
549
550impl Tool for MemorySpecStatus {
551    fn name(&self) -> &str {
552        "memory.spec.status"
553    }
554    fn tier(&self) -> Tier {
555        Tier::Zero
556    }
557
558    fn description(&self) -> Option<&str> {
559        Some(
560            "Return progress counters for a named spec feature. Use it to check the current phase, update count, and deviation count before continuing spec-driven work.",
561        )
562    }
563
564    fn input_schema(&self) -> serde_json::Value {
565        serde_json::json!({
566            "type": "object",
567            "properties": {
568                "feature": {"type": "string", "description": "Spec feature name to inspect."}
569            },
570            "required": ["feature"]
571        })
572    }
573
574    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
575        Box::pin(async move {
576            let feature = required_string(&args, "feature")?;
577            let st = self.store.status(&feature).await?;
578            Ok(Value::Struct(vec![
579                ("feature".into(), Value::Str(st.feature)),
580                ("phase".into(), Value::Str(st.phase)),
581                ("entry_count".into(), Value::Int(st.entry_count as i64)),
582                (
583                    "deviation_count".into(),
584                    Value::Int(st.deviation_count as i64),
585                ),
586            ]))
587        })
588    }
589}
590
591pub struct MemorySpecUpdate {
592    pub store: Arc<SpecStore>,
593}
594
595impl Tool for MemorySpecUpdate {
596    fn name(&self) -> &str {
597        "memory.spec.update"
598    }
599    fn tier(&self) -> Tier {
600        Tier::One
601    }
602
603    fn description(&self) -> Option<&str> {
604        Some(
605            "Append a progress entry for a spec feature and phase. Use it to persist research, design, implementation, or verification notes as spec work advances.",
606        )
607    }
608
609    fn input_schema(&self) -> serde_json::Value {
610        serde_json::json!({
611            "type": "object",
612            "properties": {
613                "feature": {"type": "string", "description": "Spec feature name to update."},
614                "phase": {"type": "string", "description": "Spec phase or section name, such as research, design, implementation, or verification."},
615                "content": {"type": "string", "description": "Progress entry content to append."}
616            },
617            "required": ["feature", "phase", "content"]
618        })
619    }
620
621    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
622        Box::pin(async move {
623            let feature = required_string(&args, "feature")?;
624            let phase = required_string(&args, "phase")?;
625            let content = required_string(&args, "content")?;
626            let entry = self.store.update(&feature, &phase, content).await?;
627            Ok(Value::Struct(vec![
628                ("id".into(), Value::Str(entry.id.to_string())),
629                ("feature".into(), Value::Str(entry.feature)),
630                ("phase".into(), Value::Str(entry.phase)),
631            ]))
632        })
633    }
634}
635
636pub struct MemorySpecDeviate {
637    pub store: Arc<SpecStore>,
638}
639
640impl Tool for MemorySpecDeviate {
641    fn name(&self) -> &str {
642        "memory.spec.deviate"
643    }
644    fn tier(&self) -> Tier {
645        Tier::One
646    }
647
648    fn description(&self) -> Option<&str> {
649        Some(
650            "Record an intentional deviation from a spec section. Use it when implementation differs from the written plan and the delta plus reason must be preserved.",
651        )
652    }
653
654    fn input_schema(&self) -> serde_json::Value {
655        serde_json::json!({
656            "type": "object",
657            "properties": {
658                "feature": {"type": "string", "description": "Spec feature name that owns the deviation."},
659                "section": {"type": "string", "description": "Spec section or decision being changed."},
660                "delta": {"type": "string", "description": "What changed from the spec."},
661                "reason": {"type": "string", "description": "Why the deviation is necessary."}
662            },
663            "required": ["feature", "section", "delta", "reason"]
664        })
665    }
666
667    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
668        Box::pin(async move {
669            let feature = required_string(&args, "feature")?;
670            let section = required_string(&args, "section")?;
671            let delta = required_string(&args, "delta")?;
672            let reason = required_string(&args, "reason")?;
673            let dev = self.store.deviate(&feature, section, delta, reason).await?;
674            Ok(Value::Struct(vec![
675                ("id".into(), Value::Str(dev.id.to_string())),
676                ("feature".into(), Value::Str(dev.feature)),
677                ("section".into(), Value::Str(dev.section)),
678            ]))
679        })
680    }
681}
682
683pub struct MemoryFetchConfessions {
684    pub store: Arc<ConfessionStore>,
685}
686
687impl Tool for MemoryFetchConfessions {
688    fn name(&self) -> &str {
689        "memory.fetch_confessions"
690    }
691
692    fn tier(&self) -> Tier {
693        Tier::Zero
694    }
695
696    fn description(&self) -> Option<&str> {
697        Some(
698            "Fetch past confession records about rule violations, optionally filtered by trigger text. Use it to recall prior mistakes and mitigations before repeating risky work.",
699        )
700    }
701
702    fn input_schema(&self) -> serde_json::Value {
703        serde_json::json!({
704            "type": "object",
705            "properties": {
706                "trigger": {"type": "string", "description": "Optional trigger substring to search for; omit to list all confession records."}
707            }
708        })
709    }
710
711    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
712        Box::pin(async move {
713            let items = match args.named("trigger") {
714                Some(Value::Str(needle)) => self.store.find_by_trigger(needle).await?,
715                _ => self.store.list().await?,
716            };
717            let list = items
718                .into_iter()
719                .map(|c| {
720                    Value::Struct(vec![
721                        ("id".into(), Value::Str(c.id.to_string())),
722                        ("trigger".into(), Value::Str(c.trigger)),
723                        ("rule_violated".into(), Value::Str(c.rule_violated)),
724                        ("what_i_did".into(), Value::Str(c.what_i_did)),
725                        ("why".into(), Value::Str(c.why)),
726                        ("mitigation".into(), Value::Str(c.mitigation)),
727                    ])
728                })
729                .collect();
730            Ok(Value::List(list))
731        })
732    }
733}
734
735pub struct MemoryHistorySearch;
736
737impl Tool for MemoryHistorySearch {
738    fn name(&self) -> &str {
739        "memory.history.search"
740    }
741
742    fn tier(&self) -> Tier {
743        Tier::Zero
744    }
745
746    fn description(&self) -> Option<&str> {
747        Some(
748            "Full-text search the current session's chat history (or optionally every session \
749             in the same project). Use it to recall past turns that fell out of your working \
750             context — e.g. `plan we agreed on this morning`, `which files did we read`, \
751             `error the user reported earlier`. NOT for searching source code; use fs.grep for \
752             that. Params: query (FTS5 syntax, required), scope (\"session\"|\"project\", \
753             default \"session\"), limit (int, default 10, max 50).",
754        )
755    }
756
757    fn input_schema(&self) -> serde_json::Value {
758        serde_json::json!({
759            "type": "object",
760            "properties": {
761                "query": {"type": "string"},
762                "scope": {"type": "string", "enum": ["session", "project"], "default": "session"},
763                "limit": {"type": "integer", "default": 10}
764            },
765            "required": ["query"]
766        })
767    }
768
769    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
770        Box::pin(async move {
771            let query = required_string(&args, "query")?;
772            let scope = match args.named("scope") {
773                Some(Value::Str(s)) if s == "project" => HistoryScope::Project,
774                _ => HistoryScope::Session,
775            };
776            let limit = match args.named("limit") {
777                Some(Value::Int(n)) if *n > 0 => (*n as usize).min(50),
778                _ => 10,
779            };
780            let Some(store) = ctx.history_store.clone() else {
781                return Err(RuntimeError::ToolFailed(
782                    "memory.history.search: no history store on context".into(),
783                ));
784            };
785            let search_scope = match scope {
786                HistoryScope::Project => crate::history_store::SearchScope::Project,
787                HistoryScope::Session => crate::history_store::SearchScope::Session,
788            };
789            let result =
790                tokio::task::spawn_blocking(move || store.search(&query, search_scope, limit))
791                    .await
792                    .map_err(|e| RuntimeError::ToolFailed(format!("history.search: {e}")))??;
793            let hits: Vec<Value> = result
794                .hits
795                .into_iter()
796                .map(|hit| {
797                    Value::Struct(vec![
798                        ("session_id".into(), Value::Str(hit.session_id)),
799                        ("seq".into(), Value::Int(hit.seq as i64)),
800                        ("ts".into(), Value::Str(hit.ts)),
801                        ("kind".into(), Value::Str(hit.kind)),
802                        ("snippet".into(), Value::Str(hit.snippet)),
803                    ])
804                })
805                .collect();
806            Ok(Value::Struct(vec![
807                ("total".into(), Value::Int(result.total as i64)),
808                ("hits".into(), Value::List(hits)),
809            ]))
810        })
811    }
812}
813
814pub struct MemoryHistoryRead;
815
816impl Tool for MemoryHistoryRead {
817    fn name(&self) -> &str {
818        "memory.history.read"
819    }
820
821    fn tier(&self) -> Tier {
822        Tier::Zero
823    }
824
825    fn description(&self) -> Option<&str> {
826        Some(
827            "Paginate through past messages of a session by turn index. Prefer \
828             memory.history.search first to find a hit, then call this for surrounding context. \
829             Params: session_id (string, default current session's directory name), offset \
830             (1-based turn index, default 1), limit (int, default 20, max 100), role_filter \
831             (comma-separated: user,assistant,tool,system; default all).",
832        )
833    }
834
835    fn input_schema(&self) -> serde_json::Value {
836        serde_json::json!({
837            "type": "object",
838            "properties": {
839                "session_id": {"type": "string"},
840                "offset": {"type": "integer", "default": 1},
841                "limit": {"type": "integer", "default": 20},
842                "role_filter": {"type": "string"}
843            }
844        })
845    }
846
847    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
848        Box::pin(async move {
849            let Some(current_dir) = ctx.session_dir.as_ref() else {
850                return Err(RuntimeError::ToolFailed(
851                    "memory.history.read: no session dir on context".into(),
852                ));
853            };
854            let session_id = match args.named("session_id") {
855                Some(Value::Str(sid)) if !sid.is_empty() => sid.clone(),
856                _ => current_dir
857                    .file_name()
858                    .map(|n| n.to_string_lossy().into_owned())
859                    .unwrap_or_default(),
860            };
861            let offset = match args.named("offset") {
862                Some(Value::Int(n)) if *n >= 1 => *n as usize,
863                _ => 1,
864            };
865            let limit = match args.named("limit") {
866                Some(Value::Int(n)) if *n >= 1 => (*n as usize).min(100),
867                _ => 20,
868            };
869            let role_filter: Option<Vec<String>> = match args.named("role_filter") {
870                Some(Value::Str(s)) if !s.is_empty() => Some(
871                    s.split(',')
872                        .map(|t| t.trim().to_lowercase())
873                        .filter(|t| !t.is_empty())
874                        .collect(),
875                ),
876                _ => None,
877            };
878            let Some(store) = ctx.history_store.clone() else {
879                return Err(RuntimeError::ToolFailed(
880                    "memory.history.read: no history store on context".into(),
881                ));
882            };
883            let query = crate::history_store::HistoryQuery {
884                session_id,
885                offset,
886                limit,
887                role_filter,
888            };
889            let page = tokio::task::spawn_blocking(move || store.read(query))
890                .await
891                .map_err(|e| RuntimeError::ToolFailed(format!("history.read: {e}")))??;
892            let item_count = page.items.len();
893            let items: Vec<Value> = page.items.into_iter().map(Value::Message).collect();
894            let start = offset;
895            let end = if item_count == 0 {
896                start
897            } else {
898                start + item_count - 1
899            };
900            let header = format!("[history: turns {start}-{end} of {}]", page.total);
901            Ok(Value::Struct(vec![
902                ("total".into(), Value::Int(page.total as i64)),
903                ("offset".into(), Value::Int(page.offset as i64)),
904                ("limit".into(), Value::Int(page.limit as i64)),
905                ("header".into(), Value::Str(header)),
906                ("items".into(), Value::List(items)),
907            ]))
908        })
909    }
910}
911
912pub struct MemoryHistoryCount;
913
914impl Tool for MemoryHistoryCount {
915    fn name(&self) -> &str {
916        "memory.history.count"
917    }
918
919    fn tier(&self) -> Tier {
920        Tier::Zero
921    }
922
923    fn description(&self) -> Option<&str> {
924        Some(
925            "Return the total message count for a session. Lightweight — use this to check \
926             how many messages exist before paginating with memory.history.read. \
927             Params: session_id (string, default current session), role_filter \
928             (comma-separated: user,assistant,tool,system; default all).",
929        )
930    }
931
932    fn input_schema(&self) -> serde_json::Value {
933        serde_json::json!({
934            "type": "object",
935            "properties": {
936                "session_id": {"type": "string"},
937                "role_filter": {"type": "string"}
938            }
939        })
940    }
941
942    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
943        Box::pin(async move {
944            let Some(current_dir) = ctx.session_dir.as_ref() else {
945                return Err(RuntimeError::ToolFailed(
946                    "memory.history.count: no session dir on context".into(),
947                ));
948            };
949            let session_id = match args.named("session_id") {
950                Some(Value::Str(sid)) if !sid.is_empty() => sid.clone(),
951                _ => current_dir
952                    .file_name()
953                    .map(|n| n.to_string_lossy().into_owned())
954                    .unwrap_or_default(),
955            };
956            let role_filter: Option<Vec<String>> = match args.named("role_filter") {
957                Some(Value::Str(s)) if !s.is_empty() => Some(
958                    s.split(',')
959                        .map(|t| t.trim().to_lowercase())
960                        .filter(|t| !t.is_empty())
961                        .collect(),
962                ),
963                _ => None,
964            };
965            let Some(store) = ctx.history_store.clone() else {
966                return Err(RuntimeError::ToolFailed(
967                    "memory.history.count: no history store on context".into(),
968                ));
969            };
970            let total = tokio::task::spawn_blocking(move || {
971                let role_refs: Option<Vec<&str>> = role_filter
972                    .as_ref()
973                    .map(|rs| rs.iter().map(|s| s.as_str()).collect());
974                store.count(&session_id, role_refs.as_deref())
975            })
976            .await
977            .map_err(|e| RuntimeError::ToolFailed(format!("history.count: {e}")))??;
978            Ok(Value::Int(total as i64))
979        })
980    }
981}
982
983enum HistoryScope {
984    Session,
985    Project,
986}
987
988fn required_string(args: &ToolArgs, name: &str) -> Result<String, RuntimeError> {
989    match args.named(name) {
990        Some(Value::Str(s)) => Ok(s.clone()),
991        Some(other) => Err(RuntimeError::TypeMismatch {
992            expected: "string".into(),
993            actual: other.kind_name().into(),
994        }),
995        None => Err(RuntimeError::MissingArg(name.into())),
996    }
997}