yoi/crates/memory/src/tool/mod.rs

102 lines
3.1 KiB
Rust

//! Memory-scoped tools: Read / Write / Edit / Search.
//!
//! All four take `kind` + `slug` (Summary takes only `kind`) and
//! resolve the path through [`WorkspaceLayout`]. The agent never has
//! to know the on-disk layout — Search returns `{slug, kind, ...}` and
//! that pair feeds straight into Read / Edit.
mod delete;
mod edit;
mod query;
mod read;
mod write;
pub use delete::delete_tool;
use std::path::PathBuf;
use llm_worker::tool::ToolError;
use serde::Deserialize;
use crate::Slug;
use crate::workspace::{RecordKind, WorkspaceLayout};
pub use edit::edit_tool;
pub use query::{QueryConfig, knowledge_query_tool, memory_query_tool};
pub use read::{read_tool, read_tool_with_usage};
pub use write::write_tool;
/// Kinds the memory tools accept as input. `Workflow` is intentionally
/// excluded — workflows are sub-Worker context, not agent-editable.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MemoryToolKind {
Summary,
Decision,
Request,
Knowledge,
}
impl std::fmt::Display for MemoryToolKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Summary => "summary",
Self::Decision => "decision",
Self::Request => "request",
Self::Knowledge => "knowledge",
})
}
}
impl MemoryToolKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Summary => "summary",
Self::Decision => "decision",
Self::Request => "request",
Self::Knowledge => "knowledge",
}
}
pub fn record_kind(self) -> RecordKind {
match self {
Self::Summary => RecordKind::Summary,
Self::Decision => RecordKind::Decision,
Self::Request => RecordKind::Request,
Self::Knowledge => RecordKind::Knowledge,
}
}
/// Resolve `(kind, slug)` to an absolute path under the workspace.
/// Summary forbids a slug; the per-record kinds require one.
pub fn resolve_path(
self,
layout: &WorkspaceLayout,
slug: Option<&str>,
) -> Result<PathBuf, ToolError> {
match self {
Self::Summary => {
if slug.is_some() {
return Err(ToolError::InvalidArgument(
"kind=summary does not accept a slug".to_string(),
));
}
Ok(layout.summary_path())
}
other => {
let raw = slug.ok_or_else(|| {
ToolError::InvalidArgument(format!("kind={} requires `slug`", other.as_str()))
})?;
let parsed =
Slug::parse(raw).map_err(|e| ToolError::InvalidArgument(e.to_string()))?;
Ok(match other {
Self::Decision => layout.decision_path(&parsed),
Self::Request => layout.request_path(&parsed),
Self::Knowledge => layout.knowledge_path(&parsed),
Self::Summary => unreachable!(),
})
}
}
}
}