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}