Shared memory and context tools for agentic work.
Code Rooms
// === m1nd-mcp/src/perspective/state.rs ===
// Themes 1, 2, 5, 11, 13, 14, 15, 16 from 12-PERSPECTIVE-SYNTHESIS.
// All perspective and lock state types. All serializable (u64 timestamps, no Instant).
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
// ---------------------------------------------------------------------------
// Enums
/// Perspective navigation mode (Theme 11).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PerspectiveMode {
Anchored,
Local,
}
/// Route family classification (Theme 4, used in keys.rs).
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RouteFamily {
Structural,
Semantic,
Temporal,
Causal,
Ghost,
Hole,
Resonant,
impl RouteFamily {
/// Enum ordinal for deterministic tie-breaking (Theme 4).
pub fn ordinal(&self) -> u8 {
match self {
Self::Structural => 0,
Self::Semantic => 1,
Self::Temporal => 2,
Self::Causal => 3,
Self::Ghost => 4,
Self::Hole => 5,
Self::Resonant => 6,
/// Watch trigger events (Theme 10).
pub enum WatchTrigger {
Ingest,
Learn,
/// Watch strategy for lock watchers (Theme 10).
/// V1: Manual, OnIngest, OnLearn only. Periodic returns WatchStrategyNotSupported.
#[serde(rename_all = "snake_case")]
pub enum WatchStrategy {
Manual,
OnIngest,
OnLearn,
Periodic, // V1: rejected at validation
/// Lock scope type (Theme 14).
pub enum LockScope {
Node,
Subgraph,
QueryNeighborhood,
Path,
// Perspective Lens (Theme 9)
/// Lens configuration controlling what the agent sees.
/// Empty arrays = no filter = all values (consistent across all tools).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PerspectiveLens {
/// Activation dimensions. Default: all 4. Empty = all.
#[serde(default = "default_dimensions")]
pub dimensions: Vec<String>,
/// Route family filter. Empty = all families.
#[serde(default)]
pub route_families: Vec<RouteFamily>,
/// Enable XLR noise cancellation.
#[serde(default = "default_true")]
pub xlr: bool,
/// Include ghost edge detection in route synthesis.
pub include_ghost_edges: bool,
/// Include structural holes in route synthesis.
pub include_structural_holes: bool,
/// Number of top results for route synthesis. Default: 8 (perspective-specific, vs activate's 20).
#[serde(default = "default_perspective_top_k")]
pub top_k: u32,
/// Namespace filter. Empty = all namespaces.
pub namespaces: Vec<String>,
/// Tag filter. Empty = all tags.
pub tags: Vec<String>,
/// Node type filter. Empty = all node types.
pub node_types: Vec<String>,
/// Optional custom ranking weights. None = use defaults.
pub ranking_weights: Option<RankingWeights>,
impl Default for PerspectiveLens {
fn default() -> Self {
Self {
dimensions: default_dimensions(),
route_families: Vec::new(),
xlr: true,
include_ghost_edges: true,
include_structural_holes: true,
top_k: 8,
namespaces: Vec::new(),
tags: Vec::new(),
node_types: Vec::new(),
ranking_weights: None,
/// Custom ranking weights for route scoring (Theme 11).
/// All weights should sum to 1.0. Validated at lens validation time.
pub struct RankingWeights {
pub local_activation: f32,
pub path_coherence: f32,
pub novelty: f32,
pub anchor_relevance: f32,
pub continuity: f32,
impl Default for RankingWeights {
local_activation: 0.35,
path_coherence: 0.25,
novelty: 0.15,
anchor_relevance: 0.15,
continuity: 0.10,
// Mode Context (Theme 11)
/// Mode context passed to every scoring function.
/// Behavioral contract:
/// - Anchored: anchor_relevance weight = 0.15, floor at 0.05.
/// - Local: anchor_relevance = 0.0, redistributed +0.10 to local_activation, +0.05 to novelty.
pub struct ModeContext {
pub mode: PerspectiveMode,
pub anchor_node: Option<String>,
pub anchor_query: Option<String>,
// Navigation (Theme 5)
/// A single navigation event in the perspective history.
/// Cap: 1000 per perspective. Compression: keep first 10, last 200, every 10th in between.
pub struct NavigationEvent {
pub action: String, // "start", "follow", "back", "branch", "lens_change"
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>, // node external_id for follow, branch name for branch
pub timestamp_ms: u64,
pub route_set_version: u64,
/// A saved checkpoint for back/restore operations (Theme 11).
/// Mode is stored so back() restores mode from checkpoint.
pub struct PerspectiveCheckpoint {
pub focus_node: Option<String>,
pub lens: PerspectiveLens,
// Route types (Theme 4, 12, 13)
/// A single route in the perspective route set.
pub struct Route {
/// Stable content-addressed ID: R_{hash[:6]}. Survives graph rebuild.
pub route_id: String,
/// 1-based page-local position.
pub route_index: u32,
/// Route family classification.
pub family: RouteFamily,
/// Target node external ID.
pub target_node: String,
/// Target node label (human-readable).
pub target_label: String,
/// One-line reason for this route.
pub reason: String,
/// Combined score [0.0, 1.0].
pub score: f32,
/// Whether peek is available for this route.
pub peek_available: bool,
/// Provenance information (optional).
pub provenance: Option<RouteProvenance>,
/// Provenance for a route target (relative paths only per Theme 6).
pub struct RouteProvenance {
pub source_path: Option<String>,
pub line_start: Option<u32>,
pub line_end: Option<u32>,
/// Cached route set with version for staleness detection (Theme 1).
pub struct CachedRouteSet {
pub routes: Vec<Route>,
pub total_routes: usize,
pub page_size: u32,
pub version: u64, // timestamp-based for monotonicity across restarts (Theme 15)
pub synthesis_elapsed_ms: f64,
pub captured_cache_generation: u64,
// Diagnostic (Theme 12)
/// Diagnostic object included when routes/suggest/affinity return empty results.
pub struct Diagnostic {
pub sources_checked: Vec<String>,
pub sources_with_results: Vec<String>,
pub sources_failed: Vec<String>,
pub reason: String, // "well_connected" | "under_indexed" | "narrow_lens" | "graph_empty" | "query_mismatch" | "dead_end" | "all_visited"
pub suggestion: String,
pub graph_stats: DiagnosticGraphStats,
pub struct DiagnosticGraphStats {
pub node_count: u32,
pub edge_count: u64,
// Affinity candidate (Theme 13)
/// A hypothesized connection candidate. NEVER phrase as certainty.
/// Max confidence capped at 0.85. Minimum threshold: 0.15.
pub struct AffinityCandidate {
/// Candidate node external ID.
pub candidate_node: String,
/// Candidate node label.
pub candidate_label: String,
/// Kind of hypothesized connection.
pub kind: AffinityCandidateKind,
/// Combined confidence [0.15, 0.85]. Geometric mean of normalized sources.
pub confidence: f32,
/// Top-level epistemic guard: always true for affinity candidates.
pub is_hypothetical: bool,
/// V1: null. Only inferred from neighbor edge patterns in V2.
pub proposed_relation: Option<String>,
/// Per-source confidence scores.
pub confidence_breakdown: ConfidenceBreakdown,
/// Affinity candidate kind. Uses "hypothesized_" prefix per Theme 13.
pub enum AffinityCandidateKind {
HypothesizedLatentEdge,
MissingBridge,
ResonantNeighbor,
/// Per-source confidence breakdown (Theme 13).
/// All normalized to [0.0, 1.0].
pub struct ConfidenceBreakdown {
/// Ghost edge: sqrt(raw) to spread biased-low distribution.
pub ghost_edge_strength: Option<f32>,
/// Structural hole: (avg - min_sibling) / (1.0 - min_sibling).
pub structural_hole_pressure: Option<f32>,
/// Resonant amplitude: divide by max amplitude in report.
pub resonant_amplitude: Option<f32>,
/// Semantic overlap: cosine similarity, already [0, 1].
pub semantic_overlap: Option<f32>,
/// Provenance: 1.0 same file within 50 lines, 0.5 same file, 0.0 otherwise.
pub provenance_overlap: Option<f32>,
/// Route-path neighborhood: 1.0 / (1.0 + hop_distance).
pub route_path_neighborhood: Option<f32>,
// Suggest types (Theme 12)
/// Suggestion output from the "next best move" advisor.
pub struct SuggestResult {
/// Recommended action (e.g., "follow R_abc123", "inspect R_def456").
pub recommended_action: String,
/// Confidence [0.0, 1.0].
/// Why this suggestion.
pub why: String,
/// What the suggestion is based on.
pub based_on: String, // "navigation_history" | "initial_ranking" | "exhaustion_recovery"
/// Up to 3 alternatives (Theme 5 cap).
pub alternatives: Vec<SuggestAlternative>,
pub struct SuggestAlternative {
pub action: String,
// PerspectiveState (Theme 2 — the core stateful struct)
/// Full perspective state for a single agent's navigation session.
/// Storage: `HashMap<(String, String), PerspectiveState>` keyed by (agent_id, perspective_id).
/// Limits: max 5 per agent, max 50MB combined memory.
pub struct PerspectiveState {
/// Unique perspective ID: `persp_{agent_id_short}_{counter}` (max 20 chars).
pub perspective_id: String,
/// Owning agent ID.
pub agent_id: String,
/// Current navigation mode.
/// Anchor node external ID (None if local mode).
/// Original start query.
/// Current focus node external ID.
/// Current lens configuration.
/// Navigation path (ordered list of visited node external_ids).
pub entry_path: Vec<String>,
/// Navigation history (capped at 1000 events).
pub navigation_history: Vec<NavigationEvent>,
/// Saved checkpoints for back/restore (capped at 200, LRU eviction).
pub checkpoints: Vec<PerspectiveCheckpoint>,
/// Set of visited node external_ids (survives checkpoint compression, Theme 13).
pub visited_nodes: HashSet<String>,
/// Cached route set (None until first routes call).
pub route_cache: Option<CachedRouteSet>,
/// Route set version (timestamp-based for monotonicity, Theme 15).
/// Cache generation at time of last route synthesis (Theme 1).
/// Whether this perspective is stale (invalidated by rebuild_engines, Theme 16).
pub stale: bool,
/// Creation timestamp (u64 epoch-ms, Theme 15).
pub created_at_ms: u64,
/// Last access timestamp (u64 epoch-ms).
pub last_accessed_ms: u64,
/// Branch names (capped at 10 per agent, Theme 5).
pub branches: Vec<String>,
// Lock types (Theme 2, 14)
/// Lock scope configuration.
pub struct LockScopeConfig {
pub scope_type: LockScope,
/// Root nodes for the scope. Non-empty for Subgraph, Node. Resolved for QueryNeighborhood.
pub root_nodes: Vec<String>,
/// BFS radius for Subgraph scope. Min 1, max 4. Exactly 0 for Node scope.
pub radius: Option<u32>,
/// Query string for QueryNeighborhood scope.
pub query: Option<String>,
/// Ordered list of node external_ids for Path scope.
pub path_nodes: Option<Vec<String>>,
/// Snapshot of a subgraph at lock creation time (Theme 4, 14).
pub struct LockSnapshot {
/// Set of node external_ids in scope.
pub nodes: HashSet<String>,
/// Edges by content-addressable key (Theme 4).
pub edges: HashMap<String, EdgeSnapshotEntry>,
/// Graph generation at capture time.
pub graph_generation: u64,
/// Capture timestamp (u64 epoch-ms).
pub captured_at_ms: u64,
/// Key format version for future migration.
pub key_format: String, // "v1_content_addr"
/// A single edge in a lock baseline snapshot.
pub struct EdgeSnapshotEntry {
pub source: String,
pub target: String,
pub relation: String,
pub weight: f32,
/// Watcher configuration for a lock (Theme 10).
/// One watcher per lock in V1. lock.watch replaces existing strategy.
pub struct WatchConfig {
pub strategy: WatchStrategy,
/// Initialized to max(lock.created_at_ms, lock.last_diff_ms).
pub last_scan_ms: u64,
/// Full lock state (Theme 2, 14).
/// Storage: `HashMap<String, LockState>` keyed by lock_id.
/// Limits: max 10 per agent, max 2000 baseline nodes, max 10000 baseline edges.
pub struct LockState {
/// Unique lock ID: `lock_{agent_id_short}_{counter}` (max 20 chars).
pub lock_id: String,
/// Owning agent ID. Checked on every operation.
/// Scope configuration.
pub scope: LockScopeConfig,
/// Baseline snapshot captured at lock creation.
pub baseline: LockSnapshot,
/// Optional watcher configuration (Theme 10).
pub watcher: Option<WatchConfig>,
/// Whether baseline is stale (set by rebuild_engines invalidation, Theme 16).
pub baseline_stale: bool,
/// Creation timestamp (u64 epoch-ms).
/// Last diff computation timestamp.
pub last_diff_ms: u64,
/// Pending watcher event (Theme 10).
pub struct WatcherEvent {
pub trigger: WatchTrigger,
// Limits (Theme 5)
/// Hard caps enforced at creation/mutation time. All configurable.
pub struct PerspectiveLimits {
pub max_perspectives_per_agent: usize,
pub max_locks_per_agent: usize,
pub max_branches_per_agent: usize,
pub max_nav_events_per_perspective: usize,
pub max_checkpoints_per_perspective: usize,
pub max_route_set_snapshots: usize,
pub max_lock_baseline_nodes: usize,
pub max_lock_baseline_edges: usize,
pub max_lock_subgraph_radius: u32,
pub max_affinity_candidates: usize,
pub max_inspect_chars: usize,
pub max_suggest_alternatives: usize,
pub max_compare_chars: usize,
pub max_lock_diff_new_nodes: usize,
pub max_lock_diff_new_edges: usize,
pub max_total_memory_bytes: usize,
impl Default for PerspectiveLimits {
max_perspectives_per_agent: 5,
max_locks_per_agent: 10,
max_branches_per_agent: 10,
max_nav_events_per_perspective: 1000,
max_checkpoints_per_perspective: 200,
max_route_set_snapshots: 10,
max_lock_baseline_nodes: 2000,
max_lock_baseline_edges: 10000,
max_lock_subgraph_radius: 4,
max_affinity_candidates: 8,
max_inspect_chars: 1500,
max_suggest_alternatives: 3,
max_compare_chars: 3000,
max_lock_diff_new_nodes: 50,
max_lock_diff_new_edges: 100,
max_total_memory_bytes: 50 * 1024 * 1024, // 50MB
// Peek types (Theme 6)
/// Security-checked peek result.
pub struct PeekContent {
/// The extracted content (truncated on char boundaries, max 2000 chars).
pub content: String,
/// Whether content was truncated.
pub truncated: bool,
/// Whether provenance is stale (file newer than graph's last ingest).
pub provenance_stale: bool,
/// Whether lossy encoding was applied.
pub encoding_lossy: bool,
/// Relative path (ingest root stripped, Theme 6).
pub relative_path: String,
/// Line range of the extracted content.
pub line_start: u32,
pub line_end: u32,
/// Peek security configuration (Theme 6).
pub struct PeekSecurityConfig {
/// Allow-list of ingest root paths.
pub allow_roots: Vec<String>,
/// Max file size to read (bytes). Default: 10MB.
pub max_file_size: u64,
/// Max lines before provenance line.
pub max_lines_before: u32,
/// Max lines after provenance line.
pub max_lines_after: u32,
/// Max chars in output.
pub max_chars: usize,
impl Default for PeekSecurityConfig {
allow_roots: Vec::new(),
max_file_size: 10 * 1024 * 1024,
max_lines_before: 20,
max_lines_after: 30,
max_chars: 2000,
// Lock diff output (Theme 14)
/// Result of diffing a lock against current graph state.
pub struct LockDiffResult {
/// If graph_generation unchanged since last diff, this is true and other fields are empty.
pub no_changes: bool,
pub new_nodes: Vec<String>,
pub removed_nodes: Vec<String>,
pub new_edges: Vec<String>, // content-addressable edge keys
pub removed_edges: Vec<String>,
/// Edges where one endpoint is in scope and the other is not.
pub boundary_edges_added: Vec<String>,
pub boundary_edges_removed: Vec<String>,
/// Weight changes for existing edges.
pub weight_changes: Vec<EdgeWeightChange>,
/// Whether baseline is stale and needs rebase.
pub elapsed_ms: f64,
pub struct EdgeWeightChange {
pub edge_key: String,
pub old_weight: f32,
pub new_weight: f32,
// Default helpers
fn default_dimensions() -> Vec<String> {
vec![
"structural".into(),
"semantic".into(),
"temporal".into(),
"causal".into(),
]
fn default_true() -> bool {
true
fn default_perspective_top_k() -> u32 {
8
// Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perspective_lens_defaults_match_spec() {
let lens = PerspectiveLens::default();
assert_eq!(lens.dimensions.len(), 4);
assert!(lens.xlr);
assert!(lens.include_ghost_edges);
assert!(lens.include_structural_holes);
assert_eq!(lens.top_k, 8); // perspective-specific, not activate's 20
assert!(lens.route_families.is_empty()); // empty = all
fn ranking_weights_default_sum_to_one() {
let w = RankingWeights::default();
let sum =
w.local_activation + w.path_coherence + w.novelty + w.anchor_relevance + w.continuity;
assert!((sum - 1.0).abs() < 0.001);
fn limits_defaults_match_synthesis() {
let l = PerspectiveLimits::default();
assert_eq!(l.max_perspectives_per_agent, 5);
assert_eq!(l.max_locks_per_agent, 10);
assert_eq!(l.max_lock_baseline_nodes, 2000);
assert_eq!(l.max_lock_subgraph_radius, 4);
assert_eq!(l.max_affinity_candidates, 8);
assert_eq!(l.max_total_memory_bytes, 50 * 1024 * 1024);
fn perspective_state_serializes_round_trip() {
let state = PerspectiveState {
perspective_id: "persp_jimi_001".into(),
agent_id: "jimi".into(),
mode: PerspectiveMode::Anchored,
anchor_node: Some("session.rs".into()),
anchor_query: Some("session management".into()),
focus_node: Some("session.rs".into()),
lens: PerspectiveLens::default(),
entry_path: vec!["session.rs".into()],
navigation_history: Vec::new(),
checkpoints: Vec::new(),
visited_nodes: HashSet::new(),
route_cache: None,
route_set_version: 1710000000000,
captured_cache_generation: 0,
stale: false,
created_at_ms: 1710000000000,
last_accessed_ms: 1710000000000,
branches: Vec::new(),
};
let json = serde_json::to_string(&state).unwrap();
let deserialized: PerspectiveState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.perspective_id, "persp_jimi_001");
assert_eq!(deserialized.mode, PerspectiveMode::Anchored);
fn lock_state_serializes_round_trip() {
let lock = LockState {
lock_id: "lock_jimi_001".into(),
scope: LockScopeConfig {
scope_type: LockScope::Node,
root_nodes: vec!["session.rs".into()],
radius: None,
query: None,
path_nodes: None,
},
baseline: LockSnapshot {
nodes: HashSet::new(),
edges: HashMap::new(),
graph_generation: 0,
captured_at_ms: 1710000000000,
key_format: "v1_content_addr".into(),
watcher: None,
baseline_stale: false,
last_diff_ms: 1710000000000,
let json = serde_json::to_string(&lock).unwrap();
let deserialized: LockState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.lock_id, "lock_jimi_001");
assert_eq!(deserialized.scope.scope_type, LockScope::Node);
fn route_family_ordinal_is_stable() {
assert_eq!(RouteFamily::Structural.ordinal(), 0);
assert_eq!(RouteFamily::Resonant.ordinal(), 6);
fn affinity_candidate_epistemic_guard() {
let c = AffinityCandidate {
candidate_node: "foo".into(),
candidate_label: "Foo".into(),
kind: AffinityCandidateKind::HypothesizedLatentEdge,
confidence: 0.42,
is_hypothetical: true,
proposed_relation: None,
confidence_breakdown: ConfidenceBreakdown {
ghost_edge_strength: Some(0.6),
structural_hole_pressure: None,
resonant_amplitude: None,
semantic_overlap: Some(0.3),
provenance_overlap: None,
route_path_neighborhood: None,
assert!(c.is_hypothetical);
assert!(c.confidence <= 0.85);
assert!(c.proposed_relation.is_none()); // V1: always None