use chrono::{SecondsFormat, TimeZone};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct McpCapsule {
pub id: String,
pub timestamp: String,
pub workspace_name: String,
pub workspace_path: String,
pub session_id: Option<String>,
pub commit_sha: Option<String>,
pub kind: String,
pub intent: Option<String>,
pub decision: Option<String>,
pub rationale: Option<String>,
pub symbols: Vec<String>,
pub files: Vec<String>,
pub failure_mode: Option<String>,
pub category: Option<String>,
pub score: Option<f32>,
}
pub fn capsule_hit_to_mcp(
hit: &crate::CapsuleHit,
workspace_name: &str,
workspace_path: &str,
) -> McpCapsule {
let ts = chrono::Utc
.timestamp_millis_opt(hit.ts_ms)
.single()
.map(|dt| dt.to_rfc3339_opts(SecondsFormat::Secs, true))
.unwrap_or_default();
let kind = match hit.meta.source.as_str() {
"git" => "git_commit",
"changelog" => "version",
"note" => "note",
"checkpoint" => "checkpoint",
_ => "agent_turn",
}
.to_string();
let failure_mode = match hit.capsule.failure_mode {
crate::types::FailureMode::None => None,
crate::types::FailureMode::Drift => Some("drift".to_string()),
crate::types::FailureMode::Rediscovery => Some("rediscovery".to_string()),
crate::types::FailureMode::DecisionConflict => Some("decision_conflict".to_string()),
crate::types::FailureMode::RetrySpiral => Some("retry_spiral".to_string()),
crate::types::FailureMode::FalseProgress => Some("false_progress".to_string()),
crate::types::FailureMode::UnboundedHorizon => Some("unbounded_horizon".to_string()),
};
let (files, symbols): (Vec<String>, Vec<String>) = hit
.capsule
.symbols
.iter()
.cloned()
.partition(|s| s.contains('/') || s.contains('.') && !s.starts_with('.'));
McpCapsule {
id: hit.id.clone(),
timestamp: ts,
workspace_name: workspace_name.to_string(),
workspace_path: workspace_path.to_string(),
session_id: hit.meta.agent_session_id.clone(),
commit_sha: hit.commit_sha.clone(),
kind,
intent: nonempty(hit.capsule.intent.trim()),
decision: nonempty(hit.capsule.decision.trim()),
rationale: nonempty(hit.capsule.rationale.trim()),
symbols,
files,
failure_mode,
category: nonempty(&hit.capsule.category),
score: Some(hit.distance),
}
}
fn nonempty(s: &str) -> Option<String> {
if s.is_empty() { None } else { Some(s.to_string()) }
}
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct QueryScope {
pub file: Option<String>,
pub symbol: Option<String>,
pub since: Option<String>,
pub until: Option<String>,
pub session_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RecallInput {
pub query: String,
pub scope: Option<QueryScope>,
pub limit: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TraceDecisionInput {
pub target: String,
pub seeds: Option<usize>,
pub fan_out: Option<usize>,
pub threshold: Option<f32>,
pub since: Option<String>,
pub until: Option<String>,
pub session_id: Option<String>,
pub from_commit: Option<String>,
pub to_commit: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ChallengeInput {
pub proposal: String,
pub limit: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ThreadInput {
pub topic: String,
pub limit: Option<usize>,
pub since: Option<String>,
pub workspaces: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct OrientInput {
pub files: Option<Vec<String>>,
pub symbols: Option<Vec<String>>,
pub intent: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct NoteInput {
pub text: String,
pub decision: Option<String>,
pub rationale: Option<String>,
pub symbols: Option<Vec<String>>,
pub files: Option<Vec<String>>,
pub source: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CapsuleGetInput {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RecallOutput {
pub hits: Vec<McpCapsule>,
pub query_used: String,
pub total_considered: usize,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ChainGap {
pub between: [String; 2],
pub reason: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TraceDecisionOutput {
pub chain: Vec<McpCapsule>,
pub gaps: Vec<ChainGap>,
pub seed_ids: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ChallengeSummaryFacts {
pub earliest_mention: Option<String>,
pub latest_mention: Option<String>,
pub distinct_sessions: usize,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ChallengeOutput {
pub prior_rationale: Vec<McpCapsule>,
pub prior_alternatives: Vec<McpCapsule>,
pub failure_modes_recorded: Vec<McpCapsule>,
pub silence: bool,
pub summary_facts: ChallengeSummaryFacts,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ThreadOutput {
pub hits: Vec<McpCapsule>,
pub workspace_breakdown: std::collections::HashMap<String, usize>,
pub span: ThreadSpan,
pub workspaces_searched: usize,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ThreadSpan {
pub earliest: Option<String>,
pub latest: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DriftSignal {
pub state: String,
pub basin: Option<String>,
pub intensity: f32,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct OrientSuggestion {
pub kind: String,
pub why: String,
pub ref_capsule_ids: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct OrientOutput {
pub recent_touches: Vec<McpCapsule>,
pub drift_signal: Option<DriftSignal>,
pub suggestions: Vec<OrientSuggestion>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct NoteOutput {
pub id: String,
pub persisted_at: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CapsuleGetOutput {
pub capsule: Option<McpCapsule>,
pub raw_text: Option<String>,
pub reference: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ServerMeta {
pub version: String,
pub workspace_name: String,
pub workspace_path: String,
pub writes_enabled: bool,
pub cross_workspace_enabled: bool,
pub embed_model: String,
pub capsule_count: usize,
}