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