1use serde::{Deserialize, Serialize};
2
3use crate::digest_compat::Digest;
4use crate::ids_compat::AgentId;
5use crate::policy::{CodecId, CompressionPolicy};
6use crate::shape::KvTensorShape;
7
8pub const POOL_MANIFEST_SCHEMA: &str = "pool_manifest_v1";
10pub const SHELL_MANIFEST_SCHEMA: &str = "shell_manifest_v1";
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct PoolManifest {
16 pub schema_version: String,
18 pub pool_id: Digest,
20 pub shape: KvTensorShape,
22 pub policy: CompressionPolicy,
24 pub num_shared_tokens: u32,
26 pub num_layers: u32,
28 pub pool_size_bytes: u64,
30 pub shared_codec: CodecId,
32 pub compression_ratio: f64,
34 pub built_at_unix: i64,
36 pub build_seed: u64,
38 #[cfg(feature = "typed-ids")]
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub scope: Option<stack_ids::ScopeKey>,
42}
43
44impl PoolManifest {
45 #[allow(clippy::too_many_arguments)]
47 pub fn new(
48 pool_id: Digest,
49 shape: KvTensorShape,
50 policy: CompressionPolicy,
51 num_shared_tokens: u32,
52 num_layers: u32,
53 pool_size_bytes: u64,
54 raw_size_bytes: u64,
55 build_seed: u64,
56 built_at_unix: i64,
57 ) -> crate::error::Result<Self> {
58 let compression_ratio = if pool_size_bytes > 0 {
59 raw_size_bytes as f64 / pool_size_bytes as f64
60 } else {
61 0.0
62 };
63
64 let manifest = Self {
65 schema_version: POOL_MANIFEST_SCHEMA.into(),
66 pool_id,
67 shape,
68 policy,
69 num_shared_tokens,
70 num_layers,
71 pool_size_bytes,
72 shared_codec: CODEC_IDENTIFIER_SHARED.into(),
73 compression_ratio,
74 built_at_unix,
75 build_seed,
76 #[cfg(feature = "typed-ids")]
77 scope: None,
78 };
79 manifest.validate()?;
80 Ok(manifest)
81 }
82
83 pub fn validate(&self) -> crate::error::Result<()> {
85 if self.schema_version != POOL_MANIFEST_SCHEMA {
86 return Err(crate::error::PolyKvError::InvalidManifest(format!(
87 "expected schema {}, got {}",
88 POOL_MANIFEST_SCHEMA, self.schema_version
89 )));
90 }
91 if self.pool_id.hex().is_empty() {
92 return Err(crate::error::PolyKvError::InvalidManifest(
93 "pool_id is empty".into(),
94 ));
95 }
96 self.shape.validate()?;
97 self.policy.validate()?;
98 Ok(())
99 }
100
101 pub fn digest(&self) -> crate::error::Result<Digest> {
102 crate::digest_compat::compute_json(self)
103 }
104
105 #[cfg(feature = "typed-ids")]
107 pub fn with_scope(mut self, scope: stack_ids::ScopeKey) -> Self {
108 self.scope = Some(scope);
109 self
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct ShellManifest {
116 pub schema_version: String,
118 pub agent_id: AgentId,
120 pub pool_digest: Digest,
122 pub num_unique_tokens: u32,
124 pub num_unique_layers: u32,
126 pub shell_size_bytes: u64,
128 pub shell_codec: CodecId,
130 pub materialized_at_unix: i64,
132 pub materialize_seed: u64,
134}
135
136impl ShellManifest {
137 pub fn new(
139 agent_id: AgentId,
140 pool_digest: Digest,
141 num_unique_tokens: u32,
142 num_unique_layers: u32,
143 shell_size_bytes: u64,
144 materialize_seed: u64,
145 materialized_at_unix: i64,
146 ) -> crate::error::Result<Self> {
147 let manifest = Self {
148 schema_version: SHELL_MANIFEST_SCHEMA.into(),
149 agent_id,
150 pool_digest,
151 num_unique_tokens,
152 num_unique_layers,
153 shell_size_bytes,
154 shell_codec: CODEC_IDENTIFIER_SHELL.into(),
155 materialized_at_unix,
156 materialize_seed,
157 };
158 manifest.validate()?;
159 Ok(manifest)
160 }
161
162 pub fn validate(&self) -> crate::error::Result<()> {
164 if self.schema_version != SHELL_MANIFEST_SCHEMA {
165 return Err(crate::error::PolyKvError::InvalidManifest(format!(
166 "expected schema {}, got {}",
167 SHELL_MANIFEST_SCHEMA, self.schema_version
168 )));
169 }
170 if self.agent_id.is_empty() {
171 return Err(crate::error::PolyKvError::InvalidManifest(
172 "agent_id is empty".into(),
173 ));
174 }
175 if self.pool_digest.hex().is_empty() {
176 return Err(crate::error::PolyKvError::InvalidManifest(
177 "pool_digest is empty".into(),
178 ));
179 }
180 Ok(())
181 }
182
183 pub fn digest(&self) -> crate::error::Result<Digest> {
185 crate::digest_compat::compute_json(self)
186 }
187}
188
189const CODEC_IDENTIFIER_SHARED: &str = crate::policy::CODEC_FIB_K4_N32;
191const CODEC_IDENTIFIER_SHELL: &str = crate::policy::CODEC_TURBO_8BIT;