Skip to main content

ai_memory/models/
skill.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4//! Data models for the Agent Skills ingestion substrate (Pillar 1.5).
5//!
6//! [`SkillManifest`] is the parsed, validated in-memory representation
7//! of a `SKILL.md` file. [`SkillRow`] mirrors the `skills` table row
8//! returned by read-side queries.
9
10use serde::{Deserialize, Serialize};
11
12/// v0.7.0 L2-7 (issue #672) — a single entry in the SKILL.md
13/// `composes_with_reflections` frontmatter list.
14///
15/// Skills declare which reflection namespaces the substrate should
16/// surface when the `memory_skill_compositional_context` MCP tool is
17/// invoked. Each entry pins a `namespace` and a `min_depth` floor: the
18/// substrate filters out reflections shallower than the floor before
19/// applying the per-namespace `max_reflection_depth` ceiling
20/// (`GovernancePolicy::effective_max_reflection_depth`). The ceiling is
21/// authoritative — composition cannot bypass the bounded-recursion
22/// guarantee documented on `GovernancePolicy::max_reflection_depth`.
23///
24/// The struct is round-trip-stable through JSON: registration parses it
25/// out of the YAML frontmatter, embeds it under
26/// `metadata.composes_with_reflections` (so older clients that don't
27/// know the field see it as opaque metadata per the v0.7.0 backward-
28/// compat guarantee), and `compositional_context` reads it back.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct ComposesWithReflectionEntry {
31    /// The reflection-bearing namespace (e.g. `"foo/observations"`).
32    pub namespace: String,
33    /// Minimum `reflection_depth` (inclusive) a memory must carry to be
34    /// surfaced for this entry. `0` admits caller-minted observations
35    /// (rare for a reflection-composition flow but legal); typical use
36    /// is `1+` to require at least one reflection pass.
37    pub min_depth: u32,
38}
39
40/// Parsed, validated SKILL.md manifest.
41///
42/// Produced by [`crate::parsing::skill_md::parse`] and consumed by the
43/// `memory_skill_register` handler to insert into the `skills` table.
44#[derive(Debug, Clone, PartialEq)]
45pub struct SkillManifest {
46    /// `namespace` field from the YAML frontmatter.
47    pub namespace: String,
48    /// `name` field — validated against `^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`,
49    /// length 1-64.
50    pub name: String,
51    /// `description` — 1-1024 chars.
52    pub description: String,
53    /// `license` — SPDX expression or free-form text.  Optional.
54    pub license: Option<String>,
55    /// `compatibility` — 1-500 chars when present.  Optional.
56    pub compatibility: Option<String>,
57    /// `allowed_tools` — list of MCP tool names.
58    pub allowed_tools: Vec<String>,
59    /// v0.7.0 L2-7 (issue #672) — declared composition with reflection
60    /// namespaces. Empty vector when the frontmatter omits the field
61    /// (the common case for non-composing skills). The field is also
62    /// duplicated into the JSON `metadata` payload for opaque-metadata
63    /// readability by older clients.
64    pub composes_with_reflections: Vec<ComposesWithReflectionEntry>,
65    /// Extra YAML keys not explicitly mapped above, serialised to JSON.
66    /// L2-7: `composes_with_reflections` is re-injected here too so
67    /// pre-L2-7 readers that only consult `metadata` still observe the
68    /// declaration as opaque-but-present data.
69    pub metadata: serde_json::Value,
70    /// Markdown body after the closing `---` fence.
71    pub body: String,
72}
73
74/// A row returned from the `skills` table.
75///
76/// Used by `memory_skill_list` (discovery payload, no `body_blob`) and
77/// `memory_skill_get` (full activation payload including decompressed
78/// body).
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SkillRow {
81    pub id: String,
82    pub namespace: String,
83    pub name: String,
84    pub description: String,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub license: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub compatibility: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub allowed_tools: Option<String>,
91    pub metadata: String,
92    /// Hex-encoded SHA-256 digest (populated by read helpers).
93    pub digest_hex: String,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub signing_agent: Option<String>,
96    pub created_at: i64,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub superseded_by: Option<String>,
99}
100
101/// A row from the `skill_resources` table.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SkillResourceRow {
104    pub skill_id: String,
105    pub resource_path: String,
106    pub resource_kind: String,
107    /// Hex-encoded SHA-256 digest over the decompressed content.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub digest_hex: Option<String>,
110}