Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Plugin architecture, registration, and trait patterns
.claude/skills/xberg-io-plugin-architecture-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 32% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 21% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 18% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 22% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 89% | 0% |
| Type | Trait | Location | | --- | --- | --- | | Document extractor (binding-facing) | DocumentExtractor: Plugin | plugins/extractor/trait.rs | | Document extractor (in-crate) | InternalDocumentExtractor: Plugin | plugins/extractor/trait.rs | | OCR backend | OcrBackend: Plugin | plugins/ocr.rs (a file, not a directory) | | Post processor | PostProcessor: Plugin | plugins/processor/trait.rs | | Validator | Validator: Plugin | plugins/validator/trait.rs | | Embedding backend | EmbeddingBackend: Plugin | plugins/embedding.rs | | Reranker backend | RerankerBackend: Plugin | plugins/reranker.rs | | Tokenizer backend | TokenizerBackend: Plugin | plugins/tokenizer.rs | | Renderer | Renderer: Plugin | plugins/renderer.rs |
Plugin (plugins/traits.rs) is Send + Sync and requires name(); version(), initialize(), shutdown(), description(), and author() have defaults. There is no 'static trait bound; registry-owned Arc<dyn Trait> supplies the necessary lifetime.
InternalDocumentExtractorDocumentExtractor is the binding-facing surface. In-crate extractors implement InternalDocumentExtractor and get DocumentExtractor from a blanket impl. Implementing DocumentExtractor directly in this crate is the wrong layer.
rust#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl InternalDocumentExtractor for MyExtractor { async fn extract_content(&self, content: &[u8], mime_type: &str, config: &ExtractionConfig) -> Result<InternalDocument> { /* ... */ } fn supported_mime_types(&self) -> &[&str] { &["application/x-custom"] } fn priority(&self) -> i32 { 50 } }
extract_path has a default that reads the file and delegates to extract_content (and errors without tokio-runtime).
Always use the two-arm cfg_attr form for async_trait. A bare #[async_trait] does not match the trait declaration on wasm32.
The public trait has exactly four items — extract, supported_mime_types, priority, can_handle. There is no as_sync_extractor; writing one is a compile error. WASM sync support is the separate SyncExtractor trait — see wasm-constraints.
| Range | Use | | --- | --- | | 0-25 | Fallback/low-quality | | 26-49 | Alternative extractors | | 50 | Default (built-in) | | 51-75 | Premium/enhanced | | 76-100 | Specialized/high-priority |
The registry selects the highest priority extractor for each MIME type. The ranges are conventions over an unclamped i32; negative and values above 100 are representable. Equal MIME and priority is a collision: the later registration replaces the earlier entry and warns. Give competing plugins distinct priorities.
rust// crates/xberg/src/extractors/mod.rs -> register_default_extractors() let registry = get_document_extractor_registry(); let mut registry = registry.write(); registry.register(Arc::new(MyExtractor::new()))?;
Feature-gate optional formats:
rust#[cfg(feature = "office")] { registry.register(Arc::new(DocxExtractor::new()))?; registry.register(Arc::new(PptxExtractor::new()))?; }
rust#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl PostProcessor for MyProcessor { async fn process(&self, result: &mut ExtractedDocument, config: &ExtractionConfig) -> Result<()> { result.content = process_content(&result.content); Ok(()) } fn processing_stage(&self) -> ProcessingStage { ProcessingStage::Middle } }
The enum is ProcessingStage and the accessor is processing_stage(). Stages: Early (default) → Middle → Late. process takes &mut ExtractedDocument, not an owned result.
Send + Sync — Plugin requires it.InternalDocumentExtractor, never DocumentExtractor.cfg_attr async_trait form on every plugin trait impl.#[cfg(feature = "...")] at the registration site.ensure_initialized() (extractors/mod.rs), called before first extraction."pdf-extractor").#[cfg_attr(alef, alef(skip))] or the binding regen aborts — seealef-generated-bindings.
plugins/registry/mod.rs.There is no universal PluginRegistry.
Arc<parking_lot::RwLock<_>>. Their guards are not poisoned and.read()/.write() return guards directly.
HashMap<mime, BTreeMap<priority, entry>>: exact MIME lookup isconstant-time on the outer map; wildcard-family lookup scans registered MIME keys.
initialize() and rejects a plugin whose initialization fails.Registries support register, remove, clear, and shutdown_all; there is no hot reload.
public APIs, so breaking changes follow the public compatibility policy.
access, and failure paths with test doubles; use real backends for integration coverage. No dispatch-overhead benchmark exists unless one is explicitly added.
The Python bridge is generated into crates/xberg-py/src/lib.rs; there is no hand-written plugins.rs. Change Alef/configuration and regenerate rather than editing the bridge.
Python::attach. Async host calls enter Python fromtokio::task::spawn_blocking and propagate the caller's contextvars context.
methods do not need repeated GIL acquisition. Do not assume allow_threads is in use.
Crossing types therefore need Serialize + Deserialize + Default, including unit enums.
XbergError::Other with plugin and method context; the originalPython exception type and traceback are not retained. Infallible methods can only warn and return Default::default(), so a default may indicate bridge failure rather than real data.
XbergError::Plugin, which is fallback-eligible;do not assume Python bridge errors have the same fallback behavior.
benchmark.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,919 | 14,580 | -2% | 1 | 1 | 0% | 2,984 | 3,928 | +32% | 0 | 0 | — |
case-02 | fail→pass | 21,791 | 15,891 | -27% | 1 | 1 | 0% | 3,438 | 4,164 | +21% | 0 | 0 | — |
case-03 | fail→pass | 20,181 | 15,201 | -25% | 1 | 1 | 0% | 2,815 | 3,324 | +18% | 0 | 0 | — |
case-04 | fail→pass | 18,944 | 5,107 | -73% | 1 | 1 | 0% | 2,284 | 2,790 | +22% | 0 | 0 | — |
case-05 | fail→pass | 9,265 | 10,309 | +11% | 1 | 1 | 0% | 1,459 | 2,760 | +89% | 0 | 0 | — |
case-06 | pass→pass | 15,101 | 9,592 | -36% | 1 | 1 | 0% | 2,507 | 2,578 | +3% | 0 | 0 | — |
case-07 | fail→pass | 21,055 | 12,594 | -40% | 1 | 1 | 0% | 3,748 | 3,506 | -6% | 0 | 0 | — |
case-08 | fail→pass | 16,836 | 6,921 | -59% | 1 | 1 | 0% | 2,875 | 2,888 | +0% | 0 | 0 | — |
case-09 | fail→pass | 22,396 | 2,550 | -89% | 1 | 1 | 0% | 2,600 | 2,218 | -15% | 0 | 0 | — |
case-10 | fail→pass | 16,644 | 3,261 | -80% | 1 | 1 | 0% | 2,670 | 2,159 | -19% | 0 | 0 | — |
case-11 | pass→fail | 10,041 | 6,791 | -32% | 1 | 1 | 0% | 1,746 | 2,033 | +16% | 0 | 0 | — |
case-12 | fail→pass | 20,767 | 2,899 | -86% | 1 | 1 | 0% | 3,148 | 2,372 | -25% | 0 | 0 | — |
case-13 | fail→pass | 17,092 | 9,040 | -47% | 1 | 1 | 0% | 2,121 | 2,479 | +17% | 0 | 0 | — |
case-14 | fail→pass | 15,849 | 3,773 | -76% | 1 | 1 | 0% | 1,818 | 2,462 | +35% | 0 | 0 | — |
case-15 | pass→pass | 16,889 | 8,458 | -50% | 1 | 1 | 0% | 2,185 | 2,460 | +13% | 0 | 0 | — |
case-16 | fail→pass | 16,580 | 8,303 | -50% | 1 | 1 | 0% | 2,242 | 2,185 | -3% | 0 | 0 | — |
case-17 | fail→pass | 28,633 | 10,811 | -62% | 1 | 1 | 0% | 1,252 | 2,817 | +125% | 0 | 0 | — |
case-18 | pass→pass | 17,731 | 12,115 | -32% | 1 | 1 | 0% | 2,026 | 2,992 | +48% | 0 | 0 | — |
case-19 | pass→pass | 18,758 | 15,010 | -20% | 1 | 1 | 0% | 2,254 | 3,515 | +56% | 0 | 0 | — |
case-20 | pass→pass | 13,750 | 13,781 | +0% | 1 | 1 | 0% | 1,670 | 3,520 | +111% | 0 | 0 | — |
case-21 | pass→pass | 15,196 | 9,928 | -35% | 1 | 1 | 0% | 1,458 | 2,738 | +88% | 0 | 0 | — |
case-22 | fail→pass | 33,186 | 9,391 | -72% | 1 | 1 | 0% | 5,278 | 2,424 | -54% | 0 | 0 | — |
case-23 | pass→pass | 19,354 | 10,702 | -45% | 1 | 1 | 0% | 2,750 | 2,862 | +4% | 0 | 0 | — |
case-24 | fail→pass | 17,601 | 11,122 | -37% | 1 | 1 | 0% | 2,970 | 2,816 | -5% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted, and 23 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +63 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/24/2026 | +59% |
| gemini-3.6-flash | verified | 8/11/2026 | +55% |
Other measured skills in the registry, with their headline benchmark lift.