vtcode-core 0.103.1

Core library for VT Code - a Rust-based terminal coding agent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
//! Skill container for multi-skill execution
//!
//! Implements VT Code's container model for managing multiple skills
//! in a single request with version support and container reuse.
//!
//! Up to 8 skills per container. Container IDs can be reused across
//! multiple turns for state preservation.

use hashbrown::HashSet;
use serde::{Deserialize, Serialize};

/// Skill source type (Anthropic-managed or custom)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SkillType {
    /// Pre-built Anthropic skills (pptx, xlsx, docx, pdf, etc.)
    #[serde(rename = "anthropic")]
    Anthropic,
    /// User-uploaded custom skills
    #[serde(rename = "custom")]
    Custom,
}

/// Skill version specification
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(untagged)]
pub enum SkillVersion {
    /// Always use latest version
    #[serde(rename = "latest")]
    #[default]
    Latest,
    /// Specific version by ID (epoch timestamp for custom skills, date for Anthropic)
    Specific(String),
}

impl SkillVersion {
    pub fn as_str(&self) -> &str {
        match self {
            SkillVersion::Latest => "latest",
            SkillVersion::Specific(v) => v,
        }
    }

    pub fn is_latest(&self) -> bool {
        matches!(self, SkillVersion::Latest)
    }
}

/// How a skill is referenced in a container
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SkillSource {
    /// Reference to a registered skill by ID
    #[serde(rename = "skill_reference")]
    Reference {
        skill_id: String,
        #[serde(default)]
        version: SkillVersion,
    },
    /// Inline base64-encoded zip bundle (no pre-registration needed)
    #[serde(rename = "inline")]
    Inline {
        /// Base64-encoded zip bundle
        bundle_b64: String,
        /// Optional SHA-256 hash for caching/deduplication
        #[serde(skip_serializing_if = "Option::is_none")]
        sha256: Option<String>,
    },
}

/// Specification for a single skill in a container
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillSpec {
    /// Type of skill (anthropic or custom)
    #[serde(rename = "type")]
    pub skill_type: SkillType,
    /// Skill identifier (short name for Anthropic, UUID for custom)
    pub skill_id: String,
    /// Version to use (latest or specific epoch timestamp)
    #[serde(default)]
    pub version: SkillVersion,
}

impl SkillSpec {
    /// Create a new skill specification
    pub fn new(skill_type: SkillType, skill_id: impl Into<String>) -> Self {
        Self {
            skill_type,
            skill_id: skill_id.into(),
            version: SkillVersion::Latest,
        }
    }

    /// Create with specific version
    pub fn with_version(mut self, version: SkillVersion) -> Self {
        self.version = version;
        self
    }

    /// Create Anthropic skill (predefined by Anthropic)
    pub fn anthropic(skill_id: impl Into<String>) -> Self {
        Self::new(SkillType::Anthropic, skill_id)
    }

    /// Create custom skill (user-uploaded)
    pub fn custom(skill_id: impl Into<String>) -> Self {
        Self::new(SkillType::Custom, skill_id)
    }
}

/// Container for managing multiple skills in a request
///
/// Implements VT Code's container model for multi-skill execution.
/// - Maximum 8 skills per container
/// - Container ID can be reused across multiple turns
/// - Each skill can have independent version pinning
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillContainer {
    /// Optional container ID for reuse across turns
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Skills to load in this container (max 8)
    pub skills: Vec<SkillSpec>,
    /// Inline skill bundles (base64-encoded zips, no pre-registration needed)
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub inline_bundles: Vec<SkillSource>,
}

impl SkillContainer {
    /// Create a new skill container
    pub fn new() -> Self {
        Self {
            id: None,
            skills: Vec::with_capacity(8),
            inline_bundles: Vec::new(),
        }
    }

    /// Create with single skill
    pub fn single(spec: SkillSpec) -> Self {
        Self {
            id: None,
            skills: vec![spec],
            inline_bundles: Vec::new(),
        }
    }

    /// Create with container ID (for reuse)
    pub fn with_id(id: impl Into<String>) -> Self {
        Self {
            id: Some(id.into()),
            skills: Vec::with_capacity(8),
            inline_bundles: Vec::new(),
        }
    }

    /// Add a skill to the container
    ///
    /// # Errors
    /// Returns error if adding skill would exceed maximum of 8 skills
    pub fn add_skill(&mut self, spec: SkillSpec) -> anyhow::Result<()> {
        if self.skills.len() >= 8 {
            anyhow::bail!(
                "Container already has maximum skills (8), cannot add '{}'",
                spec.skill_id
            );
        }
        self.skills.push(spec);
        Ok(())
    }

    /// Add multiple skills
    ///
    /// # Errors
    /// Returns error if total would exceed 8 skills
    pub fn add_skills(&mut self, mut specs: Vec<SkillSpec>) -> anyhow::Result<()> {
        let current_len = self.skills.len();
        let new_len = current_len + specs.len();
        if new_len > 8 {
            anyhow::bail!(
                "Adding {} skills would exceed maximum (8). Current: {}, requested: {}",
                specs.len(),
                current_len,
                specs.len()
            );
        }
        // Reserve capacity to avoid reallocations
        if new_len > self.skills.capacity() {
            self.skills.reserve(new_len - current_len);
        }
        self.skills.append(&mut specs);
        Ok(())
    }

    /// Add Anthropic skill
    pub fn add_anthropic(&mut self, skill_id: impl Into<String>) -> anyhow::Result<()> {
        self.add_skill(SkillSpec::anthropic(skill_id))
    }

    /// Add custom skill
    pub fn add_custom(&mut self, skill_id: impl Into<String>) -> anyhow::Result<()> {
        self.add_skill(SkillSpec::custom(skill_id))
    }

    /// Add an inline skill bundle (base64-encoded zip, no pre-registration needed)
    ///
    /// Also registers a corresponding `SkillSpec` so the skill is tracked in `skills`.
    ///
    /// # Errors
    /// Returns error if adding would exceed the maximum of 8 skills.
    pub fn add_inline(&mut self, bundle_b64: String, sha256: Option<String>) -> anyhow::Result<()> {
        if self.skills.len() >= 8 {
            anyhow::bail!("Container already has maximum skills (8)");
        }
        let spec = SkillSpec {
            skill_type: SkillType::Custom,
            skill_id: sha256
                .clone()
                .unwrap_or_else(|| format!("inline-{}", self.skills.len())),
            version: SkillVersion::Latest,
        };
        self.skills.push(spec);
        self.inline_bundles
            .push(SkillSource::Inline { bundle_b64, sha256 });
        Ok(())
    }

    /// Get number of skills in container
    pub fn len(&self) -> usize {
        self.skills.len()
    }

    /// Check if container is empty
    pub fn is_empty(&self) -> bool {
        self.skills.is_empty()
    }

    /// Check if container has a specific skill
    pub fn has_skill(&self, skill_id: &str) -> bool {
        self.skills.iter().any(|s| s.skill_id == skill_id)
    }

    /// Get skill by ID
    pub fn get_skill(&self, skill_id: &str) -> Option<&SkillSpec> {
        self.skills.iter().find(|s| s.skill_id == skill_id)
    }

    /// Validate container
    ///
    /// Checks:
    /// - No more than 8 skills
    /// - No duplicate skill IDs
    pub fn validate(&self) -> anyhow::Result<()> {
        if self.skills.len() > 8 {
            anyhow::bail!("Container has {} skills, maximum is 8", self.skills.len());
        }

        let mut seen_ids = HashSet::new();
        for spec in &self.skills {
            if !seen_ids.insert(&spec.skill_id) {
                anyhow::bail!("Duplicate skill ID in container: '{}'", spec.skill_id);
            }
        }

        Ok(())
    }

    /// Set container ID for reuse
    pub fn set_id(&mut self, id: impl Into<String>) {
        self.id = Some(id.into());
    }

    /// Clear container ID
    pub fn clear_id(&mut self) {
        self.id = None;
    }

    /// Get all skill IDs in container
    pub fn skill_ids(&self) -> Vec<&str> {
        self.skills.iter().map(|s| s.skill_id.as_str()).collect()
    }

    /// Get all skills of a specific type
    pub fn skills_by_type(&self, skill_type: SkillType) -> Vec<&SkillSpec> {
        self.skills
            .iter()
            .filter(|s| s.skill_type == skill_type)
            .collect()
    }

    /// Count anthropic skills
    pub fn anthropic_count(&self) -> usize {
        self.skills_by_type(SkillType::Anthropic).len()
    }

    /// Count custom skills
    pub fn custom_count(&self) -> usize {
        self.skills_by_type(SkillType::Custom).len()
    }
}

impl Default for SkillContainer {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_skill_spec_new() {
        let spec = SkillSpec::new(SkillType::Custom, "my-skill");
        assert_eq!(spec.skill_id, "my-skill");
        assert_eq!(spec.skill_type, SkillType::Custom);
        assert!(spec.version.is_latest());
    }

    #[test]
    fn test_skill_spec_anthropic() {
        let spec = SkillSpec::anthropic("xlsx");
        assert_eq!(spec.skill_id, "xlsx");
        assert_eq!(spec.skill_type, SkillType::Anthropic);
    }

    #[test]
    fn test_skill_spec_with_version() {
        let spec = SkillSpec::custom("my-skill")
            .with_version(SkillVersion::Specific("1759178010641129".to_string()));
        assert_eq!(spec.version.as_str(), "1759178010641129");
        assert!(!spec.version.is_latest());
    }

    #[test]
    fn test_container_creation() {
        let container = SkillContainer::new();
        assert!(container.is_empty());
        assert!(container.id.is_none());
    }

    #[test]
    fn test_container_single_skill() {
        let spec = SkillSpec::custom("test-skill");
        let container = SkillContainer::single(spec.clone());
        assert_eq!(container.len(), 1);
        assert!(container.has_skill("test-skill"));
        assert_eq!(container.get_skill("test-skill"), Some(&spec));
    }

    #[test]
    fn test_container_add_skill() {
        let mut container = SkillContainer::new();
        let spec = SkillSpec::custom("skill1");
        assert!(container.add_skill(spec).is_ok());
        assert_eq!(container.len(), 1);
    }

    #[test]
    fn test_container_max_skills() {
        let mut container = SkillContainer::new();
        for i in 0..8 {
            let spec = SkillSpec::custom(format!("skill{}", i));
            assert!(container.add_skill(spec).is_ok());
        }
        assert_eq!(container.len(), 8);

        // Try to add 9th skill
        let spec = SkillSpec::custom("skill9");
        assert!(container.add_skill(spec).is_err());
    }

    #[test]
    fn test_container_add_skills_batch() {
        let mut container = SkillContainer::new();
        let specs = vec![
            SkillSpec::custom("skill1"),
            SkillSpec::custom("skill2"),
            SkillSpec::custom("skill3"),
        ];
        assert!(container.add_skills(specs).is_ok());
        assert_eq!(container.len(), 3);
    }

    #[test]
    fn test_container_add_skills_batch_overflow() {
        let mut container = SkillContainer::new();
        for i in 0..7 {
            let spec = SkillSpec::custom(format!("skill{}", i));
            container.add_skill(spec).ok();
        }
        assert_eq!(container.len(), 7);

        let specs = vec![SkillSpec::custom("skill7"), SkillSpec::custom("skill8")];
        assert!(container.add_skills(specs).is_err());
    }

    #[test]
    fn test_container_duplicate_skill_ids() {
        let mut container = SkillContainer::new();
        assert!(container.add_skill(SkillSpec::custom("dup")).is_ok());
        assert!(container.add_skill(SkillSpec::custom("dup")).is_ok());
        assert!(container.validate().is_err());
    }

    #[test]
    fn test_container_with_id() {
        let container = SkillContainer::with_id("container-123");
        assert_eq!(container.id, Some("container-123".to_string()));
    }

    #[test]
    fn test_container_set_id() {
        let mut container = SkillContainer::new();
        container.set_id("new-id");
        assert_eq!(container.id, Some("new-id".to_string()));
    }

    #[test]
    fn test_container_skills_by_type() {
        let mut container = SkillContainer::new();
        container.add_anthropic("xlsx").ok();
        container.add_anthropic("pptx").ok();
        container.add_custom("my-skill").ok();

        let anthropic = container.skills_by_type(SkillType::Anthropic);
        assert_eq!(anthropic.len(), 2);

        let custom = container.skills_by_type(SkillType::Custom);
        assert_eq!(custom.len(), 1);

        assert_eq!(container.anthropic_count(), 2);
        assert_eq!(container.custom_count(), 1);
    }

    #[test]
    fn test_container_skill_ids() {
        let mut container = SkillContainer::new();
        container.add_skill(SkillSpec::custom("skill1")).ok();
        container.add_skill(SkillSpec::custom("skill2")).ok();
        container.add_skill(SkillSpec::custom("skill3")).ok();

        let ids = container.skill_ids();
        assert_eq!(ids, vec!["skill1", "skill2", "skill3"]);
    }

    #[test]
    fn test_container_validation() {
        let mut container = SkillContainer::new();
        for i in 0..8 {
            container
                .add_skill(SkillSpec::custom(format!("skill{}", i)))
                .ok();
        }
        assert!(container.validate().is_ok());
    }

    #[test]
    fn test_skill_spec_roundtrip() {
        // Test serialization/deserialization roundtrip
        let spec = SkillSpec {
            skill_type: SkillType::Custom,
            skill_id: "my-skill".to_string(),
            version: SkillVersion::Specific("1759178010641129".to_string()),
        };

        let json = serde_json::to_string(&spec).unwrap();
        let deserialized: SkillSpec = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.skill_id, "my-skill");
        assert_eq!(deserialized.skill_type, SkillType::Custom);
        assert_eq!(
            deserialized.version,
            SkillVersion::Specific("1759178010641129".to_string())
        );
    }

    #[test]
    fn test_container_serialization() {
        let mut container = SkillContainer::new();
        container.add_anthropic("xlsx").ok();
        container.add_custom("my-skill").ok();

        let json = serde_json::to_string(&container).unwrap();
        let deserialized: SkillContainer = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.len(), 2);
        assert!(deserialized.has_skill("xlsx"));
        assert!(deserialized.has_skill("my-skill"));
    }

    #[test]
    fn test_skill_source_reference_roundtrip() {
        let source = SkillSource::Reference {
            skill_id: "my-skill".to_string(),
            version: SkillVersion::Latest,
        };
        let json = serde_json::to_string(&source).unwrap();
        let deserialized: SkillSource = serde_json::from_str(&json).unwrap();
        assert_eq!(source, deserialized);
    }

    #[test]
    fn test_skill_source_inline_roundtrip() {
        let source = SkillSource::Inline {
            bundle_b64: "UEsFBgAAAAAAAA==".to_string(),
            sha256: Some("abc123".to_string()),
        };
        let json = serde_json::to_string(&source).unwrap();
        assert!(json.contains("\"type\":\"inline\""));
        let deserialized: SkillSource = serde_json::from_str(&json).unwrap();
        assert_eq!(source, deserialized);
    }

    #[test]
    fn test_skill_source_inline_no_sha() {
        let source = SkillSource::Inline {
            bundle_b64: "UEsFBgAAAAAAAA==".to_string(),
            sha256: None,
        };
        let json = serde_json::to_string(&source).unwrap();
        assert!(!json.contains("sha256"));
        let deserialized: SkillSource = serde_json::from_str(&json).unwrap();
        assert_eq!(source, deserialized);
    }

    #[test]
    fn test_add_inline_with_sha() {
        let mut container = SkillContainer::new();
        container
            .add_inline("UEsFBgAAAAAAAA==".to_string(), Some("deadbeef".to_string()))
            .unwrap();

        assert_eq!(container.len(), 1);
        assert!(container.has_skill("deadbeef"));
        assert_eq!(container.inline_bundles.len(), 1);
        assert!(matches!(
            &container.inline_bundles[0],
            SkillSource::Inline { sha256: Some(h), .. } if h == "deadbeef"
        ));
    }

    #[test]
    fn test_add_inline_without_sha() {
        let mut container = SkillContainer::new();
        container
            .add_inline("UEsFBgAAAAAAAA==".to_string(), None)
            .unwrap();

        assert_eq!(container.len(), 1);
        assert!(container.has_skill("inline-0"));
        assert_eq!(container.inline_bundles.len(), 1);
    }

    #[test]
    fn test_add_inline_max_skills() {
        let mut container = SkillContainer::new();
        for i in 0..8 {
            container
                .add_skill(SkillSpec::custom(format!("skill{i}")))
                .unwrap();
        }
        let result = container.add_inline("data".to_string(), None);
        assert!(result.is_err());
    }

    #[test]
    fn test_container_serialization_with_inline_bundles() {
        let mut container = SkillContainer::new();
        container.add_anthropic("xlsx").unwrap();
        container
            .add_inline("UEsFBgAAAAAAAA==".to_string(), Some("hash1".to_string()))
            .unwrap();

        let json = serde_json::to_string(&container).unwrap();
        assert!(json.contains("inline_bundles"));

        let deserialized: SkillContainer = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.len(), 2);
        assert_eq!(deserialized.inline_bundles.len(), 1);
    }

    #[test]
    fn test_container_serialization_omits_empty_inline_bundles() {
        let mut container = SkillContainer::new();
        container.add_anthropic("xlsx").unwrap();

        let json = serde_json::to_string(&container).unwrap();
        assert!(!json.contains("inline_bundles"));
    }
}