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 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 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}