a3s_code_core/capability/
sdk_batch.rs1use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12use crate::skills::{Skill, SkillKind};
13
14use super::{
15 CapabilityContribution, CapabilityDescriptor, CapabilityKind, CapabilityRuntimeError,
16 CapabilitySet, CapabilitySetError, CapabilitySource, CapabilityValue, CodeCatalogGeneration,
17 SessionCapabilityBatch, Sha256Digest,
18};
19
20pub const SDK_CAPABILITY_BATCH_SCHEMA: &str = "a3s.code.sdk_capability_batch.v1";
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct SdkCapabilityBatchV1 {
25 pub schema_version: u32,
26 pub generation: u64,
27 pub source_id: String,
28 #[serde(default)]
29 pub skills: Vec<SdkSkillCapabilityV1>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct SdkSkillCapabilityV1 {
35 pub local_id: String,
36 pub name: String,
37 #[serde(default)]
38 pub kind: String,
39 pub content: String,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct SdkCapabilityCommitReceiptV1 {
45 pub previous_generation: u64,
46 pub committed_generation: u64,
47 pub previous_digest: String,
48 pub committed_digest: String,
49}
50
51impl SdkCapabilityCommitReceiptV1 {
52 pub fn from_receipt(receipt: &super::CapabilityCommitReceipt) -> Self {
53 Self {
54 previous_generation: receipt.previous().generation().get(),
55 committed_generation: receipt.committed().generation().get(),
56 previous_digest: receipt.previous().digest().as_str().to_string(),
57 committed_digest: receipt.committed().digest().as_str().to_string(),
58 }
59 }
60}
61
62#[derive(Debug, thiserror::Error)]
63pub enum SdkCapabilityBatchError {
64 #[error("unsupported SDK capability batch schema version {found}; expected 1")]
65 UnsupportedSchema { found: u32 },
66 #[error("SDK capability batch must stage at least one Skill")]
67 EmptyBatch,
68 #[error("SDK skill name must not be empty")]
69 EmptySkillName,
70 #[error("unknown skill kind '{kind}'; use instruction, persona, or tool")]
71 UnknownSkillKind { kind: String },
72 #[error(transparent)]
73 CapabilitySet(#[from] CapabilitySetError),
74 #[error(transparent)]
75 Runtime(#[from] CapabilityRuntimeError),
76}
77
78impl SdkCapabilityBatchV1 {
79 pub fn into_session_batch(self) -> Result<SessionCapabilityBatch, SdkCapabilityBatchError> {
80 if self.schema_version != 1 {
81 return Err(SdkCapabilityBatchError::UnsupportedSchema {
82 found: self.schema_version,
83 });
84 }
85 if self.skills.is_empty() {
86 return Err(SdkCapabilityBatchError::EmptyBatch);
87 }
88
89 let source_revision = digest_bytes(self.source_id.as_bytes())?;
90 let source = CapabilitySource::host(self.source_id.trim(), source_revision)?;
91 let mut descriptors = Vec::with_capacity(self.skills.len());
92 let mut staged = Vec::with_capacity(self.skills.len());
93
94 for skill_spec in self.skills {
95 let skill = skill_from_sdk(skill_spec)?;
96 let local_id = skill.name.clone();
97 let public_name = skill.name.clone();
98 let surface = digest_bytes(
99 format!("{}:{}:{}", skill.name, skill.kind_as_str(), skill.content).as_bytes(),
100 )?;
101 let descriptor = CapabilityDescriptor::new(
102 &source,
103 CapabilityKind::Skill,
104 local_id,
105 public_name,
106 surface,
107 [],
108 )?;
109 let id = descriptor.id().clone();
110 descriptors.push(descriptor);
111 staged.push((id, CapabilityValue::Skill(skill)));
112 }
113
114 let set = CapabilitySet::from_contributions(
115 CodeCatalogGeneration::new(self.generation),
116 [CapabilityContribution::new(source, descriptors)?],
117 )?;
118 let mut batch = SessionCapabilityBatch::new(set)?;
119 for (id, value) in staged {
120 batch.stage_value(id, value)?;
121 }
122 Ok(batch)
123 }
124}
125
126fn skill_from_sdk(spec: SdkSkillCapabilityV1) -> Result<Arc<Skill>, SdkCapabilityBatchError> {
127 let name = if spec.name.trim().is_empty() {
128 spec.local_id.trim().to_string()
129 } else {
130 spec.name.trim().to_string()
131 };
132 if name.is_empty() {
133 return Err(SdkCapabilityBatchError::EmptySkillName);
134 }
135 let kind = match spec.kind.trim() {
136 "" | "instruction" => SkillKind::Instruction,
137 "persona" => SkillKind::Persona,
138 "tool" => SkillKind::Tool,
139 other => {
140 return Err(SdkCapabilityBatchError::UnknownSkillKind {
141 kind: other.to_string(),
142 })
143 }
144 };
145 Ok(Arc::new(Skill {
146 name,
147 description: String::new(),
148 allowed_tools: None,
149 disable_model_invocation: false,
150 kind,
151 content: spec.content,
152 tags: Vec::new(),
153 version: None,
154 }))
155}
156
157trait SkillKindLabel {
158 fn kind_as_str(&self) -> &'static str;
159}
160
161impl SkillKindLabel for Skill {
162 fn kind_as_str(&self) -> &'static str {
163 match self.kind {
164 SkillKind::Instruction => "instruction",
165 SkillKind::Persona => "persona",
166 SkillKind::Tool => "tool",
167 }
168 }
169}
170
171fn digest_bytes(bytes: &[u8]) -> Result<Sha256Digest, CapabilitySetError> {
172 Sha256Digest::new(format!("sha256:{:x}", Sha256::digest(bytes)))
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn sdk_skill_batch_builds_session_capability_batch() {
181 let batch = SdkCapabilityBatchV1 {
182 schema_version: 1,
183 generation: 3,
184 source_id: "sdk-host".to_string(),
185 skills: vec![SdkSkillCapabilityV1 {
186 local_id: "type-hints".to_string(),
187 name: "type-hints".to_string(),
188 kind: "instruction".to_string(),
189 content: "Prefer explicit types.".to_string(),
190 }],
191 }
192 .into_session_batch()
193 .expect("skill batch must build");
194 assert_eq!(batch.len(), 1);
195 assert_eq!(batch.target().generation().get(), 3);
196 }
197
198 #[test]
199 fn sdk_batch_rejects_empty_and_bad_schema() {
200 let empty = SdkCapabilityBatchV1 {
201 schema_version: 1,
202 generation: 1,
203 source_id: "sdk-host".to_string(),
204 skills: Vec::new(),
205 };
206 assert!(matches!(
207 empty.into_session_batch(),
208 Err(SdkCapabilityBatchError::EmptyBatch)
209 ));
210 let bad = SdkCapabilityBatchV1 {
211 schema_version: 9,
212 generation: 1,
213 source_id: "sdk-host".to_string(),
214 skills: vec![SdkSkillCapabilityV1 {
215 local_id: "x".to_string(),
216 name: "x".to_string(),
217 kind: "instruction".to_string(),
218 content: "x".to_string(),
219 }],
220 };
221 assert!(matches!(
222 bad.into_session_batch(),
223 Err(SdkCapabilityBatchError::UnsupportedSchema { found: 9 })
224 ));
225 }
226}