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