Skip to main content

kindling_types/
capability.rs

1//! Capability handshake and machine-readable kind registry.
2//!
3//! Pure, side-effect-free assembly used by `/v1/health`, `kindling status --json`,
4//! and the daemon client. The kind registry is derived from [`ObservationKind::ALL`]
5//! so it cannot drift from the canonical enum definition.
6
7use serde::{Deserialize, Serialize};
8
9#[cfg(feature = "ts-rs")]
10use ts_rs::TS;
11
12use crate::observation::ObservationKind;
13
14/// Observation fields every kind must carry on the wire (camelCase keys).
15pub const OBSERVATION_REQUIRED_FIELDS: &[&str] = &["content", "provenance", "scopeIds", "ts"];
16
17/// One entry in the machine-readable kind registry.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "ts-rs", derive(TS), ts(export, export_to = "../bindings/"))]
20#[serde(rename_all = "camelCase")]
21pub struct KindRegistryEntry {
22    /// Snake-case kind name (matches the `ObservationKind` wire encoding).
23    pub kind: String,
24    /// Required observation + documented provenance fields for this kind.
25    pub required_fields: Vec<String>,
26}
27
28/// Capability block surfaced by health and `kindling status --json`.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[cfg_attr(feature = "ts-rs", derive(TS), ts(export, export_to = "../bindings/"))]
31#[serde(rename_all = "camelCase")]
32pub struct Capability {
33    pub version: String,
34    pub schema_version: u32,
35    pub supported_kinds: Vec<String>,
36    pub storage_path: String,
37    pub kind_registry: Vec<KindRegistryEntry>,
38}
39
40impl ObservationKind {
41    /// Wire name (snake_case) for this kind.
42    pub fn wire_name(self) -> &'static str {
43        match self {
44            ObservationKind::ToolCall => "tool_call",
45            ObservationKind::Command => "command",
46            ObservationKind::FileDiff => "file_diff",
47            ObservationKind::Error => "error",
48            ObservationKind::Message => "message",
49            ObservationKind::NodeStart => "node_start",
50            ObservationKind::NodeEnd => "node_end",
51            ObservationKind::NodeOutput => "node_output",
52            ObservationKind::NodeError => "node_error",
53        }
54    }
55
56    /// Documented provenance keys adapters emit for this kind (camelCase).
57    pub fn documented_provenance_fields(self) -> &'static [&'static str] {
58        match self {
59            ObservationKind::ToolCall => &["toolName", "hasError"],
60            ObservationKind::Command => &["toolName", "hasError", "command"],
61            ObservationKind::FileDiff => &["toolName", "hasError", "filePath"],
62            ObservationKind::Error => &["source"],
63            ObservationKind::Message => &["role", "length"],
64            ObservationKind::NodeStart => &["nodeName"],
65            ObservationKind::NodeEnd => &["nodeName", "duration", "status"],
66            ObservationKind::NodeOutput => &["nodeName", "outputType", "duration"],
67            ObservationKind::NodeError => &["nodeName", "errorType", "errorMessage"],
68        }
69    }
70
71    /// Full required-field list: base observation fields plus provenance keys.
72    pub fn required_fields(self) -> Vec<String> {
73        let mut fields: Vec<String> = OBSERVATION_REQUIRED_FIELDS
74            .iter()
75            .map(|s| (*s).to_string())
76            .collect();
77        fields.extend(
78            self.documented_provenance_fields()
79                .iter()
80                .map(|s| (*s).to_string()),
81        );
82        fields
83    }
84}
85
86/// Snake-case kind names for every [`ObservationKind`] variant, in declaration order.
87pub fn supported_kind_names() -> Vec<String> {
88    ObservationKind::ALL
89        .iter()
90        .map(|k| k.wire_name().to_string())
91        .collect()
92}
93
94/// Machine-readable registry listing every kind with its required fields.
95pub fn kind_registry() -> Vec<KindRegistryEntry> {
96    ObservationKind::ALL
97        .iter()
98        .map(|&kind| KindRegistryEntry {
99            kind: kind.wire_name().to_string(),
100            required_fields: kind.required_fields(),
101        })
102        .collect()
103}
104
105/// Assemble the capability block from runtime inputs (version, schema, storage path).
106pub fn build_capability(
107    version: impl Into<String>,
108    schema_version: u32,
109    storage_path: impl Into<String>,
110) -> Capability {
111    Capability {
112        version: version.into(),
113        schema_version,
114        supported_kinds: supported_kind_names(),
115        storage_path: storage_path.into(),
116        kind_registry: kind_registry(),
117    }
118}