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/// Inline media payload attached to a [`ContextFragment`] (US-009, PR1:
55/// screenshots/images only). `ContextFragment::content` stays the
56/// text/caption — often empty for a bare screenshot — while the pixels live
57/// here, base64-encoded so the JSON wire never needs a binary frame.
58///
59/// The fragment packs atomically (see [`super::pieces`] in the compiler)
60/// and its token cost comes from [`super::estimator::ImageTokenEstimator`].
61/// A media fragment that cannot fit the budget is externalized behind a
62/// `ctx://source` handle exactly like text (US-009, PR2: the memory bridge
63/// persists the bytes behind it — see
64/// [`crate::MemoryService::retrieve_context_source`]); the memoryless core
65/// compiler mints the same handle either way, since it never knows whether
66/// a resolver is attached. See the crate README's "media fragments"
67/// section.
68#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
69pub struct MediaRef {
70 /// Declared MIME type (e.g. `"image/png"`, `"image/jpeg"`). Only PNG and
71 /// JPEG headers are sniffed for dimensions; any other value (or an
72 /// unreadable header) falls back to a deterministic, safe over-count
73 /// (see [`super::estimator::ImageTokenEstimator`]) — never rejected for
74 /// an unrecognized mime alone.
75 pub mime: String,
76 /// The raw media bytes, base64-encoded (standard alphabet, padded).
77 /// Capped at [`crate::limits::MAX_MEDIA_BYTES`] and validated for
78 /// well-formedness at compile time — a request carrying an oversized or
79 /// malformed payload is rejected before any other work.
80 pub bytes_b64: String,
81}
82
83/// The resolved original behind a `ctx://source/<hash>` handle (US-002:
84/// text sources; US-009 PR2 extends this with the fragment's inline media,
85/// when it carried one). `media` is `#[serde(default)]`: every source
86/// stored before PR2, and every text-only fragment since, round-trips with
87/// `media: None` — the exact pre-PR2 shape for a caller reading only
88/// `.content`.
89#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
90pub struct ContextSource {
91 /// The original fragment content, byte for byte (a media fragment's
92 /// caption — often empty for a bare screenshot).
93 pub content: String,
94 /// The original media payload, when the fragment carried one.
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub media: Option<MediaRef>,
97}
98
99/// One unit of caller-supplied context to compile.
100#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
101#[schemars(transform = crate::schema::strip_int_formats)]
102pub struct ContextFragment {
103 /// Caller-side identifier. When absent, the compiler derives a stable
104 /// content-addressed id (see [`super::fragment_id`]). Accepts a JSON
105 /// number or a decimal string on input (see
106 /// [`super::wire::deserialize_optional_id`]) — a caller that got a
107 /// `fragment_id` back as a string (e.g. under
108 /// [`CompilePolicy::ids_as_strings`]) can resubmit it unchanged.
109 #[serde(
110 default,
111 skip_serializing_if = "Option::is_none",
112 deserialize_with = "super::wire::deserialize_optional_id"
113 )]
114 pub id: Option<u64>,
115 /// The fragment text. `#[serde(default)]` (V2b-1): a `path` fragment
116 /// carries no `content` on the wire — the adapter resolves `path` into
117 /// `content` in a pre-pass before the pure compiler core ever sees the
118 /// request, so this stays the only field the core reads.
119 #[serde(default)]
120 pub content: String,
121 /// Read this file's content from disk in place of an inline `content`
122 /// (V2b-1 path ingestion): exactly one of `path`, non-empty `content`,
123 /// or `media` is accepted — a fragment carrying `path` together with
124 /// `content` or `media` is rejected. Requires the server to be started
125 /// with `VELESDB_MEMORY_INGEST_ROOTS` set (a colon/semicolon-separated
126 /// allowlist of directories, platform `PATH`-list syntax); otherwise
127 /// every `path` fragment fails with an explicit "ingestion disabled"
128 /// error. The path must be absolute and resolve (after following
129 /// symlinks) to a plain file under one of the configured roots, no
130 /// larger than [`crate::limits::MAX_INGEST_FILE_BYTES`] and valid UTF-8.
131 /// The **pure compiler core never reads this field** — resolution is an
132 /// adapter-side I/O pre-pass (see the crate's `context::ingest`
133 /// module); a `path` fragment that reaches [`super::ContextCompiler`]
134 /// unresolved is rejected with
135 /// [`crate::error::MemoryError::IngestDisabled`].
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub path: Option<String>,
138 /// Free-form kind hint (`"code"`, `"log"`, `"prose"`, …) — classification
139 /// works without it, but honors it when present.
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub kind: Option<String>,
142 /// Caller priority, higher packs first (default `0`).
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub priority: Option<u8>,
145 /// Caller metadata. Recognized keys: `"verbatim": true` forces
146 /// [`ContextAction::Preserve`]; `"cache": true` forces
147 /// [`ContextAction::Cache`].
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub metadata: Option<Map<String, Value>>,
150 /// Inline media payload (US-009, PR1). `None` (the default) keeps every
151 /// pre-0.9.0 request wire-compatible. When set, the fragment packs as one
152 /// atomic piece — see [`MediaRef`].
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub media: Option<MediaRef>,
155}
156
157/// Which memories the compiler may pull in alongside the caller's fragments.
158/// Consumed by the memory bridge (US-002); carried in the request shape from
159/// the start so the wire contract does not change when it lands.
160#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
161#[schemars(transform = crate::schema::strip_int_formats)]
162pub struct MemoryScope {
163 /// Restrict recalled memories to this project facet.
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub project: Option<String>,
166 /// How many memories to consider (adapter-clamped).
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub k: Option<usize>,
169 /// Graph-walk depth of the fused recall (default 2). Deeper hops reach
170 /// longer cause/fix chains from the vector seed.
171 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub hops: Option<usize>,
173 /// Fusion weight added to graph-reached memories (default 0.15). Raise
174 /// it (e.g. `0.5`–`0.8`) when pulling from curated fact chains built
175 /// with `relate`: evidence that shares **no vocabulary** with the query
176 /// can then out-rank lexically-noisy near-misses — the tri-engine's
177 /// answer to the purely lexical relevance of caller fragments.
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub graph_boost: Option<f64>,
180}
181
182/// Usage-driven importance weights of the memory-bridge blend (US-002 of
183/// EPIC-P-071): how much a pulled memory's learned RL confidence and its
184/// batch-relative recency tilt the fused similarity ranking.
185///
186/// The blend only ever applies to the pool the fused vector+graph similarity
187/// already selected — confidence is *not* relevance, so a heavily reinforced
188/// but off-topic fact can never enter the pool through these weights. Per
189/// pulled memory the ranking key becomes
190/// `fused_norm + confidence_weight·(confidence − 0.5)·2 + recency_weight·recency_norm`,
191/// clock-free and deterministic (recency is min-max normalised **within the
192/// pulled batch**, never against wall time).
193///
194/// Both weights at `0.0` disable the blend entirely: the output is
195/// byte-identical to the 0.8.0 behaviour (pinned by a golden test). The
196/// defaults are **active** on purpose — upgrading from 0.8.0 with the
197/// default policy, RL-reinforced memories rank higher out of the box; zero
198/// the weights to restore the exact 0.8.0 ordering.
199///
200/// Recommended range for both weights: `[0.0, 1.0]` (at `1.0` a term can
201/// fully offset the similarity gap within the pool). Values outside that
202/// range are **accepted verbatim, never clamped** — a negative weight
203/// deliberately inverts its term (e.g. demote reinforced facts), a weight
204/// above `1.0` lets the term dominate similarity. Only the recorded
205/// decision `relevance` is clamped into `[0, 1]`; the ranking itself uses
206/// the raw blended score.
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
208#[serde(default)]
209pub struct ImportanceWeights {
210 /// Weight of the learned RL confidence (`_veles_rl_*`, fed by
211 /// [`feedback`](crate::MemoryService::feedback)). A memory with no
212 /// feedback history counts as the neutral `0.5`, contributing exactly
213 /// `0`. Default `0.2`.
214 pub confidence: f64,
215 /// Weight of the batch-relative recency term. Inert unless
216 /// [`Self::recency_field`] is also set. Default `0.1`.
217 pub recency: f64,
218 /// Caller metadata key holding each memory's **numeric** timestamp-like
219 /// value. `None` (the default) disables the recency term completely —
220 /// there is no standard key to guess. The scale must be monotone and
221 /// homogeneous across the batch (e.g. `YYYYMMDD` integers as in
222 /// [`crate::format_dated_context`], or an epoch); it is documented, not
223 /// verified at run time. Values are min-max normalised over the pulled
224 /// memories that carry the key; a memory without the key contributes `0`
225 /// (never penalised), and a degenerate batch (`max == min`) contributes
226 /// `0` for all.
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub recency_field: Option<String>,
229}
230
231impl Default for ImportanceWeights {
232 fn default() -> Self {
233 Self {
234 confidence: 0.2,
235 recency: 0.1,
236 recency_field: None,
237 }
238 }
239}
240
241/// Tuning knobs of one compilation. `Default` is the recommended profile.
242#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
243#[serde(default)]
244#[schemars(transform = crate::schema::strip_int_formats)]
245#[allow(clippy::struct_excessive_bools)]
246pub struct CompilePolicy {
247 /// Tokens kept aside for the model's answer; the compiler packs into
248 /// `token_budget − response_reserve_tokens`. Default `0`: the caller
249 /// knows their generation length, the compiler does not guess it.
250 pub response_reserve_tokens: u64,
251 /// Collapse near-duplicates (case/whitespace variants) in addition to
252 /// exact duplicates. Default `true`.
253 pub near_dup_dedup: bool,
254 /// Rule ids to disable (e.g. `"abstract.log_dedup"`). Disabled rules are
255 /// skipped during classification; their fragments fall through to the
256 /// next matching rule.
257 pub disabled_rules: Vec<String>,
258 /// How oversized fragments are split before packing. Only
259 /// [`ChunkPolicy::max_chunk_bytes`] and [`ChunkPolicy::boundary`] apply
260 /// here — the compiler forces `overlap_bytes` to `0`, since it emits
261 /// pieces by concatenation and an overlap prefix would duplicate content
262 /// reported as verbatim. `overlap_bytes` is honoured only by the
263 /// standalone [`crate::context::chunk::chunk_text`] API.
264 pub chunk: ChunkPolicy,
265 /// Memory bridge only: record a compilation event (metadata and hashes,
266 /// **never fragment content**) so savings stay aggregatable. Default
267 /// `true`; set `false` to opt out entirely.
268 pub record_events: bool,
269 /// Memory bridge only: store each distinct fragment's original (as an
270 /// internal system fact, invisible to normal recall) so its
271 /// `ctx://source/<hash>` handle round-trips. Default `true`.
272 pub store_sources: bool,
273 /// TTL applied to stored sources (`None` keeps them until forgotten).
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub source_ttl_seconds: Option<u64>,
276 /// TTL applied to compilation events (`None` keeps them).
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub event_ttl_seconds: Option<u64>,
279 /// Caller-supplied pricing table so the insights also report the
280 /// estimated cost avoided for [`super::model::CompileRequest::target_model`] — the
281 /// **wire channel** for cost accounting (MCP and the bindings cannot
282 /// reach the Rust-only [`super::ContextCompiler::with_pricing`] builder).
283 /// Takes precedence over a builder-injected table. `None` (default)
284 /// reports tokens only.
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub pricing: Option<super::insights::PricingTable>,
287 /// Memory bridge only: usage-driven importance blend over the pulled
288 /// memories (RL confidence + batch-relative recency). The struct-level
289 /// `#[serde(default)]` keeps 0.8.0 requests wire-compatible.
290 pub importance: ImportanceWeights,
291 /// Opt-in, deterministic: before `abstract.log_dedup` groups a `kind =
292 /// "log"` fragment's repeated lines, mask each line's volatile prefix
293 /// (ISO/syslog timestamps, bracketed hex/pid counters) with **fixed**
294 /// patterns — never a caller-supplied regex, so the collapse stays
295 /// reproducible — so lines identical modulo timestamp collapse into one
296 /// annotated line instead of surviving as distinct entries. The emitted
297 /// line is still the first occurrence's exact bytes; only the grouping
298 /// key changes. Default `false`: masking is opt-in because it changes
299 /// what "duplicate" means for logs, so callers who rely on the previous
300 /// byte-exact grouping keep it unless they ask. See the crate README's
301 /// "Normalizing timestamped logs" section for the exact patterns.
302 pub normalize_log_timestamps: bool,
303 /// Wire-compat opt-in for the MCP context tools (`compile_context`,
304 /// `explain_compilation`): when `true`, every [`super::wire::ID_KEYS`]
305 /// field of the RESPONSE (`fragment_id`, `content_hash`, `memory_id`,
306 /// `fragment_ids`) is rewritten into its decimal-string form, through
307 /// the exact same tree walk the Node and WASM bindings already apply on
308 /// every response ([`super::wire::stringify_id_fields`]). A raw MCP
309 /// client — one that talks JSON-RPC directly, without either binding —
310 /// parses ids as JS `number`s (IEEE-754 doubles), which silently lose
311 /// precision above 2^53; string ids round-trip exactly. Default
312 /// `false`: existing MCP clients keep today's byte-identical numeric
313 /// response unless they opt in.
314 pub ids_as_strings: bool,
315 /// Quick win (V2a-2): when `true`, `sections` and `decisions` are
316 /// emptied out of the response after compilation — `content`,
317 /// `insights`, `risk`, `warnings`, `sources`, and `retrieval_handles`
318 /// are unaffected. The full audit trail (`sections`/`decisions`) is
319 /// still recoverable: re-compile the same request without
320 /// `slim_response` (compilation is deterministic, so nothing is lost,
321 /// only not sent this time). Default `false`.
322 pub slim_response: bool,
323}
324
325impl Default for CompilePolicy {
326 fn default() -> Self {
327 Self {
328 response_reserve_tokens: 0,
329 near_dup_dedup: true,
330 disabled_rules: Vec::new(),
331 chunk: ChunkPolicy::default(),
332 record_events: true,
333 store_sources: true,
334 source_ttl_seconds: None,
335 event_ttl_seconds: None,
336 pricing: None,
337 importance: ImportanceWeights::default(),
338 normalize_log_timestamps: false,
339 ids_as_strings: false,
340 slim_response: false,
341 }
342 }
343}
344
345/// Aggregated savings over the recorded compilation events (memory bridge).
346#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
347#[schemars(transform = crate::schema::strip_int_formats)]
348pub struct ContextSavings {
349 /// Number of compilation events aggregated.
350 pub events: u64,
351 /// Sum of estimated input tokens across events.
352 pub tokens_in: u64,
353 /// Sum of estimated output tokens across events.
354 pub tokens_out: u64,
355 /// Sum of estimated tokens saved across events.
356 pub tokens_saved: u64,
357 /// Estimated cost avoided, in micro-units, keyed by currency (events
358 /// priced under different pricing tables never silently mix).
359 pub cost_saved_micros_by_currency: std::collections::BTreeMap<String, u64>,
360 /// `true` when the aggregation hit the recall cap
361 /// ([`crate::limits::MAX_RECALL_LIMIT`]) — older events beyond the cap
362 /// were not folded in.
363 pub truncated: bool,
364}
365
366/// A full compile request: what to compile, under which budget, for whom.
367#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
368#[schemars(transform = crate::schema::strip_int_formats)]
369pub struct CompileRequest {
370 /// What the agent is working on — drives relevance scoring.
371 pub query: String,
372 /// The context fragments to compile.
373 pub fragments: Vec<ContextFragment>,
374 /// Project facet, recorded in provenance and used by the memory bridge.
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub project: Option<String>,
377 /// Target model name — selects the row of the pricing table
378 /// ([`CompilePolicy::pricing`] on the wire, or the Rust
379 /// [`super::ContextCompiler::with_pricing`] builder) for cost insights.
380 /// Without a table, insights report tokens only.
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub target_model: Option<String>,
383 /// Hard token ceiling for the assembled content.
384 pub token_budget: u64,
385 /// Which memories may be pulled in (US-002; ignored by the memoryless core).
386 #[serde(default, skip_serializing_if = "Option::is_none")]
387 pub memory_scope: Option<MemoryScope>,
388 /// Per-request policy override; `None` uses the compiler's policy.
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub policy: Option<CompilePolicy>,
391}
392
393/// Where a section sits in the assembled output.
394#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
395#[serde(rename_all = "lowercase")]
396pub enum SectionKind {
397 /// The stable, cache-marked prefix.
398 Cache,
399 /// The main compiled body.
400 Body,
401}
402
403/// One contiguous block of the assembled output.
404#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
405#[schemars(transform = crate::schema::strip_int_formats)]
406pub struct CompiledSection {
407 /// Which block this is.
408 pub kind: SectionKind,
409 /// The block's text (verbatim slice of [`CompiledContext::content`]).
410 pub content: String,
411 /// Ids of the fragments emitted into this block, in emission order.
412 pub fragment_ids: Vec<u64>,
413}
414
415/// A pointer from a compiled output back to one original fragment.
416#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
417#[schemars(transform = crate::schema::strip_int_formats)]
418pub struct SourceReference {
419 /// The fragment this source refers to.
420 pub fragment_id: u64,
421 /// Recoverable address of the original content (`ctx://source/<id>`).
422 pub handle: String,
423 /// The memory backing this source, when it came from recall (US-002).
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub memory_id: Option<u64>,
426}
427
428/// A not-emitted fragment the caller can fetch back on demand.
429#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
430#[schemars(transform = crate::schema::strip_int_formats)]
431pub struct RetrievalHandle {
432 /// Recoverable address of the original content (`ctx://source/<id>`).
433 pub handle: String,
434 /// The fragment behind the handle.
435 pub fragment_id: u64,
436 /// Estimated token cost of re-injecting the full original.
437 pub estimated_tokens: u64,
438}
439
440/// The auditable record of what happened to one fragment and why.
441#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
442#[schemars(transform = crate::schema::strip_int_formats)]
443pub struct ContextDecision {
444 /// The fragment this decision is about (caller id, or content-derived).
445 pub fragment_id: u64,
446 /// Content hash of the *original* fragment text (FNV-1a 64, the crate's
447 /// [`stable id`](super::fragment_id)) — lets an auditor prove which exact
448 /// bytes the decision covered even when the caller supplied its own id.
449 pub content_hash: u64,
450 /// What was done.
451 pub action: ContextAction,
452 /// The stable id of the rule that decided (e.g. `"preserve.code_fence"`).
453 pub rule_id: String,
454 /// Lexical relevance of the fragment to the request query, in `[0, 1]`.
455 pub relevance: f32,
456 /// Fidelity risk this single decision contributes.
457 pub risk: FidelityRisk,
458 /// Human-readable explanation of the decision.
459 pub reason: String,
460 /// The memory backing this fragment, when it came from recall (US-002).
461 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub memory_id: Option<u64>,
463 /// Recoverable address of the original content, when not fully emitted.
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub handle: Option<String>,
466}
467
468/// A mechanical heads-up over one decision, surfaced in
469/// [`CompiledContext::warnings`] so a caller can check "was anything
470/// relevant cut?" without scanning every entry of `decisions` by hand
471/// (V2a-2 quick win). Only [`ContextAction::Retrieve`] decisions at or
472/// above the relevance threshold qualify — a [`ContextAction::Drop`] in
473/// this compiler is always a byte-identical duplicate whose content
474/// survives through its kept twin (see `dup_verdict`), so it is never a
475/// real loss and never warns.
476#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
477#[schemars(transform = crate::schema::strip_int_formats)]
478pub struct ContextWarning {
479 /// The fragment this warning is about.
480 pub fragment_id: u64,
481 /// What happened to it (always [`ContextAction::Retrieve`] today).
482 pub action: ContextAction,
483 /// Lexical relevance to the request query, in `[0, 1]` — the same value
484 /// as the matching `decisions` entry.
485 pub relevance: f32,
486 /// The matching `decisions` entry's `reason`, copied verbatim.
487 pub reason: String,
488}
489
490/// The compiler's output: the assembled context plus its full audit trail.
491#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
492#[schemars(transform = crate::schema::strip_int_formats)]
493pub struct CompiledContext {
494 /// The assembled context, ready to inject into a prompt.
495 pub content: String,
496 /// The output split into ordered blocks (cache prefix first). Emptied by
497 /// [`CompilePolicy::slim_response`].
498 pub sections: Vec<CompiledSection>,
499 /// One decision per input fragment (duplicates included). Emptied by
500 /// [`CompilePolicy::slim_response`].
501 pub decisions: Vec<ContextDecision>,
502 /// One source pointer per distinct fragment.
503 pub sources: Vec<SourceReference>,
504 /// Handles for the fragments that were externalized, not emitted.
505 pub retrieval_handles: Vec<RetrievalHandle>,
506 /// Token (and optional cost) savings of this compilation.
507 pub insights: CompilationInsights,
508 /// Overall fidelity risk (the max over all decisions).
509 pub risk: FidelityRisk,
510 /// Mechanical, low-noise heads-up over `decisions` (V2a-2 quick win):
511 /// every externalized fragment relevant enough to the query that a
512 /// caller should double-check it was not needed. `#[serde(default)]` so
513 /// a pre-0.10.0 caller reading an older stored/replayed response still
514 /// deserializes (defaults to empty).
515 #[serde(default)]
516 pub warnings: Vec<ContextWarning>,
517}
518
519/// One asserted fact inside a [`WorkingContext`].
520#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
521pub struct ContextFact {
522 /// The fact text.
523 pub text: String,
524 /// Where the fact came from, when known.
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub source: Option<SourceReference>,
527}
528
529/// A lightweight pointer to a past [`ContextDecision`].
530#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
531#[schemars(transform = crate::schema::strip_int_formats)]
532pub struct ContextDecisionRef {
533 /// The fragment the decision was about.
534 pub fragment_id: u64,
535 /// The rule that decided.
536 pub rule_id: String,
537}
538
539/// The distilled working state of an agent session — small enough to carry
540/// across sessions, structured enough to resume from. Persisted and reloaded
541/// by the memory bridge (US-002) under `type = working_context` metadata.
542#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
543pub struct WorkingContext {
544 /// What the session is trying to achieve.
545 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub goal: Option<String>,
547 /// Constraints currently in force (never compressed away).
548 #[serde(default)]
549 pub active_constraints: Vec<ContextFact>,
550 /// Facts that were verified, with their sources.
551 #[serde(default)]
552 pub verified_facts: Vec<ContextFact>,
553 /// Hypotheses still open.
554 #[serde(default)]
555 pub open_hypotheses: Vec<ContextFact>,
556 /// Decisions taken so far.
557 #[serde(default)]
558 pub decisions: Vec<ContextDecisionRef>,
559 /// Exact evidence the session relies on (verbatim, addressable).
560 #[serde(default)]
561 pub exact_evidence: Vec<SourceReference>,
562 /// Actions still to do.
563 #[serde(default)]
564 pub pending_actions: Vec<String>,
565}
566
567/// One session recorded in a project's working-context index (V2a-1's
568/// `list_working_contexts` quick win).
569#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
570pub struct WorkingContextSession {
571 /// The session id, as passed to `save_working_context`.
572 pub session: String,
573 /// Unix seconds this session was last saved — updated on every
574 /// `save_working_context` call under this project + session, not just
575 /// the first (a resave never duplicates the entry).
576 pub saved_at: u64,
577}
578
579/// The per-project index [`save_working_context`](crate::MemoryService::save_working_context)
580/// maintains so [`list_working_contexts`](crate::MemoryService::list_working_contexts)
581/// never has to scan the whole store: one system fact per project, appended
582/// (or refreshed) on every save. The REJECTED alternative was an approximate
583/// `query_filtered` scan over working-context facts (capped at
584/// `MAX_RECALL_LIMIT`, imprecise) — this index is exact and O(1) to read.
585#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
586pub struct WorkingContextIndex {
587 /// Every session ever saved under this project.
588 #[serde(default)]
589 pub sessions: Vec<WorkingContextSession>,
590}