In questa sezione

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 definizioni Cap e adapter_urns facoltativi. Usa CapGroup::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 CapUrn che 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_secs per 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() o require_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 a is_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 dichiara streaming = true sul 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
  1. 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 (attualmente 2, 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. Il registry_slug è una trasformazione, sicura per i percorsi, dell’authority dell’URL del registro (host più :port facoltativa): 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 il fabricRegistryUrl del suo manifest corrisponde al registro fabric incorporato nel client, mantenendo le cap confrontabili fra tutti i registri usati da un client (vedi Esecuzione).
  2. 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).
  3. Handshake: scambio HELLO e verifica dell’identità (vedi Handshake).
  4. Gestione delle richieste: il motore invia frame REQ; la cartuccia li smista agli handler registrati.
  5. Inattività o arresto: la cartuccia può essere terminata dopo un timeout di inattività o quando il motore si arresta.
  6. 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.