use serde::{Deserialize, Serialize};
use crate::perspective::state::{
AffinityCandidate, Diagnostic, PeekContent, PerspectiveLens, PerspectiveMode, Route,
RouteFamily, SuggestResult,
};
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveStartInput {
pub agent_id: String,
pub query: String,
#[serde(default)]
pub anchor_node: Option<String>,
#[serde(default)]
pub lens: Option<PerspectiveLens>,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveStartOutput {
pub perspective_id: String,
pub mode: PerspectiveMode,
pub anchor_node: Option<String>,
pub focus_node: Option<String>,
pub routes: Vec<Route>,
pub total_routes: usize,
pub page: u32,
pub total_pages: u32,
pub route_set_version: u64,
pub cache_generation: u64,
pub suggested: Option<String>, pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveRoutesInput {
pub agent_id: String,
pub perspective_id: String,
#[serde(default = "default_page")]
pub page: u32,
#[serde(default = "default_page_size")]
pub page_size: u32,
#[serde(default)]
pub route_set_version: Option<u64>,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveRoutesOutput {
pub perspective_id: String,
pub mode: PerspectiveMode,
pub mode_effective: String, pub anchor: Option<String>,
pub focus: Option<String>,
pub lens_summary: String, pub page: u32,
pub total_pages: u32,
pub total_routes: usize,
pub route_set_version: u64,
pub cache_generation: u64,
pub routes: Vec<Route>,
pub suggested: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostic: Option<Diagnostic>,
#[serde(skip_serializing_if = "Option::is_none")]
pub family_diversity_warning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dominant_family: Option<RouteFamily>,
#[serde(default)]
pub page_size_clamped: bool,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveInspectInput {
pub agent_id: String,
pub perspective_id: String,
#[serde(default)]
pub route_id: Option<String>,
#[serde(default)]
pub route_index: Option<u32>,
pub route_set_version: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveInspectOutput {
pub route_id: String,
pub route_index: u32,
pub family: RouteFamily,
pub target_node: String,
pub target_label: String,
pub target_type: String,
pub path_preview: Vec<String>,
pub family_explanation: String,
pub score: f32,
pub score_breakdown: InspectScoreBreakdown,
#[serde(skip_serializing_if = "Option::is_none")]
pub provenance: Option<InspectProvenance>,
pub peek_available: bool,
pub affinity_candidates: Vec<AffinityCandidate>,
pub response_chars: usize,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct InspectScoreBreakdown {
pub local_activation: f32,
pub path_coherence: f32,
pub novelty: f32,
pub anchor_relevance: Option<f32>, pub continuity: Option<f32>, }
#[derive(Clone, Debug, Serialize)]
pub struct InspectProvenance {
pub source_path: Option<String>,
pub line_start: Option<u32>,
pub line_end: Option<u32>,
pub namespace: Option<String>,
pub provenance_stale: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectivePeekInput {
pub agent_id: String,
pub perspective_id: String,
#[serde(default)]
pub route_id: Option<String>,
#[serde(default)]
pub route_index: Option<u32>,
pub route_set_version: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectivePeekOutput {
pub route_id: String,
pub route_index: u32,
pub target_node: String,
pub content: PeekContent,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveFollowInput {
pub agent_id: String,
pub perspective_id: String,
#[serde(default)]
pub route_id: Option<String>,
#[serde(default)]
pub route_index: Option<u32>,
pub route_set_version: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveFollowOutput {
pub perspective_id: String,
pub previous_focus: Option<String>,
pub new_focus: String,
pub mode: PerspectiveMode,
pub mode_effective: String,
pub routes: Vec<Route>,
pub total_routes: usize,
pub page: u32,
pub total_pages: u32,
pub route_set_version: u64,
pub cache_generation: u64,
pub suggested: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostic: Option<Diagnostic>,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveSuggestInput {
pub agent_id: String,
pub perspective_id: String,
pub route_set_version: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveSuggestOutput {
pub perspective_id: String,
pub suggestion: SuggestResult,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostic: Option<Diagnostic>,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveAffinityInput {
pub agent_id: String,
pub perspective_id: String,
#[serde(default)]
pub route_id: Option<String>,
#[serde(default)]
pub route_index: Option<u32>,
pub route_set_version: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveAffinityOutput {
pub route_id: String,
pub target_node: String,
pub notice: String, pub candidates: Vec<AffinityCandidate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostic: Option<Diagnostic>,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveBranchInput {
pub agent_id: String,
pub perspective_id: String,
#[serde(default)]
pub branch_name: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveBranchOutput {
pub perspective_id: String,
pub branch_perspective_id: String,
pub branch_name: String,
pub branched_from_focus: Option<String>,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveBackInput {
pub agent_id: String,
pub perspective_id: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveBackOutput {
pub perspective_id: String,
pub restored_focus: Option<String>,
pub restored_mode: PerspectiveMode,
pub routes: Vec<Route>,
pub total_routes: usize,
pub page: u32,
pub total_pages: u32,
pub route_set_version: u64,
pub cache_generation: u64,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveCompareInput {
pub agent_id: String,
pub perspective_id_a: String,
pub perspective_id_b: String,
#[serde(default)]
pub dimensions: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveCompareOutput {
pub perspective_id_a: String,
pub perspective_id_b: String,
pub shared_nodes: Vec<String>,
pub unique_to_a: Vec<String>,
pub unique_to_b: Vec<String>,
pub dimension_deltas: Vec<DimensionDelta>,
pub response_chars: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_mismatch_warning: Option<String>,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct DimensionDelta {
pub dimension: String,
pub score_a: f32,
pub score_b: f32,
pub delta: f32,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveListInput {
pub agent_id: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveListOutput {
pub agent_id: String,
pub perspectives: Vec<PerspectiveSummary>,
pub total_memory_bytes: usize,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveSummary {
pub perspective_id: String,
pub mode: PerspectiveMode,
pub focus_node: Option<String>,
pub route_count: usize,
pub nav_event_count: usize,
pub stale: bool,
pub created_at_ms: u64,
pub last_accessed_ms: u64,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PerspectiveCloseInput {
pub agent_id: String,
pub perspective_id: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct PerspectiveCloseOutput {
pub perspective_id: String,
pub closed: bool,
pub locks_released: Vec<String>,
pub proof_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_suggested_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_step_hint: Option<String>,
}
fn default_page() -> u32 {
1
}
fn default_page_size() -> u32 {
6
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_input_deserializes_minimal() {
let json = r#"{"agent_id": "jimi", "query": "session management"}"#;
let input: PerspectiveStartInput = serde_json::from_str(json).unwrap();
assert_eq!(input.agent_id, "jimi");
assert_eq!(input.query, "session management");
assert!(input.anchor_node.is_none());
assert!(input.lens.is_none());
}
#[test]
fn routes_input_defaults() {
let json = r#"{"agent_id": "jimi", "perspective_id": "persp_jimi_001"}"#;
let input: PerspectiveRoutesInput = serde_json::from_str(json).unwrap();
assert_eq!(input.page, 1);
assert_eq!(input.page_size, 6);
}
#[test]
fn inspect_input_requires_route_ref() {
let json = r#"{"agent_id": "jimi", "perspective_id": "p1", "route_set_version": 100}"#;
let input: PerspectiveInspectInput = serde_json::from_str(json).unwrap();
assert!(input.route_id.is_none());
assert!(input.route_index.is_none());
}
#[test]
fn follow_input_accepts_route_id() {
let json = r#"{"agent_id": "jimi", "perspective_id": "p1", "route_id": "R_abc123", "route_set_version": 100}"#;
let input: PerspectiveFollowInput = serde_json::from_str(json).unwrap();
assert_eq!(input.route_id.as_deref(), Some("R_abc123"));
}
#[test]
fn compare_input_deserializes() {
let json = r#"{"agent_id": "jimi", "perspective_id_a": "p1", "perspective_id_b": "p2"}"#;
let input: PerspectiveCompareInput = serde_json::from_str(json).unwrap();
assert!(input.dimensions.is_empty()); }
}