use std::sync::Arc;
use crate::error::RuntimeError;
use crate::memory::MemoryId;
use crate::memory::confession::{Confession, ConfessionStore};
use crate::memory::goal::GoalStore;
use crate::memory::spec::SpecStore;
use crate::memory::todo::{Todo, TodoStatus, TodoStore};
use crate::tool::{BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
use crate::value::Value;
pub struct MemoryGoalGet {
pub store: Arc<GoalStore>,
}
impl Tool for MemoryGoalGet {
fn name(&self) -> &str {
"memory.goal.get"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"Return the current session goal (persistent, exposed to models as an append-only context record). Empty string when unset.",
)
}
fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let text = self
.store
.get()
.map_err(|e| RuntimeError::ToolFailed(format!("goal.get: {e}")))?;
Ok(Value::Str(text))
})
}
}
pub struct MemoryGoalSet {
pub store: Arc<GoalStore>,
}
impl Tool for MemoryGoalSet {
fn name(&self) -> &str {
"memory.goal.set"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Set the session goal — a short directive (1-2 sentences) that atman appends \
to model history as a versioned context record. It persists across turns; \
unchanged content is not appended again.\n\n\
Best practice: set the goal early (right after understanding the user's request), \
keep it concise and actionable. Update it if the user's intent changes. Clear it \
when the task is complete. Example: 'Fix the login bug in auth.rs and add a \
regression test.'",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The goal text, 1-2 sentences. Be specific: what to do, where, and what 'done' looks like."
}
},
"required": ["text"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let text = required_string(&args, "text")?;
self.store
.set(&text)
.map_err(|e| RuntimeError::ToolFailed(format!("goal.set: {e}")))?;
Ok(Value::Unit)
})
}
}
pub struct MemoryRecentTurns;
const RECENT_EXCERPT_MESSAGE_CHARS: usize = 2_000;
#[derive(Clone, Copy)]
enum RecentExcerpt {
LegacyRecent(usize),
HeadTail { head: usize, tail: usize },
}
fn recent_turns_value(
message_count: u64,
turn_count: u64,
messages: Vec<crate::message::Message>,
excerpt: Option<RecentExcerpt>,
) -> Value {
let excerpt = excerpt.map(|mode| match mode {
RecentExcerpt::LegacyRecent(limit) => bounded_recent_excerpt(&messages, limit),
RecentExcerpt::HeadTail { head, tail } => bounded_head_tail_excerpt(&messages, head, tail),
});
let items = messages.into_iter().map(Value::Message).collect();
let mut fields = vec![
(
"total_message_count".into(),
Value::Int(message_count as i64),
),
("total_turn_count".into(), Value::Int(turn_count as i64)),
("items".into(), Value::List(items)),
];
if let Some((text, truncated)) = excerpt {
fields.push(("excerpt".into(), Value::Str(text)));
fields.push(("excerpt_truncated".into(), Value::Bool(truncated)));
}
Value::Struct(fields)
}
#[derive(Default)]
struct HeadTailExcerpt {
head: String,
tail: std::collections::VecDeque<char>,
head_chars: usize,
head_limit: usize,
tail_limit: usize,
total_chars: usize,
}
impl HeadTailExcerpt {
fn new(head_limit: usize, tail_limit: usize) -> Self {
Self {
head_limit,
tail_limit,
..Self::default()
}
}
fn push(&mut self, text: &str) {
for ch in text.chars() {
if self.head_chars < self.head_limit {
self.head.push(ch);
self.head_chars += 1;
}
if self.tail_limit > 0 {
if self.tail.len() == self.tail_limit {
self.tail.pop_front();
}
self.tail.push_back(ch);
}
self.total_chars += 1;
}
}
fn finish(self) -> (String, bool) {
let head_chars = self.head_chars;
let tail_chars = self.tail.len();
let overlap = head_chars
.saturating_add(tail_chars)
.saturating_sub(self.total_chars);
let tail = self.tail.into_iter().skip(overlap).collect::<String>();
let truncated = self.total_chars > head_chars.saturating_add(tail_chars);
if !truncated {
return (self.head + &tail, false);
}
if self.head.is_empty() {
return (tail, true);
}
if tail.is_empty() {
return (self.head, true);
}
let omitted = self
.total_chars
.saturating_sub(head_chars.saturating_add(tail_chars));
(
format!(
"{}\n\n[... omitted {omitted} chars ...]\n\n{tail}",
self.head
),
true,
)
}
}
fn visit_message_excerpt_segments(
message: &crate::message::Message,
mut visit: impl FnMut(&str) -> bool,
) {
use crate::message::MessagePart;
if visit(&format!("[{}]", message.role.as_str())) {
return;
}
for part in &message.parts {
let stop = match part {
MessagePart::FinalAnswerSummary { .. } => false,
MessagePart::ContextRecord(record) => {
let label = format!("\n[context {}@{}]\n", record.key(), record.revision());
visit(&label) || visit(&record.render_for_model())
}
MessagePart::CompactSummary { summary, .. } => visit("\nsummary: ") || visit(summary),
MessagePart::Text { text } => visit("\ntext: ") || visit(text),
MessagePart::Thinking { .. } => visit("\n[thinking omitted]"),
MessagePart::Image { .. } => visit("\n[image]"),
MessagePart::ToolUse { name, intent, .. } => {
visit("\ntool_call: ")
|| visit(name)
|| intent
.as_ref()
.is_some_and(|intent| visit(" — ") || visit(intent.as_str()))
}
MessagePart::ToolResult {
tool_use_id,
content,
is_error,
} => {
let status = if *is_error { "error" } else { "ok" };
visit(&format!("\ntool_result {tool_use_id} ({status}): ")) || visit(content)
}
};
if stop {
return;
}
}
}
fn bounded_head_tail_excerpt(
messages: &[crate::message::Message],
head_chars: usize,
tail_chars: usize,
) -> (String, bool) {
let mut excerpt = HeadTailExcerpt::new(head_chars, tail_chars);
for (index, message) in messages.iter().enumerate() {
if index > 0 {
excerpt.push("\n\n");
}
visit_message_excerpt_segments(message, |segment| {
excerpt.push(segment);
false
});
}
excerpt.finish()
}
fn non_negative_excerpt_field(value: &Value, name: &str) -> Result<usize, RuntimeError> {
match value.field(name) {
Some(Value::Int(value)) if *value >= 0 => Ok(*value as usize),
Some(other) => Err(RuntimeError::TypeMismatch {
expected: format!("non-negative int for excerpt.{name}"),
actual: other.kind_name().into(),
}),
None => Ok(0),
}
}
fn recent_excerpt_arg(args: &ToolArgs) -> Result<Option<RecentExcerpt>, RuntimeError> {
let legacy = args.named("excerpt_chars");
let head_tail = args.named("excerpt");
if legacy.is_some() && head_tail.is_some() {
return Err(RuntimeError::ToolFailed(
"memory.recent_turns: choose `excerpt` or `excerpt_chars`, not both".into(),
));
}
if let Some(value) = head_tail {
let Value::Struct(_) = value else {
return Err(RuntimeError::TypeMismatch {
expected: "struct with non-negative `head` and/or `tail`".into(),
actual: value.kind_name().into(),
});
};
return Ok(Some(RecentExcerpt::HeadTail {
head: non_negative_excerpt_field(value, "head")?,
tail: non_negative_excerpt_field(value, "tail")?,
}));
}
match legacy {
Some(Value::Int(value)) if *value >= 0 => {
Ok(Some(RecentExcerpt::LegacyRecent(*value as usize)))
}
Some(other) => Err(RuntimeError::TypeMismatch {
expected: "non-negative int".into(),
actual: other.kind_name().into(),
}),
None => Ok(None),
}
}
fn bounded_recent_excerpt(
messages: &[crate::message::Message],
max_chars: usize,
) -> (String, bool) {
let mut remaining = max_chars;
let mut chunks = Vec::new();
let mut truncated = false;
for message in messages.iter().rev() {
let separator_chars = usize::from(!chunks.is_empty()) * 2;
if remaining <= separator_chars {
truncated = true;
break;
}
let message_limit = remaining
.saturating_sub(separator_chars)
.min(RECENT_EXCERPT_MESSAGE_CHARS);
let (chunk, message_truncated) = bounded_message_excerpt(message, message_limit);
remaining = remaining.saturating_sub(separator_chars + chunk.chars().count());
chunks.push(chunk);
truncated |= message_truncated;
}
if chunks.len() < messages.len() {
truncated = true;
}
chunks.reverse();
(chunks.join("\n\n"), truncated)
}
fn bounded_message_excerpt(message: &crate::message::Message, max_chars: usize) -> (String, bool) {
fn push_bounded(out: &mut String, used: &mut usize, max: usize, text: &str) -> bool {
for ch in text.chars() {
if *used == max {
return true;
}
out.push(ch);
*used += 1;
}
false
}
let mut out = String::new();
let mut used = 0;
let mut truncated = false;
visit_message_excerpt_segments(message, |segment| {
let segment_truncated = push_bounded(&mut out, &mut used, max_chars, segment);
truncated |= segment_truncated;
segment_truncated
});
(out, truncated)
}
impl Tool for MemoryRecentTurns {
fn name(&self) -> &str {
"memory.recent_turns"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"Return the last N Message values (user + assistant + tool_result) from the \
current session's event log so a flow can hand the code agent a sliding \
history window. `items` remain lossless; `excerpt: {head, tail}` returns a \
bounded text excerpt retaining independently selected transcript edges. \
`excerpt_chars` remains a legacy recent-first budget. Reads from disk; cost \
O(events file size).",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"n": {"type": "integer", "description": "Max complete turns to return (default 10)"},
"excerpt": {
"type": "object",
"description": "Also return an `excerpt` retaining independently bounded transcript edges without changing lossless `items`.",
"properties": {
"head": {"type": "integer", "minimum": 0, "description": "Characters retained from the start of the selected transcript."},
"tail": {"type": "integer", "minimum": 0, "description": "Characters retained from the end of the selected transcript."}
},
"additionalProperties": false
},
"excerpt_chars": {"type": "integer", "minimum": 0, "description": "Legacy recent-first excerpt budget; cannot be combined with `excerpt`."}
}
})
}
fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let n = match args.named("n").or_else(|| args.positional(0).ok()) {
Some(Value::Int(k)) if *k >= 0 => *k as usize,
Some(other) => {
return Err(RuntimeError::TypeMismatch {
expected: "non-negative int".into(),
actual: other.kind_name().into(),
});
}
None => 10,
};
let excerpt = recent_excerpt_arg(&args)?;
if n == 0 {
if let Some(cb) = &ctx.on_memory_recent {
cb(0);
}
return Ok(recent_turns_value(0, 0, Vec::new(), excerpt));
}
if let Some(msgs) = ctx.session_messages.as_ref() {
let (total, recent) = crate::history_store::recent_turn_messages(msgs, n);
if let Some(cb) = &ctx.on_memory_recent {
cb(recent.len() as u16);
}
return Ok(recent_turns_value(
msgs.len() as u64,
total,
recent,
excerpt,
));
}
let Some(store) = ctx.history_store.clone() else {
return Err(RuntimeError::ToolFailed(
"memory.recent_turns: no history store on context".into(),
));
};
let (message_count, turn_count, msgs) =
tokio::task::spawn_blocking(move || store.recent(n))
.await
.map_err(|e| RuntimeError::ToolFailed(format!("recent_turns: {e}")))??;
if let Some(cb) = &ctx.on_memory_recent {
cb(msgs.len() as u16);
}
Ok(recent_turns_value(message_count, turn_count, msgs, excerpt))
})
}
}
pub struct MemoryGoalClear {
pub store: Arc<GoalStore>,
}
impl Tool for MemoryGoalClear {
fn name(&self) -> &str {
"memory.goal.clear"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Clear the session goal. Call this when the task is complete or the user \
changes direction entirely. Returns nothing.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({"type": "object"})
}
fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
self.store
.clear()
.map_err(|e| RuntimeError::ToolFailed(format!("goal.clear: {e}")))?;
Ok(Value::Unit)
})
}
}
pub struct MemoryTodoSet {
pub store: Arc<TodoStore>,
}
impl Tool for MemoryTodoSet {
fn name(&self) -> &str {
"memory.todo.set"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Create a concrete execution todo. Returns the todo id (UUID string) — \
save it for memory.todo.done / memory.todo.cancel / memory.todo.delete.\n\n\
Todos are for short, trackable work items, usually inside the current \
plan step. Use plan.write/read/tick for the high-level ordered route \
through a multi-step task. Do not create todos that simply mirror plan \
steps; do not create a todo when one plan step is enough.\n\n\
Best practice: create a todo for each discrete execution item that \
should stay visible while you work. Keep `where` specific (file path \
or module), `why` one sentence, `how` a brief approach, \
`expected_result` the verification criteria. Don't create todos for \
trivial steps — only for things the user would want to track.\n\n\
To modify an existing todo, cancel the old one then create a new one. \
There is no update tool.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"where": {"type": "string", "description": "Where to do it (file path, module, etc.)"},
"why": {"type": "string", "description": "Why this needs doing"},
"how": {"type": "string", "description": "How to do it (brief approach)"},
"expected_result": {"type": "string", "description": "What success looks like"}
},
"required": ["where", "why", "how", "expected_result"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let where_ = required_string(&args, "where")?;
let why = required_string(&args, "why")?;
let how = required_string(&args, "how")?;
let expected_result = required_string(&args, "expected_result")?;
let todo = Todo {
id: MemoryId::now(),
where_,
why,
how,
expected_result,
status: TodoStatus::Pending,
};
let id = self.store.add(todo).await?;
Ok(Value::Str(id.to_string()))
})
}
}
pub struct MemoryTodoDone {
pub store: Arc<TodoStore>,
}
impl Tool for MemoryTodoDone {
fn name(&self) -> &str {
"memory.todo.done"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Mark a todo as done. Once done, a todo cannot be un-done. \
The id must be the UUID string returned by memory.todo.set. \
Returns \"ok\" on success (including if already done).",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"id": {"type": "string", "description": "The UUID returned by memory.todo.set (e.g. \"019f5500-9a53-7800-8083-b608fdc4124a\")"}
},
"required": ["id"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let id = required_string(&args, "id")?;
let uuid = uuid::Uuid::parse_str(&id).map_err(|e| {
RuntimeError::ToolFailed(format!(
"bad todo id: {e}. The id must be the UUID returned by memory.todo.set."
))
})?;
self.store
.set_status(&MemoryId(uuid), TodoStatus::Done)
.await?;
Ok(Value::Str("ok".into()))
})
}
}
pub struct MemoryTodoCancel {
pub store: Arc<TodoStore>,
}
impl Tool for MemoryTodoCancel {
fn name(&self) -> &str {
"memory.todo.cancel"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Cancel a todo. Once cancelled, a todo cannot be re-activated. \
The id must be the UUID string returned by memory.todo.set. \
Returns \"ok\" on success (including if already cancelled).",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"id": {"type": "string", "description": "The UUID returned by memory.todo.set"}
},
"required": ["id"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let id = required_string(&args, "id")?;
let uuid = uuid::Uuid::parse_str(&id).map_err(|e| {
RuntimeError::ToolFailed(format!(
"bad todo id: {e}. The id must be the UUID returned by memory.todo.set."
))
})?;
self.store
.set_status(&MemoryId(uuid), TodoStatus::Cancelled)
.await?;
Ok(Value::Str("ok".into()))
})
}
}
pub struct MemoryTodoDelete {
pub store: Arc<TodoStore>,
}
impl Tool for MemoryTodoDelete {
fn name(&self) -> &str {
"memory.todo.delete"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Permanently delete a todo. Unlike done/cancel, the todo is removed \
entirely from the list. Use for todos created by mistake. \
The id must be the UUID string returned by memory.todo.set.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"id": {"type": "string", "description": "The UUID returned by memory.todo.set"}
},
"required": ["id"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let id = required_string(&args, "id")?;
let uuid = uuid::Uuid::parse_str(&id).map_err(|e| {
RuntimeError::ToolFailed(format!(
"bad todo id: {e}. The id must be the UUID returned by memory.todo.set."
))
})?;
self.store.delete(&MemoryId(uuid)).await?;
Ok(Value::Str("ok".into()))
})
}
}
pub struct MemoryTodoList {
pub store: Arc<TodoStore>,
}
impl Tool for MemoryTodoList {
fn name(&self) -> &str {
"memory.todo.list"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"List all todos in the current session. Returns an array of \
{id, where, why, how, expected_result, status}. \
status is one of: pending, done, cancelled. \
Call this to check concrete work items before starting or resuming a \
plan step.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({"type": "object"})
}
fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let todos = self.store.list().await?;
let items: Vec<Value> = todos
.into_iter()
.map(|t| {
Value::Struct(vec![
("id".into(), Value::Str(t.id.to_string())),
("where".into(), Value::Str(t.where_)),
("why".into(), Value::Str(t.why)),
("how".into(), Value::Str(t.how)),
("expected_result".into(), Value::Str(t.expected_result)),
(
"status".into(),
Value::Str(format!("{:?}", t.status).to_lowercase()),
),
])
})
.collect();
Ok(Value::List(items))
})
}
}
pub struct MemoryConfess {
pub store: Arc<ConfessionStore>,
}
impl Tool for MemoryConfess {
fn name(&self) -> &str {
"memory.confess"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Record a confession when the agent broke a rule. Anchors are auto-filled from \
the current turn / flow_run / event_seq. Returns the new confession id.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"trigger": {"type": "string", "description": "What the user or watcher noticed."},
"rule_violated": {"type": "string", "description": "Name of the red-line rule."},
"what_i_did": {"type": "string", "description": "The concrete mistake."},
"why": {"type": "string", "description": "The reasoning that led there."},
"mitigation": {"type": "string", "description": "What will prevent recurrence."},
"anchors": {
"type": "array",
"items": {"type": "string"},
"description": "Optional extra anchor strings (auto-filled ones stay)."
}
},
"required": ["trigger", "rule_violated", "what_i_did", "why", "mitigation"]
})
}
fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
let anchors = collect_anchors(&args, ctx);
Box::pin(async move {
let trigger = required_string(&args, "trigger")?;
let rule_violated = required_string(&args, "rule_violated")?;
let what_i_did = required_string(&args, "what_i_did")?;
let why = required_string(&args, "why")?;
let mitigation = required_string(&args, "mitigation")?;
let confession = Confession {
id: MemoryId::now(),
trigger,
rule_violated,
what_i_did,
why,
mitigation,
anchors,
created_at: chrono::Utc::now(),
};
let id = self.store.append(confession).await?;
Ok(Value::Str(id.to_string()))
})
}
}
fn collect_anchors(args: &ToolArgs, ctx: &ToolCtx) -> Vec<String> {
let mut out = Vec::new();
if let Some(flow_run) = &ctx.flow_run_id {
out.push(format!("flow_run:{flow_run}"));
}
if let Some(turn) = &ctx.turn_id {
out.push(format!("turn:{turn}"));
}
if let Some(seq) = ctx.event_seq {
out.push(format!("event_seq:{seq}"));
}
if let Some(Value::List(items)) = args.named("anchors") {
for item in items {
if let Value::Str(s) = item {
out.push(s.clone());
}
}
}
out
}
pub struct MemorySpecStatus {
pub store: Arc<SpecStore>,
}
impl Tool for MemorySpecStatus {
fn name(&self) -> &str {
"memory.spec.status"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"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.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"feature": {"type": "string", "description": "Spec feature name to inspect."}
},
"required": ["feature"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let feature = required_string(&args, "feature")?;
let st = self.store.status(&feature).await?;
Ok(Value::Struct(vec![
("feature".into(), Value::Str(st.feature)),
("phase".into(), Value::Str(st.phase)),
("entry_count".into(), Value::Int(st.entry_count as i64)),
(
"deviation_count".into(),
Value::Int(st.deviation_count as i64),
),
]))
})
}
}
pub struct MemorySpecMaterialize {
pub store: Arc<SpecStore>,
}
impl Tool for MemorySpecMaterialize {
fn name(&self) -> &str {
"memory.spec.materialize"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some("Materialize runtime JSONL spec state to Markdown with revision conflict protection.")
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"feature": {"type": "string"},
"expected_revision": {"type": "string"}
},
"required": ["feature"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let feature = required_string(&args, "feature")?;
let expected = match args.named("expected_revision") {
Some(Value::Str(value)) => Some(value.as_str()),
_ => None,
};
let result = self.store.materialize(&feature, expected).await?;
Ok(Value::Struct(vec![
("path".into(), Value::Str(result.path.display().to_string())),
("revision".into(), Value::Str(result.revision)),
("changed".into(), Value::Bool(result.changed)),
]))
})
}
}
pub struct MemorySpecUpdate {
pub store: Arc<SpecStore>,
}
impl Tool for MemorySpecUpdate {
fn name(&self) -> &str {
"memory.spec.update"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"Append a progress entry for a spec feature and phase. Use it to persist research, design, implementation, or verification notes as spec work advances.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"feature": {"type": "string", "description": "Spec feature name to update."},
"phase": {"type": "string", "description": "Spec phase or section name, such as research, design, implementation, or verification."},
"content": {"type": "string", "description": "Progress entry content to append."}
},
"required": ["feature", "phase", "content"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let feature = required_string(&args, "feature")?;
let phase = required_string(&args, "phase")?;
let content = required_string(&args, "content")?;
let entry = self.store.update(&feature, &phase, content).await?;
Ok(Value::Struct(vec![
("id".into(), Value::Str(entry.id.to_string())),
("feature".into(), Value::Str(entry.feature)),
("phase".into(), Value::Str(entry.phase)),
]))
})
}
}
pub struct MemorySpecDeviate {
pub store: Arc<SpecStore>,
}
impl Tool for MemorySpecDeviate {
fn name(&self) -> &str {
"memory.spec.deviate"
}
fn tier(&self) -> Tier {
Tier::One
}
fn description(&self) -> Option<&str> {
Some(
"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.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"feature": {"type": "string", "description": "Spec feature name that owns the deviation."},
"section": {"type": "string", "description": "Spec section or decision being changed."},
"delta": {"type": "string", "description": "What changed from the spec."},
"reason": {"type": "string", "description": "Why the deviation is necessary."}
},
"required": ["feature", "section", "delta", "reason"]
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let feature = required_string(&args, "feature")?;
let section = required_string(&args, "section")?;
let delta = required_string(&args, "delta")?;
let reason = required_string(&args, "reason")?;
let dev = self.store.deviate(&feature, section, delta, reason).await?;
Ok(Value::Struct(vec![
("id".into(), Value::Str(dev.id.to_string())),
("feature".into(), Value::Str(dev.feature)),
("section".into(), Value::Str(dev.section)),
]))
})
}
}
pub struct MemoryFetchConfessions {
pub store: Arc<ConfessionStore>,
}
impl Tool for MemoryFetchConfessions {
fn name(&self) -> &str {
"memory.fetch_confessions"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"Fetch past confession records about rule violations, optionally filtered by trigger text. Use it to recall prior mistakes and mitigations before repeating risky work.",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"trigger": {"type": "string", "description": "Optional trigger substring to search for; omit to list all confession records."}
}
})
}
fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let items = match args.named("trigger") {
Some(Value::Str(needle)) => self.store.find_by_trigger(needle).await?,
_ => self.store.list().await?,
};
let list = items
.into_iter()
.map(|c| {
Value::Struct(vec![
("id".into(), Value::Str(c.id.to_string())),
("trigger".into(), Value::Str(c.trigger)),
("rule_violated".into(), Value::Str(c.rule_violated)),
("what_i_did".into(), Value::Str(c.what_i_did)),
("why".into(), Value::Str(c.why)),
("mitigation".into(), Value::Str(c.mitigation)),
])
})
.collect();
Ok(Value::List(list))
})
}
}
pub struct MemoryHistorySearch;
impl Tool for MemoryHistorySearch {
fn name(&self) -> &str {
"memory.history.search"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"Full-text search the current session's chat history (or optionally every session \
in the same project). Use it to recall past turns that fell out of your working \
context — e.g. `plan we agreed on this morning`, `which files did we read`, \
`error the user reported earlier`. NOT for searching source code; use fs.grep for \
that. Params: query (FTS5 syntax, required), scope (\"session\"|\"project\", \
default \"session\"), limit (int, default 10, max 50).",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"query": {"type": "string"},
"scope": {"type": "string", "enum": ["session", "project"], "default": "session"},
"limit": {"type": "integer", "default": 10}
},
"required": ["query"]
})
}
fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let query = required_string(&args, "query")?;
let scope = match args.named("scope") {
Some(Value::Str(s)) if s == "project" => HistoryScope::Project,
_ => HistoryScope::Session,
};
let limit = match args.named("limit") {
Some(Value::Int(n)) if *n > 0 => (*n as usize).min(50),
_ => 10,
};
let Some(store) = ctx.history_store.clone() else {
return Err(RuntimeError::ToolFailed(
"memory.history.search: no history store on context".into(),
));
};
let search_scope = match scope {
HistoryScope::Project => crate::history_store::SearchScope::Project,
HistoryScope::Session => crate::history_store::SearchScope::Session,
};
let result =
tokio::task::spawn_blocking(move || store.search(&query, search_scope, limit))
.await
.map_err(|e| RuntimeError::ToolFailed(format!("history.search: {e}")))??;
let hits: Vec<Value> = result
.hits
.into_iter()
.map(|hit| {
Value::Struct(vec![
("session_id".into(), Value::Str(hit.session_id)),
("seq".into(), Value::Int(hit.seq as i64)),
("ts".into(), Value::Str(hit.ts)),
("kind".into(), Value::Str(hit.kind)),
("snippet".into(), Value::Str(hit.snippet)),
])
})
.collect();
Ok(Value::Struct(vec![
("total".into(), Value::Int(result.total as i64)),
("hits".into(), Value::List(hits)),
]))
})
}
}
pub struct MemoryHistoryRead;
impl Tool for MemoryHistoryRead {
fn name(&self) -> &str {
"memory.history.read"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"Paginate through past messages of a session by turn index. Prefer \
memory.history.search first to find a hit, then call this for surrounding context. \
Params: session_id (string, default current session's directory name), offset \
(1-based turn index, default 1), limit (int, default 20, max 100), role_filter \
(comma-separated: user,assistant,tool,system; default all).",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"session_id": {"type": "string"},
"offset": {"type": "integer", "default": 1},
"limit": {"type": "integer", "default": 20},
"role_filter": {"type": "string"}
}
})
}
fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let Some(current_dir) = ctx.session_dir.as_ref() else {
return Err(RuntimeError::ToolFailed(
"memory.history.read: no session dir on context".into(),
));
};
let session_id = match args.named("session_id") {
Some(Value::Str(sid)) if !sid.is_empty() => sid.clone(),
_ => current_dir
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default(),
};
let offset = match args.named("offset") {
Some(Value::Int(n)) if *n >= 1 => *n as usize,
_ => 1,
};
let limit = match args.named("limit") {
Some(Value::Int(n)) if *n >= 1 => (*n as usize).min(100),
_ => 20,
};
let role_filter: Option<Vec<String>> = match args.named("role_filter") {
Some(Value::Str(s)) if !s.is_empty() => Some(
s.split(',')
.map(|t| t.trim().to_lowercase())
.filter(|t| !t.is_empty())
.collect(),
),
_ => None,
};
let Some(store) = ctx.history_store.clone() else {
return Err(RuntimeError::ToolFailed(
"memory.history.read: no history store on context".into(),
));
};
let query = crate::history_store::HistoryQuery {
session_id,
offset,
limit,
role_filter,
};
let page = tokio::task::spawn_blocking(move || store.read(query))
.await
.map_err(|e| RuntimeError::ToolFailed(format!("history.read: {e}")))??;
let item_count = page.items.len();
let items: Vec<Value> = page.items.into_iter().map(Value::Message).collect();
let start = offset;
let end = if item_count == 0 {
start
} else {
start + item_count - 1
};
let header = format!("[history: turns {start}-{end} of {}]", page.total);
Ok(Value::Struct(vec![
("total".into(), Value::Int(page.total as i64)),
("offset".into(), Value::Int(page.offset as i64)),
("limit".into(), Value::Int(page.limit as i64)),
("header".into(), Value::Str(header)),
("items".into(), Value::List(items)),
]))
})
}
}
pub struct MemoryHistoryCount;
impl Tool for MemoryHistoryCount {
fn name(&self) -> &str {
"memory.history.count"
}
fn tier(&self) -> Tier {
Tier::Zero
}
fn description(&self) -> Option<&str> {
Some(
"Return the total message count for a session. Lightweight — use this to check \
how many messages exist before paginating with memory.history.read. \
Params: session_id (string, default current session), role_filter \
(comma-separated: user,assistant,tool,system; default all).",
)
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"session_id": {"type": "string"},
"role_filter": {"type": "string"}
}
})
}
fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
Box::pin(async move {
let Some(current_dir) = ctx.session_dir.as_ref() else {
return Err(RuntimeError::ToolFailed(
"memory.history.count: no session dir on context".into(),
));
};
let session_id = match args.named("session_id") {
Some(Value::Str(sid)) if !sid.is_empty() => sid.clone(),
_ => current_dir
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default(),
};
let role_filter: Option<Vec<String>> = match args.named("role_filter") {
Some(Value::Str(s)) if !s.is_empty() => Some(
s.split(',')
.map(|t| t.trim().to_lowercase())
.filter(|t| !t.is_empty())
.collect(),
),
_ => None,
};
let Some(store) = ctx.history_store.clone() else {
return Err(RuntimeError::ToolFailed(
"memory.history.count: no history store on context".into(),
));
};
let total = tokio::task::spawn_blocking(move || {
let role_refs: Option<Vec<&str>> = role_filter
.as_ref()
.map(|rs| rs.iter().map(|s| s.as_str()).collect());
store.count(&session_id, role_refs.as_deref())
})
.await
.map_err(|e| RuntimeError::ToolFailed(format!("history.count: {e}")))??;
Ok(Value::Int(total as i64))
})
}
}
enum HistoryScope {
Session,
Project,
}
fn required_string(args: &ToolArgs, name: &str) -> Result<String, RuntimeError> {
match args.named(name) {
Some(Value::Str(s)) => Ok(s.clone()),
Some(other) => Err(RuntimeError::TypeMismatch {
expected: "string".into(),
actual: other.kind_name().into(),
}),
None => Err(RuntimeError::MissingArg(name.into())),
}
}