Skip to main content

ito_core/memory/
mod.rs

1//! Agent memory provider resolution and instruction rendering.
2//!
3//! Memory in Ito is intentionally provider-agnostic. The user configures one
4//! of three operations (`capture`, `search`, `query`) under
5//! `ItoConfig.memory`; each operation independently picks either an inline
6//! command template or a delegated skill. This module turns those
7//! configurations plus runtime inputs into a `RenderedInstruction` that
8//! the agent-facing CLI can print verbatim.
9//!
10//! See `.ito/specs/agent-memory-abstraction/` for the authoritative spec.
11
12use std::collections::BTreeMap;
13
14use ito_config::types::{MemoryConfig, MemoryOpConfig};
15use serde_json::Value;
16
17mod rendering;
18#[cfg(test)]
19mod rendering_tests;
20
21pub use rendering::shell_quote;
22
23/// Default value applied to `memory-search`'s `--limit` flag when the caller
24/// does not supply one.
25pub const DEFAULT_SEARCH_LIMIT: u64 = 10;
26
27/// Inputs accepted by `ito agent instruction memory-capture`.
28#[derive(Debug, Clone, Default)]
29pub struct CaptureInputs {
30    /// Free-form context describing what to remember.
31    pub context: Option<String>,
32    /// Files to include when the configured provider supports file context.
33    pub files: Vec<String>,
34    /// Folders to include when the configured provider supports folder packs.
35    pub folders: Vec<String>,
36}
37
38/// Inputs accepted by `ito agent instruction memory-search`.
39#[derive(Debug, Clone)]
40pub struct SearchInputs {
41    /// Search query (required).
42    pub query: String,
43    /// Maximum results. `None` falls back to [`DEFAULT_SEARCH_LIMIT`].
44    pub limit: Option<u64>,
45    /// Optional path-prefix scope (e.g. `auth/`).
46    pub scope: Option<String>,
47}
48
49/// Inputs accepted by `ito agent instruction memory-query`.
50#[derive(Debug, Clone)]
51pub struct QueryInputs {
52    /// Natural-language question (required).
53    pub query: String,
54}
55
56/// Identifies a memory operation for diagnostics and template rendering.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Operation {
59    /// `capture` — store / curate a memory.
60    Capture,
61    /// `search` — structured BM25-style ranked lookup.
62    Search,
63    /// `query` — synthesized natural-language answer over stored memory.
64    Query,
65}
66
67impl Operation {
68    /// Lower-case, brv-compatible key for this operation.
69    #[must_use]
70    pub fn as_key(self) -> &'static str {
71        match self {
72            Self::Capture => "capture",
73            Self::Search => "search",
74            Self::Query => "query",
75        }
76    }
77}
78
79/// Output of resolving and rendering a memory operation.
80///
81/// Three render branches mirror the three states a single operation can be
82/// in (configured-as-command / configured-as-skill / not-configured); see
83/// the `agent-memory-abstraction` spec.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum RenderedInstruction {
86    /// Operation is configured with `kind: "command"` — the rendered shell
87    /// command line is ready for the agent to execute.
88    Command {
89        /// Final command line with placeholders substituted.
90        line: String,
91    },
92    /// Operation is configured with `kind: "skill"` — the agent should
93    /// invoke the named skill with the listed inputs and opaque options.
94    Skill {
95        /// Skill identifier as configured.
96        skill_id: String,
97        /// Structured inputs for this operation, named by the operation's
98        /// schema (`context`, `files`, `folders`, `query`, `limit`,
99        /// `scope`).
100        inputs: BTreeMap<String, Value>,
101        /// Opaque per-skill options from configuration.
102        ///
103        /// `None` when no `options` field was supplied; the skill receives
104        /// nothing for it and decides on its own defaults.
105        options: Option<Value>,
106    },
107    /// Operation is not configured — the artifact should print provider
108    /// setup guidance (the caller embeds [`Operation`] context to build a
109    /// useful hint).
110    NotConfigured {
111        /// Which operation is missing.
112        operation: Operation,
113    },
114}
115
116/// Render the `memory-capture` instruction for the given config and inputs.
117///
118/// Returns [`RenderedInstruction::NotConfigured`] when `memory.capture` is
119/// absent, regardless of how `search` and `query` are configured.
120#[must_use]
121pub fn render_capture(
122    config: Option<&MemoryConfig>,
123    inputs: &CaptureInputs,
124) -> RenderedInstruction {
125    let op_cfg = config.and_then(|c| c.capture.as_ref());
126    let Some(op_cfg) = op_cfg else {
127        return RenderedInstruction::NotConfigured {
128            operation: Operation::Capture,
129        };
130    };
131    match op_cfg {
132        MemoryOpConfig::Command { command } => RenderedInstruction::Command {
133            line: rendering::render_capture_command(command, inputs),
134        },
135        MemoryOpConfig::Skill { skill, options } => RenderedInstruction::Skill {
136            skill_id: skill.clone(),
137            inputs: rendering::capture_inputs_as_structured(inputs),
138            options: options.clone(),
139        },
140    }
141}
142
143/// Render the `memory-search` instruction for the given config and inputs.
144#[must_use]
145pub fn render_search(config: Option<&MemoryConfig>, inputs: &SearchInputs) -> RenderedInstruction {
146    let op_cfg = config.and_then(|c| c.search.as_ref());
147    let Some(op_cfg) = op_cfg else {
148        return RenderedInstruction::NotConfigured {
149            operation: Operation::Search,
150        };
151    };
152    match op_cfg {
153        MemoryOpConfig::Command { command } => RenderedInstruction::Command {
154            line: rendering::render_search_command(command, inputs),
155        },
156        MemoryOpConfig::Skill { skill, options } => RenderedInstruction::Skill {
157            skill_id: skill.clone(),
158            inputs: rendering::search_inputs_as_structured(inputs),
159            options: options.clone(),
160        },
161    }
162}
163
164/// Render the `memory-query` instruction for the given config and inputs.
165#[must_use]
166pub fn render_query(config: Option<&MemoryConfig>, inputs: &QueryInputs) -> RenderedInstruction {
167    let op_cfg = config.and_then(|c| c.query.as_ref());
168    let Some(op_cfg) = op_cfg else {
169        return RenderedInstruction::NotConfigured {
170            operation: Operation::Query,
171        };
172    };
173    match op_cfg {
174        MemoryOpConfig::Command { command } => RenderedInstruction::Command {
175            line: rendering::render_query_command(command, inputs),
176        },
177        MemoryOpConfig::Skill { skill, options } => RenderedInstruction::Skill {
178            skill_id: skill.clone(),
179            inputs: rendering::query_inputs_as_structured(inputs),
180            options: options.clone(),
181        },
182    }
183}