velesdb_memory/context/model.rs
1//! Data model of the context compiler: the request/response value types.
2//!
3//! Like [`crate::model`], these are pure data with `Serialize`/`Deserialize` +
4//! `JsonSchema` derives, so the domain types double as the MCP wire types —
5//! no duplicate DTO layer. Invariants the compiler upholds over these shapes:
6//! same request ⇒ byte-identical [`CompiledContext`] (determinism), and the
7//! assembled content never exceeds the request's token budget.
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value};
12
13use super::chunk::ChunkPolicy;
14use super::insights::CompilationInsights;
15
16/// What the compiler decided to do with one fragment.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
18#[serde(rename_all = "lowercase")]
19pub enum ContextAction {
20 /// Emitted verbatim — critical content (code, constraints, exact values).
21 Preserve,
22 /// Emitted as a deterministic structured reduction (never a generative
23 /// summary) — e.g. repeated log lines collapsed with a count.
24 Abstract,
25 /// Not emitted, but recoverable through its `ctx://source/<id>` handle.
26 Retrieve,
27 /// Not emitted and not externalized — redundant content (duplicates).
28 Drop,
29 /// Emitted verbatim at the front of the output, forming a stable prefix
30 /// that maximizes provider prompt-cache hits across compilations.
31 Cache,
32}
33
34/// How much fidelity a compiled context may have lost versus its input.
35///
36/// Ordered: `Low < Medium < High`, so callers can compare against a policy
37/// threshold.
38#[derive(
39 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
40)]
41#[serde(rename_all = "lowercase")]
42pub enum FidelityRisk {
43 /// Nothing was lost: everything fit, only exact duplicates were dropped.
44 #[default]
45 Low,
46 /// Recoverable reductions happened: abstractions, or non-critical
47 /// fragments externalized behind retrieval handles.
48 Medium,
49 /// Critical content (a preserve-classified fragment) could not be packed
50 /// — the caller should consider retrieving it or raising the budget.
51 High,
52}
53
54/// One unit of caller-supplied context to compile.
55#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
56#[schemars(transform = crate::schema::strip_int_formats)]
57pub struct ContextFragment {
58 /// Caller-side identifier. When absent, the compiler derives a stable
59 /// content-addressed id (see [`super::fragment_id`]).
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub id: Option<u64>,
62 /// The fragment text.
63 pub content: String,
64 /// Free-form kind hint (`"code"`, `"log"`, `"prose"`, …) — classification
65 /// works without it, but honors it when present.
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub kind: Option<String>,
68 /// Caller priority, higher packs first (default `0`).
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub priority: Option<u8>,
71 /// Caller metadata. Recognized keys: `"verbatim": true` forces
72 /// [`ContextAction::Preserve`]; `"cache": true` forces
73 /// [`ContextAction::Cache`].
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub metadata: Option<Map<String, Value>>,
76}
77
78/// Which memories the compiler may pull in alongside the caller's fragments.
79/// Consumed by the memory bridge (US-002); carried in the request shape from
80/// the start so the wire contract does not change when it lands.
81#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
82#[schemars(transform = crate::schema::strip_int_formats)]
83pub struct MemoryScope {
84 /// Restrict recalled memories to this project facet.
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub project: Option<String>,
87 /// How many memories to consider (adapter-clamped).
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub k: Option<usize>,
90 /// Graph-walk depth of the fused recall (default 2). Deeper hops reach
91 /// longer cause/fix chains from the vector seed.
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub hops: Option<usize>,
94 /// Fusion weight added to graph-reached memories (default 0.15). Raise
95 /// it (e.g. `0.5`–`0.8`) when pulling from curated fact chains built
96 /// with `relate`: evidence that shares **no vocabulary** with the query
97 /// can then out-rank lexically-noisy near-misses — the tri-engine's
98 /// answer to the purely lexical relevance of caller fragments.
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub graph_boost: Option<f64>,
101}
102
103/// Tuning knobs of one compilation. `Default` is the recommended profile.
104#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
105#[serde(default)]
106#[schemars(transform = crate::schema::strip_int_formats)]
107pub struct CompilePolicy {
108 /// Tokens kept aside for the model's answer; the compiler packs into
109 /// `token_budget − response_reserve_tokens`. Default `0`: the caller
110 /// knows their generation length, the compiler does not guess it.
111 pub response_reserve_tokens: u64,
112 /// Collapse near-duplicates (case/whitespace variants) in addition to
113 /// exact duplicates. Default `true`.
114 pub near_dup_dedup: bool,
115 /// Rule ids to disable (e.g. `"abstract.log_dedup"`). Disabled rules are
116 /// skipped during classification; their fragments fall through to the
117 /// next matching rule.
118 pub disabled_rules: Vec<String>,
119 /// How oversized fragments are split before packing. Only
120 /// [`ChunkPolicy::max_chunk_bytes`] and [`ChunkPolicy::boundary`] apply
121 /// here — the compiler forces `overlap_bytes` to `0`, since it emits
122 /// pieces by concatenation and an overlap prefix would duplicate content
123 /// reported as verbatim. `overlap_bytes` is honoured only by the
124 /// standalone [`crate::context::chunk::chunk_text`] API.
125 pub chunk: ChunkPolicy,
126 /// Memory bridge only: record a compilation event (metadata and hashes,
127 /// **never fragment content**) so savings stay aggregatable. Default
128 /// `true`; set `false` to opt out entirely.
129 pub record_events: bool,
130 /// Memory bridge only: store each distinct fragment's original (as an
131 /// internal system fact, invisible to normal recall) so its
132 /// `ctx://source/<hash>` handle round-trips. Default `true`.
133 pub store_sources: bool,
134 /// TTL applied to stored sources (`None` keeps them until forgotten).
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub source_ttl_seconds: Option<u64>,
137 /// TTL applied to compilation events (`None` keeps them).
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub event_ttl_seconds: Option<u64>,
140 /// Caller-supplied pricing table so the insights also report the
141 /// estimated cost avoided for [`super::model::CompileRequest::target_model`] — the
142 /// **wire channel** for cost accounting (MCP and the bindings cannot
143 /// reach the Rust-only [`super::ContextCompiler::with_pricing`] builder).
144 /// Takes precedence over a builder-injected table. `None` (default)
145 /// reports tokens only.
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub pricing: Option<super::insights::PricingTable>,
148}
149
150impl Default for CompilePolicy {
151 fn default() -> Self {
152 Self {
153 response_reserve_tokens: 0,
154 near_dup_dedup: true,
155 disabled_rules: Vec::new(),
156 chunk: ChunkPolicy::default(),
157 record_events: true,
158 store_sources: true,
159 source_ttl_seconds: None,
160 event_ttl_seconds: None,
161 pricing: None,
162 }
163 }
164}
165
166/// Aggregated savings over the recorded compilation events (memory bridge).
167#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
168#[schemars(transform = crate::schema::strip_int_formats)]
169pub struct ContextSavings {
170 /// Number of compilation events aggregated.
171 pub events: u64,
172 /// Sum of estimated input tokens across events.
173 pub tokens_in: u64,
174 /// Sum of estimated output tokens across events.
175 pub tokens_out: u64,
176 /// Sum of estimated tokens saved across events.
177 pub tokens_saved: u64,
178 /// Estimated cost avoided, in micro-units, keyed by currency (events
179 /// priced under different pricing tables never silently mix).
180 pub cost_saved_micros_by_currency: std::collections::BTreeMap<String, u64>,
181 /// `true` when the aggregation hit the recall cap
182 /// ([`crate::limits::MAX_RECALL_LIMIT`]) — older events beyond the cap
183 /// were not folded in.
184 pub truncated: bool,
185}
186
187/// A full compile request: what to compile, under which budget, for whom.
188#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
189#[schemars(transform = crate::schema::strip_int_formats)]
190pub struct CompileRequest {
191 /// What the agent is working on — drives relevance scoring.
192 pub query: String,
193 /// The context fragments to compile.
194 pub fragments: Vec<ContextFragment>,
195 /// Project facet, recorded in provenance and used by the memory bridge.
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub project: Option<String>,
198 /// Target model name — selects the row of the pricing table
199 /// ([`CompilePolicy::pricing`] on the wire, or the Rust
200 /// [`super::ContextCompiler::with_pricing`] builder) for cost insights.
201 /// Without a table, insights report tokens only.
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub target_model: Option<String>,
204 /// Hard token ceiling for the assembled content.
205 pub token_budget: u64,
206 /// Which memories may be pulled in (US-002; ignored by the memoryless core).
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub memory_scope: Option<MemoryScope>,
209 /// Per-request policy override; `None` uses the compiler's policy.
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub policy: Option<CompilePolicy>,
212}
213
214/// Where a section sits in the assembled output.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
216#[serde(rename_all = "lowercase")]
217pub enum SectionKind {
218 /// The stable, cache-marked prefix.
219 Cache,
220 /// The main compiled body.
221 Body,
222}
223
224/// One contiguous block of the assembled output.
225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
226#[schemars(transform = crate::schema::strip_int_formats)]
227pub struct CompiledSection {
228 /// Which block this is.
229 pub kind: SectionKind,
230 /// The block's text (verbatim slice of [`CompiledContext::content`]).
231 pub content: String,
232 /// Ids of the fragments emitted into this block, in emission order.
233 pub fragment_ids: Vec<u64>,
234}
235
236/// A pointer from a compiled output back to one original fragment.
237#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
238#[schemars(transform = crate::schema::strip_int_formats)]
239pub struct SourceReference {
240 /// The fragment this source refers to.
241 pub fragment_id: u64,
242 /// Recoverable address of the original content (`ctx://source/<id>`).
243 pub handle: String,
244 /// The memory backing this source, when it came from recall (US-002).
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub memory_id: Option<u64>,
247}
248
249/// A not-emitted fragment the caller can fetch back on demand.
250#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
251#[schemars(transform = crate::schema::strip_int_formats)]
252pub struct RetrievalHandle {
253 /// Recoverable address of the original content (`ctx://source/<id>`).
254 pub handle: String,
255 /// The fragment behind the handle.
256 pub fragment_id: u64,
257 /// Estimated token cost of re-injecting the full original.
258 pub estimated_tokens: u64,
259}
260
261/// The auditable record of what happened to one fragment and why.
262#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
263#[schemars(transform = crate::schema::strip_int_formats)]
264pub struct ContextDecision {
265 /// The fragment this decision is about (caller id, or content-derived).
266 pub fragment_id: u64,
267 /// Content hash of the *original* fragment text (FNV-1a 64, the crate's
268 /// [`stable id`](super::fragment_id)) — lets an auditor prove which exact
269 /// bytes the decision covered even when the caller supplied its own id.
270 pub content_hash: u64,
271 /// What was done.
272 pub action: ContextAction,
273 /// The stable id of the rule that decided (e.g. `"preserve.code_fence"`).
274 pub rule_id: String,
275 /// Lexical relevance of the fragment to the request query, in `[0, 1]`.
276 pub relevance: f32,
277 /// Fidelity risk this single decision contributes.
278 pub risk: FidelityRisk,
279 /// Human-readable explanation of the decision.
280 pub reason: String,
281 /// The memory backing this fragment, when it came from recall (US-002).
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub memory_id: Option<u64>,
284 /// Recoverable address of the original content, when not fully emitted.
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub handle: Option<String>,
287}
288
289/// The compiler's output: the assembled context plus its full audit trail.
290#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
291#[schemars(transform = crate::schema::strip_int_formats)]
292pub struct CompiledContext {
293 /// The assembled context, ready to inject into a prompt.
294 pub content: String,
295 /// The output split into ordered blocks (cache prefix first).
296 pub sections: Vec<CompiledSection>,
297 /// One decision per input fragment (duplicates included).
298 pub decisions: Vec<ContextDecision>,
299 /// One source pointer per distinct fragment.
300 pub sources: Vec<SourceReference>,
301 /// Handles for the fragments that were externalized, not emitted.
302 pub retrieval_handles: Vec<RetrievalHandle>,
303 /// Token (and optional cost) savings of this compilation.
304 pub insights: CompilationInsights,
305 /// Overall fidelity risk (the max over all decisions).
306 pub risk: FidelityRisk,
307}
308
309/// One asserted fact inside a [`WorkingContext`].
310#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
311pub struct ContextFact {
312 /// The fact text.
313 pub text: String,
314 /// Where the fact came from, when known.
315 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub source: Option<SourceReference>,
317}
318
319/// A lightweight pointer to a past [`ContextDecision`].
320#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
321#[schemars(transform = crate::schema::strip_int_formats)]
322pub struct ContextDecisionRef {
323 /// The fragment the decision was about.
324 pub fragment_id: u64,
325 /// The rule that decided.
326 pub rule_id: String,
327}
328
329/// The distilled working state of an agent session — small enough to carry
330/// across sessions, structured enough to resume from. Persisted and reloaded
331/// by the memory bridge (US-002) under `type = working_context` metadata.
332#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
333pub struct WorkingContext {
334 /// What the session is trying to achieve.
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub goal: Option<String>,
337 /// Constraints currently in force (never compressed away).
338 #[serde(default)]
339 pub active_constraints: Vec<ContextFact>,
340 /// Facts that were verified, with their sources.
341 #[serde(default)]
342 pub verified_facts: Vec<ContextFact>,
343 /// Hypotheses still open.
344 #[serde(default)]
345 pub open_hypotheses: Vec<ContextFact>,
346 /// Decisions taken so far.
347 #[serde(default)]
348 pub decisions: Vec<ContextDecisionRef>,
349 /// Exact evidence the session relies on (verbatim, addressable).
350 #[serde(default)]
351 pub exact_evidence: Vec<SourceReference>,
352 /// Actions still to do.
353 #[serde(default)]
354 pub pending_actions: Vec<String>,
355}