pub mod admin;
pub mod coverage;
pub mod params;
pub mod protocol;
pub mod raw_turn;
pub mod session;
pub mod skill;
pub mod span;
pub mod trace;
use serde::{Deserialize, Deserializer};
pub use admin::{
DeriveRunResponse, ReconcileStats, RederiveReport, SeedDemoRequest, SeedResult, StatsResponse,
};
pub use params::{
ExportDetail, ExportSessionParams, ExportSessionsParams, PayloadDetail, SearchSpansParams,
SessionListParams, SessionTracesParams, SkillScope, SkillSort, SkillsListParams, SortDirection,
StatsParams, TraceListParams, TraceParams,
};
pub use protocol::{ErrorResponse, McpError, McpRequest, McpResponse};
pub use raw_turn::{
RawTurnAttribution, RawTurnAttributionRepairRequest, RawTurnAttributionRepairResult,
RawTurnHeaderItem, RawTurnListResponse, RepairPendingSession,
};
pub use session::{
ModelUsage, SessionDetailResponse, SessionItem, SessionListResponse, SessionRollup,
SessionTracesResponse, SessionUpdateRequest, SessionUsage, TreeTask,
};
pub use skill::{
CreateSkillRequest, GenerateSkillRequest, GenerateSkillRequestHint, PublishSkillRequest,
SessionSkillsResponse, SkillCounts, SkillResponse, SkillVersionResponse, SkillVersionsResponse,
SkillsListResponse, UpdateSkillRequest,
};
pub use span::{SpanItem, SpanLinkItem, SpanSearchOutput, SpanSearchResult};
pub use trace::{MainUsage, TraceDetail, TraceItem, TraceListResponse, TraceUsage};
pub trait ContractModel: serde::Serialize + serde::de::DeserializeOwned {
const SCHEMA: &'static str;
}
pub fn null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use serde_json::json;
#[test]
fn an_absent_field_decodes_to_its_default_rather_than_failing() {
let session: SessionItem = serde_json::from_value(json!({})).unwrap();
assert_eq!(session.id, "");
assert_eq!(session.rollup.turn_count, 0);
}
#[test]
fn an_unknown_field_passes_rather_than_failing_the_document() {
let session: SessionItem =
serde_json::from_value(json!({"id": "s-1", "a_field_from_the_future": 7})).unwrap();
assert_eq!(session.id, "s-1");
}
#[test]
fn a_null_composite_decodes_to_empty_rather_than_blanking_the_response() {
let session: SessionItem = serde_json::from_value(json!({
"id": "s-1",
"harness_metadata": null,
"rollup": null,
}))
.unwrap();
assert_eq!(session.id, "s-1");
assert!(session.harness_metadata.is_empty());
assert_eq!(session.rollup, SessionRollup::default());
}
#[test]
fn a_one_field_update_sends_exactly_that_field() {
let rename = UpdateSkillRequest {
name: Some("gum glow charm".to_owned()),
..Default::default()
};
let sent = serde_json::to_value(&rename).unwrap();
assert_eq!(sent, json!({"name": "gum glow charm"}));
}
#[test]
fn an_empty_partial_update_sends_an_empty_document() {
assert_eq!(
serde_json::to_value(UpdateSkillRequest::default()).unwrap(),
json!({}),
);
assert_eq!(
serde_json::to_value(SessionUpdateRequest::default()).unwrap(),
json!({}),
);
}
#[test]
fn a_rename_body_tells_clearing_apart_from_not_touching() {
let clear = SessionUpdateRequest {
display_name: Some(String::new()),
};
let untouched = SessionUpdateRequest::default();
assert_eq!(
serde_json::to_value(&clear).unwrap(),
json!({"display_name": ""}),
);
assert_eq!(serde_json::to_value(&untouched).unwrap(), json!({}));
}
#[test]
fn a_present_but_empty_update_field_still_reaches_the_wire() {
let untag = UpdateSkillRequest {
tags: Some(Vec::new()),
..Default::default()
};
assert_eq!(serde_json::to_value(&untag).unwrap(), json!({"tags": []}));
}
#[test]
fn a_notification_frame_carries_no_id() {
let notification = McpRequest {
id: None,
jsonrpc: "2.0".to_owned(),
method: "notifications/initialized".to_owned(),
params: BTreeMap::new(),
};
let sent = serde_json::to_value(¬ification).unwrap();
assert_eq!(sent.get("id"), None, "got: {sent}");
assert_eq!(sent["method"], "notifications/initialized");
}
#[test]
fn a_nullable_object_keeps_the_distinction_the_contract_draws() {
let judged: SpanItem =
serde_json::from_value(json!({"verdict": {"decision": "allow"}})).unwrap();
let unjudged: SpanItem = serde_json::from_value(json!({"verdict": null})).unwrap();
assert!(judged.verdict.is_some());
assert!(unjudged.verdict.is_none());
}
}