Skip to main content

remem/context_bundle/
domain.rs

1//! Versioned DTOs for the Context Bundle v1 contract (GH-932).
2//!
3//! Every top-level DTO carries `schema_version` so later revisions can
4//! break the shape explicitly. Serialization is serde JSON with snake_case
5//! enum values; `tests/schema.rs` pins the exact structure.
6
7use serde::{Deserialize, Serialize};
8
9/// Version of the request/plan/bundle/audit JSON shapes.
10pub const CONTEXT_BUNDLE_SCHEMA_VERSION: u32 = 1;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum AgentRole {
15    Coder,
16    Reviewer,
17    Planner,
18    Researcher,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum RiskClass {
24    Low,
25    Medium,
26    High,
27}
28
29/// Bounded degradation contract: enrichment/vector/rerank incompatibility
30/// may degrade to `CanonicalOnly`; only canonical schema or scope-safety
31/// failures produce `Blocked`.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum DegradedMode {
35    Full,
36    CanonicalOnly,
37    Blocked,
38}
39
40/// Where an item's text came from. Generated or graph-derived projections
41/// explain "why it was found" and must never masquerade as canonical
42/// memory.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum SourceKind {
46    Canonical,
47    Generated,
48    GraphDerived,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ItemValidity {
54    Current,
55    Stale,
56    Superseded,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum TrustClass {
62    Trusted,
63    Standard,
64    Quarantined,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum ContextIntent {
70    SessionStart,
71    // GH-934 retrieval-router intents. The router compiles a
72    // `RetrievalPlan` for these; `SessionStart` stays the bundle-planner
73    // intent and is not routable (see `crate::retrieval_router`).
74    ResumeWork,
75    ExplainDecision,
76    DebugFailure,
77    ApplyPreference,
78    ReviewChange,
79    ExploreHistory,
80}
81
82/// The retrieval channels the v1 planner knows about; they mirror the
83/// SessionStart sections.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum ChannelKind {
87    Preferences,
88    Lessons,
89    Core,
90    Workstreams,
91    MemoryIndex,
92    Sessions,
93}
94
95impl ChannelKind {
96    /// Deterministic execution/budget order; mirrors the SessionStart
97    /// render order (lessons before core before index before sessions).
98    pub const ORDERED: [ChannelKind; 6] = [
99        ChannelKind::Preferences,
100        ChannelKind::Lessons,
101        ChannelKind::Core,
102        ChannelKind::Workstreams,
103        ChannelKind::MemoryIndex,
104        ChannelKind::Sessions,
105    ];
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct ProjectRef {
110    pub key: String,
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub struct ContextRequest {
115    pub schema_version: u32,
116    pub task: String,
117    pub project: ProjectRef,
118    pub branch: Option<String>,
119    pub worktree: Option<String>,
120    pub role: AgentRole,
121    pub as_of_epoch: i64,
122    pub token_budget: u32,
123    pub risk: RiskClass,
124    pub include_superseded: bool,
125}
126
127/// One candidate or selected context item. `stable_key` follows the
128/// SessionStart identity convention (`memory:<id>`, `session_summary:<id>`).
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct ContextItem {
131    pub stable_key: String,
132    pub channel: ChannelKind,
133    pub title: String,
134    pub text: String,
135    pub source_kind: SourceKind,
136    pub canonical_ref: Option<String>,
137    pub projection_ref: Option<String>,
138    pub evidence_refs: Vec<String>,
139    pub validity: ItemValidity,
140    pub trust: TrustClass,
141    pub project: Option<String>,
142    pub branch: Option<String>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct PlannedChannel {
147    pub channel: ChannelKind,
148    pub item_limit: u32,
149    /// Whether the SessionStart relevance selector governs this channel.
150    pub relevance_governed: bool,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct ContextFilters {
155    pub project: String,
156    pub branch: Option<String>,
157    pub include_superseded: bool,
158    pub as_of_epoch: i64,
159}
160
161/// Per-section token budgets plus the total request budget.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub struct SectionBudgets {
164    pub total_tokens: u32,
165    pub preferences: u32,
166    pub lessons: u32,
167    pub core: u32,
168    pub workstreams: u32,
169    pub memory_index: u32,
170    pub sessions: u32,
171}
172
173impl SectionBudgets {
174    pub fn for_channel(&self, channel: ChannelKind) -> u32 {
175        match channel {
176            ChannelKind::Preferences => self.preferences,
177            ChannelKind::Lessons => self.lessons,
178            ChannelKind::Core => self.core,
179            ChannelKind::Workstreams => self.workstreams,
180            ChannelKind::MemoryIndex => self.memory_index,
181            ChannelKind::Sessions => self.sessions,
182        }
183    }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ContextPlan {
188    pub schema_version: u32,
189    pub policy_version: String,
190    pub relevance_policy_version: String,
191    pub intent: ContextIntent,
192    pub relevance_query: Option<String>,
193    pub relevance_k: u32,
194    pub channels: Vec<PlannedChannel>,
195    pub filters: ContextFilters,
196    pub section_budgets: SectionBudgets,
197    /// SHA-256 over the canonical plan JSON with this field empty.
198    pub plan_hash: String,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202pub struct AuditEntry {
203    pub stable_key: String,
204    pub channel: ChannelKind,
205    pub source_kind: SourceKind,
206    pub validity: ItemValidity,
207    pub selected: bool,
208    /// Machine-readable snake_case reason; see `policy` reason constants
209    /// and the SessionStart relevance drop reasons.
210    pub reason: String,
211    pub relevance_score: Option<f64>,
212    pub token_estimate: u32,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216pub struct ContextAudit {
217    pub schema_version: u32,
218    pub policy_version: String,
219    pub relevance_policy_version: String,
220    pub plan_hash: String,
221    pub degraded_mode: DegradedMode,
222    pub candidates_considered: u32,
223    pub selected_count: u32,
224    pub dropped_count: u32,
225    pub token_estimate: u32,
226    pub token_budget: u32,
227    pub truncation_reason: Option<String>,
228    pub entries: Vec<AuditEntry>,
229}
230
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub struct ContextBundle {
233    pub schema_version: u32,
234    pub plan_hash: String,
235    pub degraded_mode: DegradedMode,
236    pub preferences: Vec<ContextItem>,
237    pub failure_lessons: Vec<ContextItem>,
238    pub current_truth: Vec<ContextItem>,
239    pub workstreams: Vec<ContextItem>,
240    pub memory_index: Vec<ContextItem>,
241    pub recent_sessions: Vec<ContextItem>,
242    pub audit: ContextAudit,
243}
244
245impl ContextBundle {
246    pub fn section_mut(&mut self, channel: ChannelKind) -> &mut Vec<ContextItem> {
247        match channel {
248            ChannelKind::Preferences => &mut self.preferences,
249            ChannelKind::Lessons => &mut self.failure_lessons,
250            ChannelKind::Core => &mut self.current_truth,
251            ChannelKind::Workstreams => &mut self.workstreams,
252            ChannelKind::MemoryIndex => &mut self.memory_index,
253            ChannelKind::Sessions => &mut self.recent_sessions,
254        }
255    }
256}