meerkat-contracts 0.5.2

Wire format contracts and capability registry for Meerkat
Documentation
use serde::Serialize;

#[derive(Debug, Clone, Copy)]
pub struct RpcMethodCatalogOptions {
    pub runtime_available: bool,
    pub mob_enabled: bool,
    pub mcp_enabled: bool,
    pub comms_enabled: bool,
}

impl RpcMethodCatalogOptions {
    pub const fn documented_surface() -> Self {
        Self {
            runtime_available: true,
            mob_enabled: true,
            mcp_enabled: true,
            comms_enabled: true,
        }
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RpcMethodDescriptor {
    pub name: &'static str,
    pub description: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params_type: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result_type: Option<&'static str>,
}

impl RpcMethodDescriptor {
    const fn basic(name: &'static str, description: &'static str) -> Self {
        Self {
            name,
            description,
            params_type: None,
            result_type: None,
        }
    }

    const fn typed(
        name: &'static str,
        description: &'static str,
        params_type: &'static str,
        result_type: &'static str,
    ) -> Self {
        Self {
            name,
            description,
            params_type: Some(params_type),
            result_type: Some(result_type),
        }
    }

    const fn result_only(
        name: &'static str,
        description: &'static str,
        result_type: &'static str,
    ) -> Self {
        Self {
            name,
            description,
            params_type: None,
            result_type: Some(result_type),
        }
    }
}

pub fn rpc_method_catalog(options: RpcMethodCatalogOptions) -> Vec<RpcMethodDescriptor> {
    let mut methods = vec![
        RpcMethodDescriptor::basic("initialize", "Handshake, returns server capabilities"),
        RpcMethodDescriptor::basic("session/create", "Create session + run first turn"),
        RpcMethodDescriptor::basic("session/list", "List active sessions"),
        RpcMethodDescriptor::basic("session/read", "Get session state"),
        RpcMethodDescriptor::basic("session/history", "Get full session history"),
        RpcMethodDescriptor::typed(
            "blob/get",
            "Fetch raw blob payload metadata and bytes by blob id",
            "BlobGetParams",
            "BlobPayload",
        ),
        RpcMethodDescriptor::basic("session/archive", "Remove session"),
        RpcMethodDescriptor::basic(
            "session/external_event",
            "Queue a runtime-backed external event",
        ),
        RpcMethodDescriptor::basic(
            "session/inject_context",
            "Stage runtime system context for application at the next LLM boundary",
        ),
        RpcMethodDescriptor::basic("session/stream_open", "Open a session event stream"),
        RpcMethodDescriptor::basic("session/stream_close", "Close a session event stream"),
        RpcMethodDescriptor::typed(
            "schedule/create",
            "Create a schedule",
            "CreateScheduleRequest",
            "Schedule",
        ),
        RpcMethodDescriptor::typed(
            "schedule/get",
            "Get one schedule",
            "ScheduleIdParams",
            "Schedule",
        ),
        RpcMethodDescriptor::typed(
            "schedule/list",
            "List schedules",
            "ListSchedulesParams",
            "ScheduleListResult",
        ),
        RpcMethodDescriptor::typed(
            "schedule/update",
            "Update a schedule",
            "UpdateScheduleParams",
            "Schedule",
        ),
        RpcMethodDescriptor::typed(
            "schedule/pause",
            "Pause a schedule",
            "ScheduleIdParams",
            "Schedule",
        ),
        RpcMethodDescriptor::typed(
            "schedule/resume",
            "Resume a schedule",
            "ScheduleIdParams",
            "Schedule",
        ),
        RpcMethodDescriptor::typed(
            "schedule/delete",
            "Delete a schedule",
            "ScheduleIdParams",
            "Schedule",
        ),
        RpcMethodDescriptor::typed(
            "schedule/occurrences",
            "List schedule occurrences",
            "ScheduleOccurrencesParams",
            "ScheduleOccurrencesResult",
        ),
        RpcMethodDescriptor::result_only(
            "schedule/tools",
            "List schedule transport tools",
            "ScheduleToolsResult",
        ),
        RpcMethodDescriptor::typed(
            "schedule/call",
            "Call a schedule transport tool",
            "ScheduleToolCallParams",
            "Value",
        ),
        RpcMethodDescriptor::basic("turn/start", "Start a new turn on existing session"),
        RpcMethodDescriptor::basic("turn/interrupt", "Cancel in-flight turn"),
        RpcMethodDescriptor::basic("config/get", "Read config"),
        RpcMethodDescriptor::basic("config/set", "Replace config"),
        RpcMethodDescriptor::basic("config/patch", "Merge-patch config"),
        RpcMethodDescriptor::basic("capabilities/get", "Get runtime capabilities"),
        RpcMethodDescriptor::result_only(
            "models/catalog",
            "Get the effective model catalog (built-in plus config-backed entries)",
            "ModelsCatalogResponse",
        ),
        RpcMethodDescriptor::basic("skills/list", "List available skills"),
        RpcMethodDescriptor::basic("skills/inspect", "Inspect one skill"),
    ];

    if options.runtime_available {
        methods.extend([
            RpcMethodDescriptor::typed(
                "runtime/state",
                "Get a session runtime's current state",
                "RuntimeStateParams",
                "RuntimeStateResult",
            ),
            RpcMethodDescriptor::typed(
                "runtime/accept",
                "Accept a runtime input for a session",
                "RuntimeAcceptParams",
                "RuntimeAcceptResult",
            ),
            RpcMethodDescriptor::typed(
                "runtime/retire",
                "Retire a session runtime",
                "RuntimeRetireParams",
                "RuntimeRetireResult",
            ),
            RpcMethodDescriptor::typed(
                "runtime/reset",
                "Reset a session runtime",
                "RuntimeResetParams",
                "RuntimeResetResult",
            ),
            RpcMethodDescriptor::typed(
                "input/state",
                "Get the state of a specific runtime input",
                "InputStateParams",
                "InputStateResult",
            ),
            RpcMethodDescriptor::typed(
                "input/list",
                "List active runtime inputs for a session",
                "InputListParams",
                "InputListResult",
            ),
        ]);
    }

    if options.mob_enabled {
        methods.extend([
            RpcMethodDescriptor::typed(
                "mob/create",
                "Create a mob from a definition",
                "MobCreateParams",
                "MobCreateResult",
            ),
            RpcMethodDescriptor::basic("mob/list", "List active mobs"),
            RpcMethodDescriptor::basic("mob/status", "Get mob lifecycle status"),
            RpcMethodDescriptor::basic("mob/lifecycle", "Apply a mob lifecycle action"),
            RpcMethodDescriptor::basic("mob/spawn", "Spawn a new mob member"),
            RpcMethodDescriptor::basic("mob/spawn_many", "Spawn multiple new mob members"),
            RpcMethodDescriptor::basic("mob/retire", "Retire a mob member"),
            RpcMethodDescriptor::basic("mob/respawn", "Respawn a mob member with topology restore"),
            RpcMethodDescriptor::typed(
                "mob/wire",
                "Wire a local mob member to a local or external peer",
                "MobWireParams",
                "MobWireResult",
            ),
            RpcMethodDescriptor::typed(
                "mob/unwire",
                "Unwire a local mob member from a local or external peer",
                "MobUnwireParams",
                "MobUnwireResult",
            ),
            RpcMethodDescriptor::basic("mob/members", "List members in a mob roster"),
            RpcMethodDescriptor::basic("mob/events", "Read mob event history"),
            RpcMethodDescriptor::typed(
                "mob/member_send",
                "Deliver ordinary content to a specific mob member via the host control plane",
                "MobMemberSendParams",
                "MobMemberSendResult",
            ),
            RpcMethodDescriptor::basic(
                "mob/append_system_context",
                "Append system context for a mob member",
            ),
            RpcMethodDescriptor::basic("mob/flows", "List flows defined for a mob"),
            RpcMethodDescriptor::basic("mob/flow_run", "Start a mob flow run"),
            RpcMethodDescriptor::basic("mob/flow_status", "Get status for a mob flow run"),
            RpcMethodDescriptor::basic("mob/flow_cancel", "Cancel a mob flow run"),
            RpcMethodDescriptor::basic(
                "mob/spawn_helper",
                "Spawn a helper member and wait for completion",
            ),
            RpcMethodDescriptor::basic(
                "mob/fork_helper",
                "Fork a helper member and wait for completion",
            ),
            RpcMethodDescriptor::basic("mob/force_cancel", "Force-cancel a mob member"),
            RpcMethodDescriptor::basic("mob/member_status", "Get live status for a mob member"),
            RpcMethodDescriptor::basic(
                "mob/wait_kickoff",
                "Wait for kickoff completion for a member",
            ),
            RpcMethodDescriptor::basic("mob/profile/create", "Create a realm-scoped mob profile"),
            RpcMethodDescriptor::basic("mob/profile/get", "Read a realm-scoped mob profile"),
            RpcMethodDescriptor::basic("mob/profile/list", "List realm-scoped mob profiles"),
            RpcMethodDescriptor::basic("mob/profile/update", "Update a realm-scoped mob profile"),
            RpcMethodDescriptor::basic("mob/profile/delete", "Delete a realm-scoped mob profile"),
            RpcMethodDescriptor::basic("mob/stream_open", "Open a mob event stream"),
            RpcMethodDescriptor::basic("mob/stream_close", "Close a mob event stream"),
        ]);
    }

    if options.mcp_enabled {
        methods.extend([
            RpcMethodDescriptor::typed(
                "mcp/add",
                "Stage live MCP server add for a session",
                "McpAddParams",
                "McpLiveOpResponse",
            ),
            RpcMethodDescriptor::typed(
                "mcp/remove",
                "Stage live MCP server remove for a session",
                "McpRemoveParams",
                "McpLiveOpResponse",
            ),
            RpcMethodDescriptor::typed(
                "mcp/reload",
                "Optional skeleton for live MCP reload",
                "McpReloadParams",
                "McpLiveOpResponse",
            ),
        ]);
    }

    if options.comms_enabled {
        methods.extend([
            RpcMethodDescriptor::basic("comms/send", "Send a comms command from a session"),
            RpcMethodDescriptor::basic("comms/peers", "List peers visible to a session"),
        ]);
    }

    methods
}

pub fn rpc_method_names(options: RpcMethodCatalogOptions) -> Vec<String> {
    rpc_method_catalog(options)
        .into_iter()
        .map(|method| method.name.to_string())
        .collect()
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RpcNotificationDescriptor {
    pub name: &'static str,
    pub description: &'static str,
}

impl RpcNotificationDescriptor {
    const fn basic(name: &'static str, description: &'static str) -> Self {
        Self { name, description }
    }
}

pub fn rpc_notification_catalog(
    options: RpcMethodCatalogOptions,
) -> Vec<RpcNotificationDescriptor> {
    let mut notifications = vec![
        RpcNotificationDescriptor::basic(
            "initialized",
            "Client notification acknowledged silently (no response)",
        ),
        RpcNotificationDescriptor::basic(
            "cancel",
            "Cancel an in-flight JSON-RPC request by request id",
        ),
        RpcNotificationDescriptor::basic("session/event", "AgentEvent payload during turns"),
        RpcNotificationDescriptor::basic(
            "session/stream_event",
            "Standalone session stream event notification",
        ),
        RpcNotificationDescriptor::basic(
            "session/stream_end",
            "Standalone session stream terminal notification",
        ),
    ];

    if options.mob_enabled {
        notifications.extend([
            RpcNotificationDescriptor::basic("mob/stream_event", "Mob stream event notification"),
            RpcNotificationDescriptor::basic("mob/stream_end", "Mob stream terminal notification"),
        ]);
    }

    notifications
}

pub fn rpc_notification_names(options: RpcMethodCatalogOptions) -> Vec<String> {
    rpc_notification_catalog(options)
        .into_iter()
        .map(|notification| notification.name.to_string())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn documented_surface_keeps_live_runtime_and_mob_methods() {
        let methods = rpc_method_names(RpcMethodCatalogOptions::documented_surface());
        assert!(methods.iter().any(|m| m == "session/inject_context"));
        assert!(methods.iter().any(|m| m == "mob/events"));
        assert!(methods.iter().any(|m| m == "mob/member_send"));
        assert!(methods.iter().any(|m| m == "mob/wait_kickoff"));
        assert!(methods.iter().any(|m| m == "mob/profile/create"));
        assert!(methods.iter().any(|m| m == "schedule/list"));
        assert!(methods.iter().any(|m| m == "schedule/occurrences"));
        assert!(methods.iter().any(|m| m == "schedule/call"));
    }

    #[test]
    fn documented_surface_keeps_live_stream_notifications() {
        let notifications = rpc_notification_names(RpcMethodCatalogOptions::documented_surface());
        assert!(notifications.iter().any(|n| n == "cancel"));
        assert!(notifications.iter().any(|n| n == "session/stream_event"));
        assert!(notifications.iter().any(|n| n == "session/stream_end"));
        assert!(notifications.iter().any(|n| n == "mob/stream_event"));
        assert!(notifications.iter().any(|n| n == "mob/stream_end"));
    }
}