La struttura di una cartuccia: punto di ingresso, manifest, definizioni delle cap e registrazione degli handler.
Che cos’è una cartuccia
Una cartuccia è un binario autonomo che comunica con il motore tramite il protocollo CapDAG su stdin/stdout. Ogni cartuccia fornisce una o più capacità (cap). Il motore individua le cartucce, le avvia su richiesta quando servono le loro cap e ne gestisce il ciclo di vita attraverso il CartridgeHostRuntime.
Cartucce esistenti:
| Nome | Linguaggio | Framework | Cap |
|---|---|---|---|
| ggufcartridge | Rust | llama.cpp | Generazione di testo LLM, visione, embedding, vocabolario, informazioni sul modello |
| candlecartridge | Rust | Candle | Embedding BERT, CLIP, BLIP, Whisper |
| mlxcartridge | Swift | MLX | Generazione di testo LLM, visione, embedding |
| pdfcartridge | Rust | — | Miniatura, metadati, struttura, disbind |
| txtcartridge | Rust | — | Miniatura, metadati, struttura per file di testo |
| modelcartridge | Rust | — | Download e cache dei modelli |
Punto di ingresso
Rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manifest = build_manifest();
let mut runtime = CartridgeRuntime::with_manifest(manifest);
runtime.register_op("cap:in=...;out=...;generate", || Box::new(GenerateOp));
runtime.register_op("cap:in=...;out=...;extract;target=metadata", || Box::new(MetadataOp));
runtime.run().await?;
Ok(())
}
L’attributo #[tokio::main] predispone il runtime asincrono multi-thread. run() rileva se il binario è stato avviato con argomenti CLI (modalità CLI) o senza (modalità cartuccia CapDAG) e gestisce entrambi i casi.
Sorgente: ggufcartridge/src/main.rs, candlecartridge/src/main.rs, pdfcartridge/src/main.rs, txtcartridge/src/main.rs.
Swift
@main
struct MLXCartridge {
static func main() {
let manifest = buildManifest()
let manifestData = try! JSONEncoder().encode(manifest)
let runtime = CartridgeRuntime(manifest: manifestData)
runtime.register_op_type(capUrn: CAP_GENERATE, make: GenerateTextOp.init)
runtime.register_op_type(capUrn: CAP_DESCRIBE, make: DescribeImageOp.init)
try! runtime.run()
}
}
Le cartucce Swift usano @main per il punto di ingresso. run() si blocca finché stdin non si chiude.
Sorgente: mlxcartridge/Sources/mlxcartridge/main.swift.
Manifest
Il manifest dice al motore che cosa sa fare questa cartuccia. È una struct CapManifest serializzata in JSON e inviata durante l’handshake HELLO:
let caps = vec![
identity_cap(), // Obbligatoria: verifica dell'identità
thumbnail_cap,
metadata_cap,
outline_cap,
disbind_cap,
];
let manifest = CapManifest::new(
"pdfcartridge".to_string(),
"1.0.0".to_string(),
"PDF document processing".to_string(),
vec![CapGroup::default_group(caps)],
);
Campi:
- name: nome della cartuccia.
- version: stringa di versione semver.
- description: che cosa fa questa cartuccia.
- cap_groups: lista di istanze di
CapGroup, ognuna contenente un insieme nominato di definizioniCapeadapter_urnsfacoltativi. UsaCapGroup::default_group(caps)per un unico gruppo senza nome. - author (facoltativo): nome del manutentore.
- page_url (facoltativo): URL della documentazione o del repository.
La struttura cap_groups esiste per supportare l’instradamento tramite adattatori: un gruppo può dichiarare adapter_urns che identificano quali adattatori di media sono disponibili per le cap di quel gruppo. Per la maggior parte delle cartucce, la scelta corretta è un unico gruppo predefinito senza URN di adattatori.
Sorgente: capdag-rs/src/bifaci/manifest.rs, capdag-rs/src/cap/definition.rs.
Cap di identità
Ogni manifest deve includere la cap di identità, identity_cap() in Rust o l’equivalente in Swift. Questa cap gestisce il passo di verifica dell’identità durante l’handshake (vedi Handshake). CartridgeRuntime::new() va in panic se CAP_IDENTITY (cap:effect=none) manca dal manifest.
L’handler di identità viene registrato automaticamente dal runtime. Devi solo includere identity_cap() nella lista delle cap del manifest.
Sorgente: capdag-rs/src/standard/caps.rs.
Definizione di una cap
Una Cap definisce una singola capacità:
let cap = Cap::new(
CapUrn::from_string("cap:in=\"media:document;ext=pdf\";out=\"media:record\";extract;target=metadata").unwrap(),
"Extract Metadata".to_string(),
"extract-metadata".to_string(), // slug (nome del sottocomando CLI)
);
Campi:
- urn: il
CapUrnche identifica questa capacità. Definisce i tipi di media di input/output e i tag dell’operazione. - title: nome leggibile da mostrare.
- slug: nome breve usato come sottocomando CLI.
- args: lista di definizioni
CapArg(argomenti di input). - metadata: coppie chiave-valore facoltative (ad es.
activity_timeout_secsper timeout personalizzati).
Sorgente: capdag-rs/src/cap/definition.rs.
CapArg
Ogni argomento accettato da una cap è descritto da un CapArg:
let arg = CapArg::new(
"media:document;ext=pdf", // media URN che identifica questo argomento
true, // obbligatorio
vec![ArgSource::Stdin { stdin: "media:document;ext=pdf".to_string() }],
);
Campi:
- media_urn: il media URN che identifica questo argomento nello stream. L’handler lo usa per trovare l’argomento tramite
find_stream()orequire_stream(). - required: se l’argomento deve essere fornito.
- sources: come può essere fornito l’argomento (per la modalità CLI).
- default_value (facoltativo): valore predefinito come
serde_json::Value. - description (facoltativo): descrizione leggibile.
- is_sequence: cardinalità, cioè se l’argomento trasporta una sequenza di
elementi (framing
emit_list_item) o un singolo valore. - streaming (
.streaming(true)): limitatezza: l’argomento viene consumato senza una promessa di lunghezza (un feed); la cap elabora gli elementi man mano che arrivano e può essere collegata in tempo reale a un produttore illimitato. È ortogonale ais_sequence, non è un asse dell’URN ed è consentito solo sull’input principale (RULE14). L’output ha lo stesso flag (CapOutput::streaming): la cap può aprire uno stream di output illimitato (start_unbounded). Dichiarali entrambi onestamente; l’esecutore verifica ogni STREAM_START emesso rispetto a essi e decide in base a essi se un salto va in pipeline o viene materializzato (15.2 §Contratti di streaming). Un trascrittore che legge un microfono ed emette segmenti man mano dichiarastreaming = truesul suo input e sul suo output; un riassuntore che ha bisogno dell’intero testo non dichiara né l’uno né l’altro.
Sorgenti degli argomenti
ArgSource descrive come un argomento può essere fornito in modalità CLI:
| Variante | Descrizione | Esempio |
|---|---|---|
Stdin { stdin } |
Stream di input principale. Il media URN di stdin identifica il tipo di dati. | Un file PDF passato tramite pipe su stdin. |
Position { position } |
Argomento CLI posizionale (indice a partire da 0). | mycart extract doc.pdf (posizione 0 = percorso del file). |
CliFlag { cli_flag } |
Flag CLI con nome. | mycart generate --model gpt2. |
Sorgente: capdag-rs/src/cap/definition.rs.
Costruzione dei cap URN
Pattern builder (Rust)
CapUrnBuilder costruisce i cap URN in modo programmatico:
use capdag::CapUrnBuilder;
let urn = CapUrnBuilder::new()
.in_urn("media:document;ext=pdf")
.out_urn("media:record")
.tag("op", "extract")
.tag("target", "metadata")
.build()?;
Helper standard
Per le operazioni comuni, gli helper standard in capdag::standard::caps generano il cap URN appropriato:
use capdag::standard::caps::*;
let thumb_urn = generate_thumbnail_urn("media:document;ext=pdf"); // generazione della miniatura
let meta_urn = extract_metadata_urn("media:document;ext=pdf"); // estrazione dei metadati
let outline_urn = extract_outline_urn("media:document;ext=pdf"); // estrazione della struttura
let disbind_urn = disbind_urn("media:document;ext=pdf"); // estrazione delle pagine
Ogni helper riceve un media URN e restituisce la stringa del cap URN canonico per quell’operazione su quel tipo di media.
Costanti cablate nel codice (Swift)
Le cartucce Swift di solito usano costanti stringa cablate nel codice per i cap URN invece dei pattern builder.
Registrazione degli handler
Ogni cap del manifest deve avere un handler corrispondente registrato presso CartridgeRuntime. La stringa del cap URN usata per la registrazione deve corrispondere a ciò che il dispatch risolverà: deve essere is_dispatchable rispetto alle richieste in arrivo per le cap dichiarate nel manifest.
// Funzione factory: crea un nuovo handler per ogni richiesta
runtime.register_op("cap:in=...;out=...;generate", || Box::new(GenerateOp));
// Tipo predefinito: crea tramite Default::default()
runtime.register_op_type::<MetadataOp>("cap:in=...;out=...;extract;target=metadata");
Registrare un handler per un cap URN che non è nel manifest è innocuo ma inutile, poiché per esso non arriverà alcuna richiesta. Non registrare un handler per una cap del manifest significa che le richieste per quella cap riceveranno un errore NoHandler.
Ciclo di vita della cartuccia
stateDiagram-v2
[*] --> Registered : Individuazione
Registered --> Spawning : Prima REQ per questa cap
Spawning --> Handshaking : Processo avviato
Handshaking --> Live : HELLO + identità OK
Handshaking --> Failed : HELLO fallito
Live --> Live : Gestisce le richieste
Live --> Dead : Processo terminato
Live --> Killed : Arresto del motore / timeout di inattività
Dead --> Spawning : La REQ successiva provoca un nuovo avvio
Killed --> Registered : Disponibile per un nuovo avvio
Failed --> [*] : Fallimento permanente
- Individuazione: il registro delle cartucce del motore o la mappatura dev_cartridges identifica quale binario di cartuccia fornisce una data cap. Il registro delle cartucce è versionato: i binari incorporano una
CARTRIDGE_REGISTRY_VERSION(attualmente2, prefisso dell’URL del manifest.../v2/manifest), e le cartucce vengono risolte su disco in{registry_slug}/v{cartridge_registry_version}/{channel}/{name}/{version}/cartridge.json, con la directory di versione fissata alla versione incorporata nell’host come il canale. Ilregistry_slugè una trasformazione, sicura per i percorsi, dell’authority dell’URL del registro (host più:portfacoltativa): convertita in minuscolo, con i caratteri al di fuori di[a-z0-9.-]sostituiti da-, URL nullo →dev, non un hash, quindi tutte le versioni di uno stesso registro condividono uno slug. Un registro viene acquisito solo se ilfabricRegistryUrldel suo manifest corrisponde al registro fabric incorporato nel client, mantenendo le cap confrontabili fra tutti i registri usati da un client (vedi Esecuzione). - Prima richiesta: quando arriva una REQ per una cap fornita da questa cartuccia, il CartridgeHostRuntime avvia il binario senza argomenti (attivando la modalità CBOR della cartuccia).
- Handshake: scambio HELLO e verifica dell’identità (vedi Handshake).
- Gestione delle richieste: il motore invia frame REQ; la cartuccia li smista agli handler registrati.
- Inattività o arresto: la cartuccia può essere terminata dopo un timeout di inattività o quando il motore si arresta.
- Nuovo avvio: la richiesta successiva per le cap di questa cartuccia provoca un nuovo avvio. Le cache dei modelli sono su disco, quindi il ricaricamento è l’unico costo.
Sorgente: host_runtime.rs, executor.rs.