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, auto-injected as system prefix). 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 injects \
as a system-prompt prefix on every LLM call. It persists across turns, never \
enters message history, and is never compacted.\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;
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. 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 message count to return (default 10)"}
}
})
}
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,
};
if n == 0 {
if let Some(cb) = &ctx.on_memory_recent {
cb(0);
}
return Ok(Value::Struct(vec![
("total_message_count".into(), Value::Int(0)),
("items".into(), Value::List(Vec::new())),
]));
}
if let Some(msgs) = ctx.session_messages.as_ref() {
let total = msgs.len() as u64;
let start = msgs.len().saturating_sub(n);
let out: Vec<Value> = msgs[start..].iter().cloned().map(Value::Message).collect();
if let Some(cb) = &ctx.on_memory_recent {
cb(out.len() as u16);
}
return Ok(Value::Struct(vec![
("total_message_count".into(), Value::Int(total as i64)),
("items".into(), Value::List(out)),
]));
}
let Some(store) = ctx.history_store.clone() else {
return Err(RuntimeError::ToolFailed(
"memory.recent_turns: no history store on context".into(),
));
};
let (total, 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);
}
let items: Vec<Value> = msgs.into_iter().map(Value::Message).collect();
Ok(Value::Struct(vec![
("total_message_count".into(), Value::Int(total as i64)),
("items".into(), Value::List(items)),
]))
})
}
}
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 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())),
}
}