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}