Shared memory and context tools for agentic work.
Code Rooms
# m1nd v0.4.0 Contracts -- FORGE-CONTRACTS
## Status: COMPLETE
Agent: `contracts-v04` | Date: 2026-03-15
---
## Overview
6 new tools for v0.4.0: `search`, `help`, `report`, `panoramic`, `savings`, plus `perspective.routes` fix.
All contracts follow existing conventions from `protocol/core.rs`, `protocol/surgical.rs`, `protocol/layers.rs`:
- Input: `#[derive(Clone, Debug, Deserialize)]`
- Output: `#[derive(Clone, Debug, Serialize)]`
- All inputs require `agent_id: String`
- Optional params use `Option<T>` or `#[serde(default = "fn_name")]`
- Doc comments reference PRD section
## 1. search
Full-text + graph-aware code search. Unlike `seek` (intent-based semantic search), `search` is a
lower-level tool that supports literal, regex, and semantic search modes with context lines around
matches -- closer to grep but enriched with graph node references.
### Rust Structs
```rust
// === protocol/v04.rs (or add to protocol/layers.rs) ===
use serde::{Deserialize, Serialize};
// ---------------------------------------------------------------------------
// search
/// Search mode: how the query string is interpreted.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SearchMode {
/// Exact substring match (case-insensitive by default).
Literal,
/// Regular expression (Rust regex crate syntax).
Regex,
/// Semantic: tokenize query, score nodes by activation + trigram similarity.
/// Falls back to Literal if graph is empty.
Semantic,
}
impl Default for SearchMode {
fn default() -> Self {
SearchMode::Literal
/// Input for search.
///
/// Low-level search across ingested codebase. Returns file-level matches
/// with optional context lines and graph node cross-references.
#[derive(Clone, Debug, Deserialize)]
pub struct SearchInput {
/// Calling agent identifier.
pub agent_id: String,
/// Search query string. Interpretation depends on `mode`.
pub query: String,
/// How to interpret the query. Default: "literal".
#[serde(default)]
pub mode: SearchMode,
/// Optional file path prefix filter (e.g. "backend/core/" to scope search).
pub scope: Option<String>,
/// Maximum results to return. Default: 50. Clamped to 1..=500.
#[serde(default = "default_search_top_k")]
pub top_k: usize,
/// Lines of context before and after each match. Default: 2. Clamped to 0..=10.
#[serde(default = "default_context_lines")]
pub context_lines: u32,
/// Case-sensitive matching (literal/regex modes only). Default: false.
pub case_sensitive: bool,
fn default_search_top_k() -> usize { 50 }
fn default_context_lines() -> u32 { 2 }
/// A single search match result.
#[derive(Clone, Debug, Serialize)]
pub struct SearchMatch {
/// Absolute or workspace-relative file path where the match was found.
pub file_path: String,
/// 1-based line number of the match.
pub line_number: u32,
/// Content of the matching line.
pub line_content: String,
/// Lines before the match (up to `context_lines`).
pub context_before: Vec<String>,
/// Lines after the match (up to `context_lines`).
pub context_after: Vec<String>,
/// Graph node ID if this file/symbol is in the graph. None if not ingested.
#[serde(skip_serializing_if = "Option::is_none")]
pub node_id: Option<String>,
/// Relevance score: 1.0 for exact matches, 0.0-1.0 for semantic.
pub match_score: f64,
/// Output for search.
pub struct SearchOutput {
/// Original query echoed back.
/// Search mode used.
pub mode: String,
/// Matching results, ordered by match_score descending.
pub matches: Vec<SearchMatch>,
/// Total matches found (may exceed top_k).
pub total_matches: usize,
/// True when total_matches > top_k (results were truncated).
pub truncated: bool,
/// Elapsed milliseconds.
pub elapsed_ms: f64,
```
### JSON Schema (MCP Registration)
```json
{
"name": "search",
"description": "Low-level code search: literal, regex, or semantic. Returns file matches with context lines and graph node cross-references.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"query": { "type": "string", "description": "Search query string" },
"mode": {
"type": "string",
"enum": ["literal", "regex", "semantic"],
"default": "literal",
"description": "Search mode: literal (substring), regex (pattern), semantic (graph-aware)"
},
"scope": { "type": "string", "description": "File path prefix filter" },
"top_k": { "type": "integer", "default": 50, "description": "Max results (1-500)" },
"context_lines": { "type": "integer", "default": 2, "description": "Lines of context before/after match (0-10)" },
"case_sensitive": { "type": "boolean", "default": false, "description": "Case-sensitive matching" }
"required": ["agent_id", "query"]
### Dispatch Match Arm
"search" => {
let input: v04::SearchInput = serde_json::from_value(params.clone())
.map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_search(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
### Error Variants
No new error variants needed. Uses existing:
- `M1ndError::InvalidParams` -- invalid regex pattern, top_k=0, etc.
- `M1ndError::EmptyGraph` -- semantic mode with no graph (falls back to literal)
- `M1ndError::Serde` -- malformed input
## 2. help
Returns a formatted help string. No JSON struct output -- just a String.
// help
/// Input for help.
/// Returns formatted help text for m1nd tools. When `tool` is specified,
/// returns detailed help for that tool. Otherwise returns overview.
pub struct HelpInput {
/// Optional: specific tool name to get detailed help for (e.g. "activate", "search").
/// Omit for overview of all tools.
pub tool: Option<String>,
/// Output format. Default: "text". Options: "text", "markdown", "json".
#[serde(default = "default_help_format")]
pub format: String,
fn default_help_format() -> String { "text".into() }
Output is `serde_json::Value::String(...)` -- no output struct needed.
The handler builds a formatted string from the tool registry.
### JSON Schema
"name": "help",
"description": "Get help text for m1nd tools. Returns overview or detailed help for a specific tool.",
"tool": { "type": "string", "description": "Specific tool name for detailed help (omit for overview)" },
"format": {
"enum": ["text", "markdown", "json"],
"default": "text",
"description": "Output format"
"required": ["agent_id"]
"help" => {
let input: v04::HelpInput = serde_json::from_value(params.clone())
let text = layer_handlers::handle_help(state, input)?;
Ok(serde_json::Value::String(text))
## 3. m1nd.report
Session intelligence report: what was queried, what was found, graph evolution, and savings.
// m1nd.report
/// Input for m1nd.report.
/// Generates a session intelligence report covering queries made,
/// bugs found, graph evolution, and estimated savings.
pub struct ReportInput {
/// Output format. Default: "markdown".
#[serde(default = "default_report_format")]
pub format: Option<String>,
fn default_report_format() -> Option<String> { Some("markdown".into()) }
/// Top-level output for m1nd.report.
pub struct ReportOutput {
/// Session summary: uptime, queries, graph size.
pub session_summary: SessionSummary,
/// Record of queries made during this session.
pub queries_made: Vec<QueryRecord>,
/// Bugs or anomalies detected by antibody/tremor systems.
pub bugs_found: Vec<BugRecord>,
/// Graph evolution metrics since session start.
pub graph_evolution: GraphEvolution,
/// Estimated token/cost savings from using m1nd.
pub savings: SavingsEstimate,
/// Formatted report text (in requested format).
pub formatted: Option<String>,
/// Session summary section of the report.
pub struct SessionSummary {
/// Server uptime in seconds.
pub uptime_seconds: f64,
/// Total queries processed this session.
pub total_queries: u64,
/// Number of unique agents seen.
pub unique_agents: usize,
/// Current graph node count.
pub node_count: u32,
/// Current graph edge count.
pub edge_count: u64,
/// Number of active perspectives.
pub active_perspectives: usize,
/// Number of active locks.
pub active_locks: usize,
/// A recorded query for the report.
pub struct QueryRecord {
/// Tool name that was called.
pub tool: String,
/// Agent that made the call.
/// Unix timestamp (ms) of the call.
pub timestamp_ms: u64,
/// Elapsed time for this query in ms.
/// Number of results returned.
pub result_count: usize,
/// Brief summary of the query (first 100 chars of query/input).
pub summary: String,
/// A bug/anomaly record for the report.
pub struct BugRecord {
/// Bug category: "antibody_match", "tremor_spike", "layer_violation", "trust_deficit".
pub category: String,
/// Affected node ID.
pub node_id: String,
/// Human-readable description.
pub description: String,
/// Severity: 0.0 (info) to 1.0 (critical).
pub severity: f64,
/// Unix timestamp (ms) when detected.
pub detected_at_ms: u64,
/// Graph evolution metrics.
pub struct GraphEvolution {
/// Nodes added during this session.
pub nodes_added: u32,
/// Nodes removed during this session.
pub nodes_removed: u32,
/// Edges added during this session.
pub edges_added: u64,
/// Edges removed during this session.
pub edges_removed: u64,
/// Number of ingestions performed.
pub ingestions: u32,
/// Number of learn/feedback events.
pub learn_events: u32,
/// Number of apply/write operations.
pub apply_operations: u32,
/// Graph generation at session start.
pub generation_start: u64,
/// Graph generation now.
pub generation_current: u64,
/// Estimated savings from using m1nd vs. manual grep/read.
pub struct SavingsEstimate {
/// Estimated tokens saved by using m1nd instead of reading full files.
pub tokens_saved: u64,
/// Estimated cost saved in USD (at ~$0.01/1K input tokens for Opus).
pub cost_saved_usd: f64,
/// Number of file reads avoided via graph queries.
pub file_reads_avoided: u64,
/// Total lines that would have been read without m1nd.
pub lines_avoided: u64,
"name": "m1nd.report",
"description": "Session intelligence report: queries, bugs, graph evolution, and estimated savings.",
"enum": ["text", "json", "markdown"],
"default": "markdown",
"description": "Output format for the formatted report"
"m1nd.report" => {
let input: v04::ReportInput = serde_json::from_value(params.clone())
let output = layer_handlers::handle_report(state, input)?;
### Implementation Notes
The handler needs access to `SessionState` fields:
- `sessions` -- for agent count and query history
- `queries_processed` -- total count
- `start_time` -- uptime calculation
- `graph` -- node/edge counts
- `perspectives`, `locks` -- active counts
- New: `query_log: Vec<QueryRecord>` field on `SessionState` (capped at 1000 entries)
- New: `session_start_generation: u64` field on `SessionState` (set at boot)
## 4. m1nd.panoramic
Full-graph health overview: per-module risk scores combining trust, tremor, layer violations,
antibody matches, and turbulence.
// m1nd.panoramic
/// Input for m1nd.panoramic.
/// Generates a panoramic health overview of the entire graph or a scoped
/// subset. Combines trust, tremor, layer, antibody, and turbulence signals
/// into a per-module risk score.
pub struct PanoramicInput {
/// Optional file path prefix to scope the analysis.
/// Include recommendation text per module. Default: true.
#[serde(default = "default_true")]
pub include_recommendations: bool,
/// Minimum combined risk score to include a module. Default: 0.0 (include all).
pub min_risk: f64,
/// Maximum modules to return. Default: 100. Clamped to 1..=1000.
#[serde(default = "default_panoramic_limit")]
pub limit: usize,
fn default_true() -> bool { true }
fn default_panoramic_limit() -> usize { 100 }
/// Per-module risk assessment.
pub struct ModuleRisk {
/// Graph node ID for this module.
/// Human-readable label.
pub label: String,
/// Node type (File, Function, Class, Module).
#[serde(rename = "type")]
pub node_type: String,
/// Trust score from TrustLedger (0.0 = untrusted, 1.0 = fully trusted).
pub trust_score: f64,
/// Tremor magnitude from TremorRegistry (0.0 = stable, higher = more volatile).
pub tremor_magnitude: f64,
/// Number of layer violations detected for this node.
pub layer_violations: u32,
/// Turbulence: rate of change in activation patterns (high = unstable).
pub turbulence: f64,
/// Number of antibody pattern matches (potential bugs).
pub antibody_matches: u32,
/// Combined risk score: weighted combination of all signals.
/// Formula: (1-trust)*0.3 + tremor*0.25 + violations*0.2 + turbulence*0.15 + antibodies*0.1
pub combined_risk: f64,
/// Optional recommendation text (populated when include_recommendations=true).
pub recommendation: Option<String>,
/// Provenance: source file path.
pub source_path: Option<String>,
/// Alert: a critical-severity issue detected during panoramic scan.
pub struct PanoramicAlert {
/// Alert category: "high_risk", "trust_deficit", "tremor_spike",
/// "layer_violation_cluster", "antibody_critical".
/// Affected node IDs.
pub affected_nodes: Vec<String>,
/// Severity: 0.0-1.0.
/// Output for m1nd.panoramic.
pub struct PanoramicOutput {
/// Per-module risk assessments, ordered by combined_risk descending.
pub modules: Vec<ModuleRisk>,
/// Overall graph health score (0.0 = critical, 1.0 = perfect).
pub overall_health: f64,
/// Critical alerts that need immediate attention.
pub critical_alerts: Vec<PanoramicAlert>,
/// Total modules scanned (before filtering by min_risk/limit).
pub total_scanned: usize,
"name": "m1nd.panoramic",
"description": "Panoramic graph health overview: per-module risk scores combining trust, tremor, layer violations, antibodies, and turbulence.",
"include_recommendations": { "type": "boolean", "default": true, "description": "Include recommendation text per module" },
"min_risk": { "type": "number", "default": 0.0, "description": "Minimum combined risk to include (0.0-1.0)" },
"limit": { "type": "integer", "default": 100, "description": "Max modules to return (1-1000)" }
"m1nd.panoramic" => {
let input: v04::PanoramicInput = serde_json::from_value(params.clone())
let output = layer_handlers::handle_panoramic(state, input)?;
Reads from existing engines (no new state):
- `state.trust` (TrustLedger) -- `trust_score` per node
- `state.tremor_registry` (TremorRegistry) -- tremor magnitude
- `state.graph` -- layer detection via `state.topology`
- Antibody scan via `state.graph` antibody store
Combined risk formula:
combined_risk = (1.0 - trust_score) * 0.30
+ clamp(tremor_magnitude, 0.0, 1.0) * 0.25
+ clamp(layer_violations as f64 / 5.0, 0.0, 1.0) * 0.20
+ clamp(turbulence, 0.0, 1.0) * 0.15
+ clamp(antibody_matches as f64 / 3.0, 0.0, 1.0) * 0.10
Overall health: `1.0 - (mean of top 10% combined_risk scores)`
## 5. m1nd.savings
Token/cost savings estimator. Standalone tool (subset of report's savings).
// m1nd.savings
/// Input for m1nd.savings.
/// Returns estimated token and cost savings from using m1nd during this session.
pub struct SavingsInput {
/// Per-session savings breakdown.
pub struct SessionSavings {
/// Number of m1nd queries made this session.
pub queries_made: u64,
/// Estimated cost saved in USD.
/// Number of file reads avoided.
/// Session uptime in seconds.
pub session_uptime_seconds: f64,
/// Average tokens saved per query.
pub avg_tokens_per_query: f64,
/// Global (all-time) savings estimate.
pub struct GlobalSavings {
/// Total sessions tracked.
pub total_sessions: u64,
/// Total queries across all sessions.
/// Total tokens saved across all sessions.
pub total_tokens_saved: u64,
/// Total cost saved in USD across all sessions.
pub total_cost_saved_usd: f64,
/// Total file reads avoided across all sessions.
pub total_file_reads_avoided: u64,
/// Output for m1nd.savings.
pub struct SavingsOutput {
/// Current session savings.
pub current_session: SessionSavings,
/// Global (all-time) savings. May be empty if no persistence.
pub global: GlobalSavings,
"name": "m1nd.savings",
"description": "Estimated token and cost savings from using m1nd. Shows current session and global totals.",
"agent_id": { "type": "string", "description": "Calling agent identifier" }
"m1nd.savings" => {
let input: v04::SavingsInput = serde_json::from_value(params.clone())
let output = layer_handlers::handle_savings(state, input)?;
### Savings Estimation Algorithm
Per query, estimate tokens saved:
1. `activate` query: ~2000 tokens saved (avoids reading 5+ files)
2. `impact/predict/counterfactual`: ~3000 tokens (avoids reading blast radius)
3. `surgical_context`: ~1500 tokens (targeted vs full file reads)
4. `seek/scan`: ~1000 tokens (vs grep + read)
5. Other tools: ~500 tokens average
Cost: tokens_saved * $0.00001 (Opus input token price ~$10/1M)
New `SessionState` fields needed:
- `savings_tracker: SavingsTracker` -- accumulates per-query savings
- Global savings: persisted to `~/.m1nd/savings.json` alongside graph snapshot
/// Savings tracker (lives on SessionState).
pub struct SavingsTracker {
pub queries_by_tool: HashMap<String, u64>,
impl SavingsTracker {
pub fn new() -> Self {
Self {
queries_by_tool: HashMap::new(),
tokens_saved: 0,
file_reads_avoided: 0,
lines_avoided: 0,
/// Call after every successful tool dispatch.
pub fn record(&mut self, tool: &str, result_nodes: usize) {
*self.queries_by_tool.entry(tool.to_string()).or_insert(0) += 1;
let (tokens, files, lines) = match tool {
"activate" => (2000, 5, 500),
"impact" | "predict" | "counterfactual" => (3000, 8, 800),
"surgical_context" | "surgical_context_v2" => (1500, 3, 300),
"seek" | "scan" | "search" => (1000, 4, 400),
_ => (500, 2, 200),
};
self.tokens_saved += tokens;
self.file_reads_avoided += files;
self.lines_avoided += lines;
## 6. perspective.routes Fix
The `perspective.routes` handler currently works but has a known issue: it does not
include `route_set_version` validation feedback when the client sends a stale version.
The fix is in the handler, not in the protocol types.
### Current Behavior (perspective_handlers.rs:392)
The handler calls `validate_route_set_version()` which returns `Err(RouteSetStale)` when
version mismatches. This is correct but the error message is generic.
### Fix: Enhanced Staleness Response
Instead of returning an error, return a success response with `stale: true` and the
current version, so the agent can retry without an error path.
// Add to PerspectiveRoutesOutput (in protocol/perspective.rs):
/// Output for m1nd.perspective.routes (enhanced).
pub struct PerspectiveRoutesOutput {
// ... existing fields ...
/// True when the requested route_set_version was stale.
/// When stale=true, routes are re-synthesized from current state.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub stale: bool,
/// The previous version that was requested (when stale=true).
pub requested_version: Option<u64>,
### Handler Fix (perspective_handlers.rs)
pub fn handle_perspective_routes(
state: &mut SessionState,
input: PerspectiveRoutesInput,
) -> M1ndResult<PerspectiveRoutesOutput> {
let persp = require_perspective(state, &input.agent_id, &input.perspective_id)?;
let mut stale = false;
let mut requested_version: Option<u64> = None;
// Instead of erroring on stale version, mark as stale and continue
if let Some(input_version) = input.route_set_version {
if let Some(cached) = &persp.route_cache {
if input_version != cached.route_set_version {
stale = true;
requested_version = Some(input_version);
// Continue with current version instead of erroring
// ... rest of handler unchanged, but include stale + requested_version in output ...
## New Error Variants
No new error variants needed for v0.4.0 tools. All errors map to existing variants:
| Error | Used By |
|-------|---------|
| `InvalidParams { tool, detail }` | search (bad regex), panoramic (invalid limit) |
| `EmptyGraph` | search (semantic fallback), panoramic, report |
| `Serde(e)` | All tools (malformed input) |
## New SessionState Fields
// Add to SessionState in session.rs:
/// Query log for report tool (ring buffer, capped at 1000).
pub query_log: Vec<v04::QueryRecord>,
/// Graph generation at session start (for report's graph_evolution).
pub session_start_generation: u64,
/// Node count at session start.
pub session_start_node_count: u32,
/// Edge count at session start.
pub session_start_edge_count: u64,
/// Savings tracker.
pub savings_tracker: v04::SavingsTracker,
## Protocol Module Update
// === protocol/mod.rs ===
pub mod core;
pub mod perspective;
pub mod lock;
pub mod layers;
pub mod surgical;
pub mod v04; // NEW
pub use self::core::*;
## Summary of Dispatch Arms (server.rs)
Add to `dispatch_core_tool()` match:
## Handler Signatures (layer_handlers.rs)
pub fn handle_search(state: &mut SessionState, input: v04::SearchInput) -> M1ndResult<v04::SearchOutput>;
pub fn handle_help(state: &mut SessionState, input: v04::HelpInput) -> M1ndResult<String>;
pub fn handle_report(state: &mut SessionState, input: v04::ReportInput) -> M1ndResult<v04::ReportOutput>;
pub fn handle_panoramic(state: &mut SessionState, input: v04::PanoramicInput) -> M1ndResult<v04::PanoramicOutput>;
pub fn handle_savings(state: &mut SessionState, input: v04::SavingsInput) -> M1ndResult<v04::SavingsOutput>;
## Verification Checklist
- [ ] All structs have `#[derive(Clone, Debug, Serialize)]` (output) or `#[derive(Clone, Debug, Deserialize)]` (input)
- [ ] All inputs have `agent_id: String`
- [ ] All outputs have `elapsed_ms: f64` where applicable
- [ ] Optional fields use `#[serde(skip_serializing_if = "Option::is_none")]`
- [ ] Default helpers match existing naming convention
- [ ] JSON schemas match Rust struct fields exactly
- [ ] Dispatch arms follow `parse -> handle -> serialize` pattern
- [ ] No new dependencies required (uses existing engines)
- [ ] perspective.routes fix is backward-compatible (new fields are skip_serializing_if)