The structure of a cartridge: entry point, manifest, cap definitions, and handler registration.
What is a cartridge
A cartridge is a standalone binary that communicates with the engine via the CapDAG protocol over stdin/stdout. Each cartridge provides one or more capabilities (caps). The engine discovers cartridges, spawns them on demand when their caps are needed, and manages their lifecycle through the CartridgeHostRuntime.
Existing cartridges:
| Name | Language | Framework | Caps |
|---|---|---|---|
| ggufcartridge | Rust | llama.cpp | LLM text generation, vision, embeddings, vocab, model info |
| candlecartridge | Rust | Candle | BERT embeddings, CLIP, BLIP, Whisper |
| mlxcartridge | Swift | MLX | LLM text generation, vision, embeddings |
| pdfcartridge | Rust | — | Thumbnail, metadata, outline, disbind |
| txtcartridge | Rust | — | Thumbnail, metadata, outline for text files |
| modelcartridge | Rust | — | Model downloading and caching |
Entry point
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(())
}
The #[tokio::main] attribute sets up the multi-threaded async runtime. run() detects whether the binary was launched with CLI arguments (CLI mode) or without (CapDAG cartridge mode) and handles both.
Source: 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()
}
}
Swift cartridges use @main for the entry point. run() blocks until stdin closes.
Source: mlxcartridge/Sources/mlxcartridge/main.swift.
Manifest
The manifest tells the engine what this cartridge can do. It is a CapManifest struct serialized to JSON and sent during the HELLO handshake:
let caps = vec![
identity_cap(), // Required: identity verification
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)],
);
Fields:
- name: Cartridge name.
- version: Semver version string.
- description: What this cartridge does.
- cap_groups: List of
CapGroupinstances, each containing a named set ofCapdefinitions and optionaladapter_urns. UseCapGroup::default_group(caps)for a single unnamed group. - author (optional): Maintainer name.
- page_url (optional): Documentation or repository URL.
The cap_groups structure exists to support adapter routing: a group can declare adapter_urns that identify which media adapters are available for that group’s caps. For most cartridges, a single default group with no adapter URNs is correct.
Source: capdag-rs/src/bifaci/manifest.rs, capdag-rs/src/cap/definition.rs.
Identity cap
Every manifest must include the identity cap, identity_cap() in Rust or the equivalent in Swift. This cap handles the identity verification step during handshake (see Handshake). CartridgeRuntime::new() panics if CAP_IDENTITY (cap:effect=none) is missing from the manifest.
The identity handler is registered automatically by the runtime. You only need to include identity_cap() in the manifest’s cap list.
Source: capdag-rs/src/standard/caps.rs.
Cap definition
A Cap defines a single capability:
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 (CLI subcommand name)
);
Fields:
- urn: The
CapUrnidentifying this capability. Defines the input/output media types and operation tags. - title: Human-readable name for display.
- slug: Short name used as the CLI subcommand.
- args: List of
CapArgdefinitions (input arguments). - metadata: Optional key-value pairs (e.g.,
activity_timeout_secsfor custom timeouts).
Source: capdag-rs/src/cap/definition.rs.
CapArg
Each argument a cap accepts is described by a CapArg:
let arg = CapArg::new(
"media:document;ext=pdf", // media URN identifying this argument
true, // required
vec![ArgSource::Stdin { stdin: "media:document;ext=pdf".to_string() }],
);
Fields:
- media_urn: The media URN that identifies this argument in the stream. The handler uses this to find the argument via
find_stream()orrequire_stream(). - required: Whether the argument must be provided.
- sources: How the argument can be provided (for CLI mode).
- default_value (optional): Default value as
serde_json::Value. - description (optional): Human-readable description.
- is_sequence: Cardinality, whether the argument carries a sequence of
items (
emit_list_itemframing) or a single value. - streaming (
.streaming(true)): Boundedness: the argument is consumed without a length promise (a feed); the cap processes items as they arrive and may be wired live to an unbounded producer. Orthogonal tois_sequence, not a URN axis, and allowed on the main input only (RULE14). The output has the same flag (CapOutput::streaming): the cap may open an unbounded output stream (start_unbounded). Declare both honestly; the executor audits every emitted STREAM_START against them and decides from them whether a hop pipelines or materialises (15.2 §Streaming contracts). A transcriber that reads a microphone and emits segments as it goes declaresstreaming = trueon its input and output; a summariser that needs the whole text declares neither.
Argument sources
ArgSource describes how an argument can be provided in CLI mode:
| Variant | Description | Example |
|---|---|---|
Stdin { stdin } |
Primary input stream. The stdin media URN identifies the data type. | A PDF file piped via stdin. |
Position { position } |
Positional CLI argument (0-based index). | mycart extract doc.pdf (position 0 = file path). |
CliFlag { cli_flag } |
Named CLI flag. | mycart generate --model gpt2. |
Source: capdag-rs/src/cap/definition.rs.
Cap URN construction
Builder pattern (Rust)
CapUrnBuilder constructs cap URNs programmatically:
use capdag::CapUrnBuilder;
let urn = CapUrnBuilder::new()
.in_urn("media:document;ext=pdf")
.out_urn("media:record")
.tag("op", "extract")
.tag("target", "metadata")
.build()?;
Standard helpers
For common operations, standard helpers in capdag::standard::caps generate the appropriate cap URN:
use capdag::standard::caps::*;
let thumb_urn = generate_thumbnail_urn("media:document;ext=pdf"); // thumbnail generation
let meta_urn = extract_metadata_urn("media:document;ext=pdf"); // metadata extraction
let outline_urn = extract_outline_urn("media:document;ext=pdf"); // outline extraction
let disbind_urn = disbind_urn("media:document;ext=pdf"); // page extraction
Each helper takes a media URN and returns the canonical cap URN string for that operation on that media type.
Hardcoded constants (Swift)
Swift cartridges typically use hardcoded string constants for cap URNs rather than builder patterns.
Handler registration
Each cap in the manifest must have a corresponding handler registered with CartridgeRuntime. The cap URN string used for registration must match what dispatch will resolve: it must be is_dispatchable against incoming requests for the caps declared in the manifest.
// Factory function — creates new handler per request
runtime.register_op("cap:in=...;out=...;generate", || Box::new(GenerateOp));
// Default type — creates via Default::default()
runtime.register_op_type::<MetadataOp>("cap:in=...;out=...;extract;target=metadata");
Registering a handler for a cap URN that is not in the manifest is harmless but useless, since no request will arrive for it. Failing to register a handler for a cap in the manifest means requests for that cap will get a NoHandler error.
Cartridge lifecycle
stateDiagram-v2
[*] --> Registered : Discovery
Registered --> Spawning : First REQ for this cap
Spawning --> Handshaking : Process started
Handshaking --> Live : HELLO + identity OK
Handshaking --> Failed : HELLO failed
Live --> Live : Handle requests
Live --> Dead : Process died
Live --> Killed : Engine shutdown / idle timeout
Dead --> Spawning : Next REQ triggers respawn
Killed --> Registered : Available for respawn
Failed --> [*] : Permanent failure
- Discovery: The engine’s cartridge registry or dev_cartridges mapping identifies which cartridge binary provides a given cap. The cartridge registry is versioned: binaries bake a
CARTRIDGE_REGISTRY_VERSION(currently2, manifest URL prefix.../v2/manifest), and cartridges resolve on disk at{registry_slug}/v{cartridge_registry_version}/{channel}/{name}/{version}/cartridge.json, with the version directory pinned to the host’s baked version like the channel. Theregistry_slugis a path-safe transform of the registry URL’s authority (host plus optional:port): lowercased, non-[a-z0-9.-]characters replaced by-, null URL →dev, not a hash, so all versions of one registry share a slug. A registry is ingested only if its manifest’sfabricRegistryUrlmatches the client’s baked fabric registry, keeping caps comparable across every registry a client uses (see Execution). - First request: When a REQ arrives for a cap this cartridge provides, the CartridgeHostRuntime spawns the binary with no arguments (triggering cartridge CBOR mode).
- Handshake: HELLO exchange and identity verification (see Handshake).
- Request handling: The engine sends REQ frames; the cartridge dispatches them to registered handlers.
- Idle or shutdown: The cartridge may be killed after an idle timeout or when the engine shuts down.
- Respawn: The next request for this cartridge’s caps triggers a fresh spawn. Model caches are on disk, so reloading is the only cost.
Source: host_runtime.rs, executor.rs.