diff options
| author | main <main@swarm.moe> | 2026-03-20 23:19:33 -0400 |
|---|---|---|
| committer | main <main@swarm.moe> | 2026-03-20 23:19:33 -0400 |
| commit | eb0f0f73b7da9d76ff6833757fd265725d3e4b14 (patch) | |
| tree | 38d64a437cac0518caf2cca5aa4bff5984e64515 /crates/fidget-spinner-cli/src/mcp | |
| parent | ae809af85f6687ae21d7e2f7140aa88354c446cc (diff) | |
| download | fidget_spinner-eb0f0f73b7da9d76ff6833757fd265725d3e4b14.zip | |
Polish metric slices and MCP time projections
Diffstat (limited to 'crates/fidget-spinner-cli/src/mcp')
| -rw-r--r-- | crates/fidget-spinner-cli/src/mcp/output.rs | 15 | ||||
| -rw-r--r-- | crates/fidget-spinner-cli/src/mcp/projection.rs | 216 | ||||
| -rw-r--r-- | crates/fidget-spinner-cli/src/mcp/service.rs | 139 |
3 files changed, 285 insertions, 85 deletions
diff --git a/crates/fidget-spinner-cli/src/mcp/output.rs b/crates/fidget-spinner-cli/src/mcp/output.rs index 2e11e20..494fe23 100644 --- a/crates/fidget-spinner-cli/src/mcp/output.rs +++ b/crates/fidget-spinner-cli/src/mcp/output.rs @@ -118,21 +118,6 @@ pub(crate) fn projected_tool_output( )) } -pub(crate) fn fallback_tool_output( - concise: &impl Serialize, - full: &impl Serialize, - kind: SurfaceKind, - stage: FaultStage, - operation: &str, -) -> Result<ToolOutput, FaultRecord> { - let projection = FallbackJsonProjection::new(concise, full, kind) - .map_err(|error| projection_fault(error, stage, operation))?; - let concise_text = projection - .porcelain_projection(DetailLevel::Concise) - .map_err(|error| projection_fault(error, stage, operation))?; - projected_tool_output(&projection, concise_text, None, stage, operation) -} - pub(crate) fn fallback_detailed_tool_output( concise: &impl Serialize, full: &impl Serialize, diff --git a/crates/fidget-spinner-cli/src/mcp/projection.rs b/crates/fidget-spinner-cli/src/mcp/projection.rs index ca89af0..a36e915 100644 --- a/crates/fidget-spinner-cli/src/mcp/projection.rs +++ b/crates/fidget-spinner-cli/src/mcp/projection.rs @@ -2,15 +2,18 @@ use std::collections::BTreeMap; use fidget_spinner_core::{ AttachmentTargetRef, CommandRecipe, ExperimentAnalysis, ExperimentOutcome, FrontierBrief, - FrontierRecord, MetricValue, NonEmptyText, RunDimensionValue, + FrontierRecord, MetricDefinition, MetricValue, NonEmptyText, RunDimensionDefinition, + RunDimensionValue, TagRecord, }; use fidget_spinner_store_sqlite::{ - ArtifactDetail, ArtifactSummary, ExperimentDetail, ExperimentSummary, FrontierOpenProjection, - FrontierSummary, HypothesisCurrentState, HypothesisDetail, MetricBestEntry, MetricKeySummary, - MetricObservationSummary, ProjectStore, StoreError, VertexSummary, + ArtifactDetail, ArtifactSummary, EntityHistoryEntry, ExperimentDetail, ExperimentSummary, + FrontierOpenProjection, FrontierSummary, HypothesisCurrentState, HypothesisDetail, + MetricBestEntry, MetricKeySummary, MetricObservationSummary, ProjectStore, StoreError, + VertexSummary, }; use libmcp::{ ProjectionError, SelectorProjection, StructuredProjection, SurfaceKind, SurfacePolicy, + TimestampText, }; use serde::Serialize; use serde_json::Value; @@ -44,7 +47,7 @@ pub(crate) struct FrontierSummaryProjection { pub(crate) status: String, pub(crate) active_hypothesis_count: u64, pub(crate) open_experiment_count: u64, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -55,7 +58,7 @@ pub(crate) struct FrontierBriefProjection { pub(crate) unknowns: Vec<String>, pub(crate) revision: u64, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) updated_at: Option<String>, + pub(crate) updated_at: Option<TimestampText>, } #[derive(Clone, Serialize)] @@ -80,8 +83,8 @@ pub(crate) struct FrontierRecordProjection { pub(crate) objective: String, pub(crate) status: String, pub(crate) revision: u64, - pub(crate) created_at: String, - pub(crate) updated_at: String, + pub(crate) created_at: TimestampText, + pub(crate) updated_at: TimestampText, pub(crate) brief: FrontierBriefProjection, } @@ -127,7 +130,7 @@ pub(crate) struct HypothesisSummaryProjection { pub(crate) open_experiment_count: u64, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) latest_verdict: Option<String>, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -139,8 +142,8 @@ pub(crate) struct HypothesisRecordProjection { pub(crate) body: String, pub(crate) tags: Vec<String>, pub(crate) revision: u64, - pub(crate) created_at: String, - pub(crate) updated_at: String, + pub(crate) created_at: TimestampText, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -151,7 +154,7 @@ pub(crate) struct HypothesisReadRecordProjection { pub(crate) summary: String, pub(crate) tags: Vec<String>, pub(crate) revision: u64, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -229,9 +232,9 @@ pub(crate) struct ExperimentSummaryProjection { pub(crate) verdict: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) primary_metric: Option<MetricObservationSummaryProjection>, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) closed_at: Option<String>, + pub(crate) closed_at: Option<TimestampText>, } #[derive(Clone, Serialize)] @@ -246,8 +249,8 @@ pub(crate) struct ExperimentRecordProjection { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) outcome: Option<ExperimentOutcomeProjection>, pub(crate) revision: u64, - pub(crate) created_at: String, - pub(crate) updated_at: String, + pub(crate) created_at: TimestampText, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -262,7 +265,7 @@ pub(crate) struct ExperimentReadRecordProjection { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) verdict: Option<String>, pub(crate) revision: u64, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -329,7 +332,7 @@ pub(crate) struct ArtifactSummaryProjection { pub(crate) locator: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) media_type: Option<String>, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -343,8 +346,8 @@ pub(crate) struct ArtifactRecordProjection { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) media_type: Option<String>, pub(crate) revision: u64, - pub(crate) created_at: String, - pub(crate) updated_at: String, + pub(crate) created_at: TimestampText, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -358,7 +361,7 @@ pub(crate) struct ArtifactReadRecordProjection { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) media_type: Option<String>, pub(crate) revision: u64, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -452,7 +455,7 @@ pub(crate) struct ExperimentOutcomeProjection { pub(crate) rationale: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) analysis: Option<ExperimentAnalysisProjection>, - pub(crate) closed_at: String, + pub(crate) closed_at: TimestampText, } #[derive(Clone, Serialize)] @@ -484,7 +487,7 @@ pub(crate) struct VertexSummaryProjection { pub(crate) title: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option<String>, - pub(crate) updated_at: String, + pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] @@ -515,6 +518,82 @@ pub(crate) struct MetricBestOutput { pub(crate) entries: Vec<MetricBestEntryProjection>, } +#[derive(Clone, Serialize)] +pub(crate) struct TagRecordProjection { + pub(crate) name: String, + pub(crate) description: String, + pub(crate) created_at: TimestampText, +} + +#[derive(Clone, Serialize, libmcp::ToolProjection)] +#[libmcp(kind = "mutation")] +pub(crate) struct TagRecordOutput { + pub(crate) record: TagRecordProjection, +} + +#[derive(Clone, Serialize, libmcp::ToolProjection)] +#[libmcp(kind = "list")] +pub(crate) struct TagListOutput { + pub(crate) count: usize, + pub(crate) tags: Vec<TagRecordProjection>, +} + +#[derive(Clone, Serialize)] +pub(crate) struct MetricDefinitionProjection { + pub(crate) key: String, + pub(crate) unit: String, + pub(crate) objective: String, + pub(crate) visibility: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) description: Option<String>, + pub(crate) created_at: TimestampText, + pub(crate) updated_at: TimestampText, +} + +#[derive(Clone, Serialize, libmcp::ToolProjection)] +#[libmcp(kind = "mutation")] +pub(crate) struct MetricDefinitionOutput { + pub(crate) record: MetricDefinitionProjection, +} + +#[derive(Clone, Serialize)] +pub(crate) struct RunDimensionDefinitionProjection { + pub(crate) key: String, + pub(crate) value_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) description: Option<String>, + pub(crate) created_at: TimestampText, + pub(crate) updated_at: TimestampText, +} + +#[derive(Clone, Serialize, libmcp::ToolProjection)] +#[libmcp(kind = "mutation")] +pub(crate) struct RunDimensionDefinitionOutput { + pub(crate) record: RunDimensionDefinitionProjection, +} + +#[derive(Clone, Serialize, libmcp::ToolProjection)] +#[libmcp(kind = "list")] +pub(crate) struct RunDimensionListOutput { + pub(crate) count: usize, + pub(crate) dimensions: Vec<RunDimensionDefinitionProjection>, +} + +#[derive(Clone, Serialize)] +pub(crate) struct HistoryEntryProjection { + pub(crate) revision: u64, + pub(crate) event_kind: String, + pub(crate) occurred_at: TimestampText, + pub(crate) snapshot: Value, +} + +#[derive(Clone, Serialize, libmcp::ToolProjection)] +#[libmcp(kind = "list")] +pub(crate) struct HistoryOutput { + pub(crate) count: usize, + pub(crate) history: Vec<HistoryEntryProjection>, +} + pub(crate) fn frontier_list(frontiers: &[FrontierSummary]) -> FrontierListOutput { FrontierListOutput { count: frontiers.len(), @@ -784,6 +863,50 @@ pub(crate) fn metric_best(entries: &[MetricBestEntry]) -> MetricBestOutput { } } +pub(crate) fn tag_record(tag: &TagRecord) -> TagRecordOutput { + TagRecordOutput { + record: tag_record_projection(tag), + } +} + +pub(crate) fn tag_list(tags: &[TagRecord]) -> TagListOutput { + TagListOutput { + count: tags.len(), + tags: tags.iter().map(tag_record_projection).collect(), + } +} + +pub(crate) fn metric_definition(metric: &MetricDefinition) -> MetricDefinitionOutput { + MetricDefinitionOutput { + record: metric_definition_projection(metric), + } +} + +pub(crate) fn run_dimension_definition( + dimension: &RunDimensionDefinition, +) -> RunDimensionDefinitionOutput { + RunDimensionDefinitionOutput { + record: run_dimension_definition_projection(dimension), + } +} + +pub(crate) fn run_dimension_list(dimensions: &[RunDimensionDefinition]) -> RunDimensionListOutput { + RunDimensionListOutput { + count: dimensions.len(), + dimensions: dimensions + .iter() + .map(run_dimension_definition_projection) + .collect(), + } +} + +pub(crate) fn history(history: &[EntityHistoryEntry]) -> HistoryOutput { + HistoryOutput { + count: history.len(), + history: history.iter().map(history_entry_projection).collect(), + } +} + fn frontier_summary(frontier: &FrontierSummary) -> FrontierSummaryProjection { FrontierSummaryProjection { slug: frontier.slug.to_string(), @@ -969,6 +1092,47 @@ fn metric_key_summary(metric: &MetricKeySummary) -> MetricKeySummaryProjection { } } +fn tag_record_projection(tag: &TagRecord) -> TagRecordProjection { + TagRecordProjection { + name: tag.name.to_string(), + description: tag.description.to_string(), + created_at: timestamp_value(tag.created_at), + } +} + +fn metric_definition_projection(metric: &MetricDefinition) -> MetricDefinitionProjection { + MetricDefinitionProjection { + key: metric.key.to_string(), + unit: metric.unit.as_str().to_owned(), + objective: metric.objective.as_str().to_owned(), + visibility: metric.visibility.as_str().to_owned(), + description: metric.description.as_ref().map(ToString::to_string), + created_at: timestamp_value(metric.created_at), + updated_at: timestamp_value(metric.updated_at), + } +} + +fn run_dimension_definition_projection( + dimension: &RunDimensionDefinition, +) -> RunDimensionDefinitionProjection { + RunDimensionDefinitionProjection { + key: dimension.key.to_string(), + value_type: dimension.value_type.as_str().to_owned(), + description: dimension.description.as_ref().map(ToString::to_string), + created_at: timestamp_value(dimension.created_at), + updated_at: timestamp_value(dimension.updated_at), + } +} + +fn history_entry_projection(entry: &EntityHistoryEntry) -> HistoryEntryProjection { + HistoryEntryProjection { + revision: entry.revision, + event_kind: entry.event_kind.to_string(), + occurred_at: timestamp_value(entry.occurred_at), + snapshot: entry.snapshot.clone(), + } +} + fn metric_best_entry(entry: &MetricBestEntry) -> MetricBestEntryProjection { MetricBestEntryProjection { experiment: experiment_summary(&entry.experiment), @@ -1127,10 +1291,8 @@ fn attachment_target( } } -fn timestamp_value(timestamp: time::OffsetDateTime) -> String { - timestamp - .format(&time::format_description::well_known::Rfc3339) - .unwrap_or_else(|_| timestamp.unix_timestamp().to_string()) +fn timestamp_value(timestamp: time::OffsetDateTime) -> TimestampText { + TimestampText::from(timestamp) } fn store_fault(operation: &str) -> impl Fn(StoreError) -> FaultRecord + '_ { diff --git a/crates/fidget-spinner-cli/src/mcp/service.rs b/crates/fidget-spinner-cli/src/mcp/service.rs index 3ce68ae..7c649aa 100644 --- a/crates/fidget-spinner-cli/src/mcp/service.rs +++ b/crates/fidget-spinner-cli/src/mcp/service.rs @@ -26,8 +26,8 @@ use serde_json::{Map, Value, json}; use crate::mcp::fault::{FaultKind, FaultRecord, FaultStage}; use crate::mcp::output::{ - ToolOutput, fallback_detailed_tool_output, fallback_tool_output, projected_tool_output, - split_presentation, tool_success, + ToolOutput, fallback_detailed_tool_output, projected_tool_output, split_presentation, + tool_success, }; use crate::mcp::projection; use crate::mcp::protocol::{TRANSIENT_ONCE_ENV, TRANSIENT_ONCE_MARKER_ENV, WorkerOperation}; @@ -103,13 +103,7 @@ impl WorkerService { TagName::new(args.name).map_err(store_fault(&operation))?, NonEmptyText::new(args.description).map_err(store_fault(&operation))?, )); - fallback_tool_output( - &tag, - &tag, - libmcp::SurfaceKind::Mutation, - FaultStage::Worker, - &operation, - )? + tag_record_output(&tag, &operation)? } "tag.list" => tag_list_output(&lift!(self.store.list_tags()), &operation)?, "frontier.create" => { @@ -471,13 +465,7 @@ impl WorkerService { .map_err(store_fault(&operation))?, }) ); - fallback_tool_output( - &metric, - &metric, - libmcp::SurfaceKind::Mutation, - FaultStage::Worker, - &operation, - )? + metric_definition_output(&metric, &operation)? } "metric.keys" => { let args = deserialize::<MetricKeysArgs>(arguments)?; @@ -517,23 +505,11 @@ impl WorkerService { .map_err(store_fault(&operation))?, }) ); - fallback_tool_output( - &dimension, - &dimension, - libmcp::SurfaceKind::Mutation, - FaultStage::Worker, - &operation, - )? + run_dimension_definition_output(&dimension, &operation)? } "run.dimension.list" => { let dimensions = lift!(self.store.list_run_dimensions()); - fallback_tool_output( - &dimensions, - &dimensions, - libmcp::SurfaceKind::List, - FaultStage::Worker, - &operation, - )? + run_dimension_list_output(&dimensions, &operation)? } other => { return Err(FaultRecord::new( @@ -1064,17 +1040,27 @@ fn project_status_output( ) } +fn tag_record_output( + tag: &fidget_spinner_core::TagRecord, + operation: &str, +) -> Result<ToolOutput, FaultRecord> { + let projection = projection::tag_record(tag); + projected_tool_output( + &projection, + format!("tag {} — {}", tag.name, tag.description), + None, + FaultStage::Worker, + operation, + ) +} + fn tag_list_output( tags: &[fidget_spinner_core::TagRecord], operation: &str, ) -> Result<ToolOutput, FaultRecord> { - let concise = json!({ - "count": tags.len(), - "tags": tags, - }); - fallback_detailed_tool_output( - &concise, - &concise, + let projection = projection::tag_list(tags); + projected_tool_output( + &projection, if tags.is_empty() { "no tags".to_owned() } else { @@ -1084,7 +1070,6 @@ fn tag_list_output( .join("\n") }, None, - libmcp::SurfaceKind::List, FaultStage::Worker, operation, ) @@ -1527,6 +1512,26 @@ fn metric_keys_output( ) } +fn metric_definition_output( + metric: &fidget_spinner_core::MetricDefinition, + operation: &str, +) -> Result<ToolOutput, FaultRecord> { + let projection = projection::metric_definition(metric); + projected_tool_output( + &projection, + format!( + "metric {} [{} {} {}]", + metric.key, + metric.unit.as_str(), + metric.objective.as_str(), + metric.visibility.as_str() + ), + None, + FaultStage::Worker, + operation, + ) +} + fn metric_best_output( entries: &[MetricBestEntry], operation: &str, @@ -1562,14 +1567,63 @@ fn metric_best_output( ) } +fn run_dimension_definition_output( + dimension: &fidget_spinner_core::RunDimensionDefinition, + operation: &str, +) -> Result<ToolOutput, FaultRecord> { + let projection = projection::run_dimension_definition(dimension); + projected_tool_output( + &projection, + format!( + "dimension {} [{}]", + dimension.key, + dimension.value_type.as_str() + ), + None, + FaultStage::Worker, + operation, + ) +} + +fn run_dimension_list_output( + dimensions: &[fidget_spinner_core::RunDimensionDefinition], + operation: &str, +) -> Result<ToolOutput, FaultRecord> { + let projection = projection::run_dimension_list(dimensions); + projected_tool_output( + &projection, + if dimensions.is_empty() { + "no run dimensions".to_owned() + } else { + dimensions + .iter() + .map(|dimension| { + format!( + "{} [{}]{}", + dimension.key, + dimension.value_type.as_str(), + dimension + .description + .as_ref() + .map_or_else(String::new, |description| format!(" — {description}")) + ) + }) + .collect::<Vec<_>>() + .join("\n") + }, + None, + FaultStage::Worker, + operation, + ) +} + fn history_output( history: &[EntityHistoryEntry], operation: &str, ) -> Result<ToolOutput, FaultRecord> { - let concise = json!({ "count": history.len(), "history": history }); - fallback_detailed_tool_output( - &concise, - &concise, + let projection = projection::history(history); + projected_tool_output( + &projection, if history.is_empty() { "no history".to_owned() } else { @@ -1585,7 +1639,6 @@ fn history_output( .join("\n") }, None, - libmcp::SurfaceKind::List, FaultStage::Worker, operation, ) |