Skip to main content

lean_ctx/core/context_kernel/
capsule_wire.rs

1//! Wire types for transferring reference-only context between agents.
2
3use std::collections::{HashMap, HashSet};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6/// Wire-compatible context capsule for multi-agent transfer.
7/// Contains only references and metadata — never raw content payloads.
8#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
9pub struct ContextCapsuleV1 {
10    /// Content-derived capsule identifier.
11    pub capsule_id: String,
12    /// Content-addressed reference to the capsule manifest.
13    pub manifest_ref: String,
14    /// Selected content references, without payloads.
15    pub selected_refs: Vec<String>,
16    /// Intent the transferred context supports.
17    pub intent: String,
18    /// Total token budget assigned to the capsule.
19    pub budget_tokens: u64,
20    /// Token budget still available to recipients.
21    pub budget_remaining: u64,
22    /// Policy version used to construct the capsule.
23    pub policy_version: String,
24    /// Capsule sensitivity classification.
25    pub sensitivity: String,
26    /// Agent that owns the capsule.
27    pub owner_agent: String,
28    /// Agents permitted to receive the capsule.
29    pub target_agents: Vec<String>,
30    /// Parent capsule identifier for transfer chains.
31    pub parent_capsule: Option<String>,
32    /// Creation time as seconds since the Unix epoch.
33    pub created_at_epoch: u64,
34}
35
36/// Represents the difference between a base capsule and an updated one.
37/// Used to minimize transfer size in multi-agent chains.
38#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
39pub struct DeltaTransfer {
40    /// Identifier of the capsule this delta applies to.
41    pub base_ref: String,
42    /// Content references added by the update.
43    pub added_refs: Vec<String>,
44    /// Content references removed by the update.
45    pub removed_refs: Vec<String>,
46    /// Signed change to the capsule token budget.
47    pub budget_delta: i64,
48    /// Opaque references to findings produced since the base capsule.
49    pub new_findings: Vec<String>,
50}
51
52/// Cross-capsule reference overlap and deduplication metrics.
53#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
54pub struct DedupReport {
55    /// References present in more than one sibling capsule.
56    pub shared_refs: Vec<String>,
57    /// References present only in the corresponding capsule.
58    pub unique_per_capsule: Vec<Vec<String>>,
59    /// Fraction of reference entries removable through deduplication.
60    pub dedup_ratio: f64,
61}
62
63impl ContextCapsuleV1 {
64    /// Creates a reference-only capsule with a deterministic content-derived ID.
65    pub fn new(intent: &str, refs: Vec<String>, budget: u64, owner: &str) -> Self {
66        let capsule_id = capsule_id(intent, &refs);
67        Self {
68            manifest_ref: capsule_id.clone(),
69            capsule_id,
70            selected_refs: refs,
71            intent: intent.to_owned(),
72            budget_tokens: budget,
73            budget_remaining: budget,
74            policy_version: "v1".to_owned(),
75            sensitivity: "internal".to_owned(),
76            owner_agent: owner.to_owned(),
77            target_agents: Vec::new(),
78            parent_capsule: None,
79            created_at_epoch: SystemTime::now()
80                .duration_since(UNIX_EPOCH)
81                .map_or(0, |duration| duration.as_secs()),
82        }
83    }
84}
85impl DeltaTransfer {
86    /// Returns `true` if the delta has fewer total refs than the updated capsule.
87    ///
88    /// When this returns `false`, callers should send the full capsule instead
89    /// of the delta, since the disjoint-update case can produce a delta that
90    /// is larger than the full capsule.
91    pub fn is_efficient(&self, updated_ref_count: usize) -> bool {
92        self.added_refs.len() + self.removed_refs.len() < updated_ref_count
93    }
94}
95
96/// Computes the reference and budget difference between two capsules.
97pub fn compute_delta(base: &ContextCapsuleV1, updated: &ContextCapsuleV1) -> DeltaTransfer {
98    let base_refs: HashSet<&str> = base.selected_refs.iter().map(String::as_str).collect();
99    let updated_refs: HashSet<&str> = updated.selected_refs.iter().map(String::as_str).collect();
100
101    DeltaTransfer {
102        base_ref: base.capsule_id.clone(),
103        added_refs: updated
104            .selected_refs
105            .iter()
106            .filter(|reference| !base_refs.contains(reference.as_str()))
107            .cloned()
108            .collect(),
109        removed_refs: base
110            .selected_refs
111            .iter()
112            .filter(|reference| !updated_refs.contains(reference.as_str()))
113            .cloned()
114            .collect(),
115        budget_delta: signed_difference(updated.budget_tokens, base.budget_tokens),
116        new_findings: Vec::new(),
117    }
118}
119
120/// Applies a reference and budget delta to a base capsule.
121pub fn apply_delta(base: &ContextCapsuleV1, delta: &DeltaTransfer) -> ContextCapsuleV1 {
122    let removed: HashSet<&str> = delta.removed_refs.iter().map(String::as_str).collect();
123    let mut selected_refs: Vec<String> = base
124        .selected_refs
125        .iter()
126        .filter(|reference| !removed.contains(reference.as_str()))
127        .cloned()
128        .collect();
129    let mut present: HashSet<String> = selected_refs.iter().cloned().collect();
130    for reference in &delta.added_refs {
131        if present.insert(reference.clone()) {
132            selected_refs.push(reference.clone());
133        }
134    }
135
136    let mut updated = base.clone();
137    updated.budget_tokens = apply_signed(base.budget_tokens, delta.budget_delta);
138    updated.budget_remaining = apply_signed(base.budget_remaining, delta.budget_delta);
139    updated.selected_refs = selected_refs;
140    updated.capsule_id = capsule_id(&updated.intent, &updated.selected_refs);
141    updated.manifest_ref.clone_from(&updated.capsule_id);
142    updated
143}
144
145/// Finds references shared across sibling capsules and reports transfer savings.
146pub fn dedup_siblings(capsules: &[ContextCapsuleV1]) -> DedupReport {
147    let mut counts: HashMap<&str, usize> = HashMap::new();
148    for capsule in capsules {
149        let refs: HashSet<&str> = capsule.selected_refs.iter().map(String::as_str).collect();
150        for reference in refs {
151            *counts.entry(reference).or_default() += 1;
152        }
153    }
154
155    let mut seen = HashSet::new();
156    let shared_refs = capsules
157        .iter()
158        .flat_map(|capsule| &capsule.selected_refs)
159        .filter(|reference| counts.get(reference.as_str()).copied().unwrap_or(0) > 1)
160        .filter(|reference| seen.insert(reference.as_str()))
161        .cloned()
162        .collect();
163    let unique_per_capsule = capsules
164        .iter()
165        .map(|capsule| {
166            capsule
167                .selected_refs
168                .iter()
169                .filter(|reference| counts.get(reference.as_str()).copied().unwrap_or(0) == 1)
170                .cloned()
171                .collect()
172        })
173        .collect();
174    let total_refs: usize = capsules
175        .iter()
176        .map(|capsule| capsule.selected_refs.len())
177        .sum();
178    let distinct_refs = counts.len();
179    let dedup_ratio = if total_refs == 0 {
180        0.0
181    } else {
182        (total_refs.saturating_sub(distinct_refs)) as f64 / total_refs as f64
183    };
184
185    DedupReport {
186        shared_refs,
187        unique_per_capsule,
188        dedup_ratio,
189    }
190}
191
192fn capsule_id(intent: &str, refs: &[String]) -> String {
193    let mut hasher = blake3::Hasher::new();
194    hasher.update(&(intent.len() as u64).to_le_bytes());
195    hasher.update(intent.as_bytes());
196    for reference in refs {
197        hasher.update(&(reference.len() as u64).to_le_bytes());
198        hasher.update(reference.as_bytes());
199    }
200    format!("blake3:{}", hasher.finalize().to_hex())
201}
202
203fn signed_difference(updated: u64, base: u64) -> i64 {
204    if updated >= base {
205        i64::try_from(updated - base).unwrap_or(i64::MAX)
206    } else {
207        -i64::try_from(base - updated).unwrap_or(i64::MAX)
208    }
209}
210
211fn apply_signed(value: u64, delta: i64) -> u64 {
212    if delta >= 0 {
213        value.saturating_add(delta.unsigned_abs())
214    } else {
215        value.saturating_sub(delta.unsigned_abs())
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::{ContextCapsuleV1, apply_delta, compute_delta, dedup_siblings};
222
223    fn capsule(refs: &[&str], budget: u64) -> ContextCapsuleV1 {
224        ContextCapsuleV1::new(
225            "handoff",
226            refs.iter()
227                .map(|reference| (*reference).to_owned())
228                .collect(),
229            budget,
230            "agent-a",
231        )
232    }
233
234    #[test]
235    fn new_capsule_has_content_addressed_id() {
236        let first = capsule(&["ref-a", "ref-b"], 100);
237        let second = capsule(&["ref-a", "ref-b"], 100);
238
239        assert_eq!(first.capsule_id, second.capsule_id);
240        assert!(first.capsule_id.starts_with("blake3:"));
241        assert_eq!(first.manifest_ref, first.capsule_id);
242    }
243
244    #[test]
245    fn compute_delta_finds_added_removed() {
246        let delta = compute_delta(&capsule(&["A", "B"], 100), &capsule(&["B", "C"], 80));
247
248        assert_eq!(delta.added_refs, ["C"]);
249        assert_eq!(delta.removed_refs, ["A"]);
250        assert_eq!(delta.budget_delta, -20);
251    }
252
253    #[test]
254    fn apply_delta_reconstructs_capsule() {
255        let base = capsule(&["A", "B"], 100);
256        let updated = capsule(&["B", "C"], 80);
257
258        assert_eq!(apply_delta(&base, &compute_delta(&base, &updated)), updated);
259    }
260
261    #[test]
262    fn delta_smaller_than_full() {
263        let base = capsule(&["ref-a", "ref-b", "ref-c"], 100);
264        let updated = capsule(&["ref-a", "ref-b", "ref-d"], 90);
265        let delta = compute_delta(&base, &updated);
266
267        assert!(
268            serde_json::to_vec(&delta).unwrap().len() < serde_json::to_vec(&updated).unwrap().len()
269        );
270    }
271
272    #[test]
273    fn dedup_siblings_finds_shared_refs() {
274        let capsules = [
275            capsule(&["ref1", "ref2", "only-a"], 100),
276            capsule(&["ref1", "ref2", "only-b"], 100),
277            capsule(&["ref1", "ref2", "only-c"], 100),
278        ];
279        let report = dedup_siblings(&capsules);
280
281        assert_eq!(report.shared_refs, ["ref1", "ref2"]);
282        assert_eq!(report.unique_per_capsule[0], ["only-a"]);
283        assert!((report.dedup_ratio - 4.0 / 9.0).abs() < f64::EPSILON);
284    }
285
286    #[test]
287    fn empty_capsule_delta_is_identity() {
288        let base = capsule(&[], 100);
289        let delta = compute_delta(&base, &base);
290
291        assert!(delta.added_refs.is_empty());
292        assert!(delta.removed_refs.is_empty());
293        assert_eq!(apply_delta(&base, &delta), base);
294    }
295}