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