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