Packaging & Providers
A language is only as usable as its ecosystem and its defaults. Sema ships a package manager that speaks both ecosystems, native Sema packages, a declarative config layer, first-class dependency injection, and a seam to override any model backend — all so a project works out of the box and then bends to whatever you need.
This guide walks the packaging and configuration surface, with the DI example
drawn from the finops-ledger project.
See also Project Layout for where these files live.
Installing packages
Section titled “Installing packages”sema add handles PyPI and native Sema packages with one command:
sema add numpy # install a PyPI package into .sema/venv (via uv; pip fallback)sema list # discoverysema remove numpyFor PyPI, sema add creates a project-local environment (.sema/venv), installs
with uv (the fast pip) or pip, records the package in .sema/packages.txt,
and points sema.toml [python] bin at the env — so the package is immediately
usable from Sema via python.import(...) (see
Python Interop), classes and methods included. The
generic adaptation layer avoids per-package bindings, but compatibility across
native extensions, ABIs, worker protocols, and platforms is not universal.
Native Sema packages
Section titled “Native Sema packages”A native Sema package is a directory with a sema-pkg.toml ([package] name = …)
plus src/*.sema. Install it from a local path or a git URL:
sema add ./greetings # a local Sema packagesema add git+https://example.com/greetings.gitThe loader resolves imports from installed packages, so their modules are used
like any other — from greetings.greet import hello. Installed packages live in
.sema/packages/<name>/ (gitignored, reproducible), and a project module
overrides a package module of the same name.
The standard library is written in Sema
Section titled “The standard library is written in Sema”The std package ships embedded in the compiler — its source lives in
stdlib/sema/*.sema — and is available to every project with no installation:
from std.belief import Belieffrom std.cache import memoizefrom std.document import Report, renderfrom std.agent_loop import loop_untilBecause these neurosymbolic components are expressed in Sema, not hardcoded in Rust, their parameters and logic are changeable in the language, and a user module of the same stem shadows the stdlib one.
The config layer: sema.toml
Section titled “The config layer: sema.toml”An optional sema.toml at the project root overrides defaults without touching
code, and every value is readable from a program via config.get /
config.model / config.temperature. Embedding-driven ops (~=) run on a
built-in engine even unconfigured — the hash embedder. Semantic and generative
ops use a real local GGUF model when the real-model backend is linked and
configured; without a backend they fail with a typed error unless
[engine] deterministic = true opts into the hermetic deterministic engine.
[engine]seed = 12345temperature = 0.7
[stream] # long-stream compaction defaultswindow = 1000budget = 4000
[models] # which model backs each capability — swap in your ownembed = "my-custom-embedder"vision = "siglip2-base"generate = "models/qwen3-0.6b.Q4_K_M.gguf"
[journal] # audit-trail preset; default "standard"level = "audit" # off | minimal | standard | auditretention_days = 186Missing file means all defaults; present keys override; an unspecified capability keeps its smart default. This is the single place to change model-specific traits, register custom models, and adjust compaction behaviour — flexible when you need it, invisible when you don’t.
Typed config and dependency injection
Section titled “Typed config and dependency injection”For real applications, configuration is typed program data, not a bag of strings.
Sema has args (a typed CLI), config (a validated config tree with ordered
sources), and a lexical dependency container. finops-ledger wires all three:
args LedgerCli: config: str = option("--config", default="config/ledger.yaml") tenant: str = option("--tenant") dry_run: bool = flag("--dry-run") overrides: list[ConfigPatch] = option("--set")
config LedgerConfig: source yaml LedgerCli.config source env prefix "FINOPS_" source cli LedgerCli.overrides tenant: str sem "Tenant id used for ledger reads and regulatory routing" dry_run: bool = LedgerCli.dry_run paths: inbound_dir: str = "inbound/statements" ledger_snapshot: str = "state/ledger/current.json" models: anomaly_writer: temperature: f32 = 0.1 where 0.0 <= value <= 2.0 top_p: f32 = 0.9 where 0.0 < value <= 1.0 max_tokens: int = 2048 where value > 0 require tenant == LedgerCli.tenantargsdeclares the CLI as typed data; the compiler generates parsing, help text, defaults, and completion.configdeclares a typed tree with ordered sources — defaults are lowest precedence, then file sources, then env and CLI at declared paths. Each leaf has a type, asemdescriptor, and validation (where …); a value that fails validation never enters the graph.
The container binds components to a lifetime and injects them:
component LedgerRuntime: sem "Scoped dependency object for one reconciliation invocation" lifetime scoped(run) inject: cfg: LedgerConfig writer: ModelClient named "anomaly_writer" def statement_files() -> list[str] !{fs.read}: return []
provide configured_anomaly_writer(cfg: LedgerConfig) -> ModelClient lifetime singleton: sem "Attach validated sampling config to the pinned anomaly writer model" return anomaly_writer.with(cfg.models.anomaly_writer)
container LedgerApp: args LedgerCli config LedgerConfig bind ModelClient named "anomaly_writer" = configured_anomaly_writer(LedgerConfig) bind LedgerRuntime lifetime scoped(run) expose mainThe decorated entry point injects the runtime and uses it:
@LedgerApp@LedgerOpsdef main() -> None !{fs.read, fs.write, ffi.call, model.invoke, model.embed, net.connect, code.patch, observe.record}: runtime = inject LedgerRuntime batches = runtime.statement_files() for statement_path in batches: summary = run_reconciliation_batch(statement_path, runtime.cfg.paths.ledger_snapshot) log.info("reconciliation batch complete", lines=summary.statement_lines)container is a lexical dependency graph, not a global service locator: every
inject must resolve to exactly one binding by (type, qualifier), ambiguity is
a compile error unless disambiguated with named "…", and a missing provider is a
compile error for a static entrypoint. Model configuration flows through the same
machinery — writer.with(cfg.model) turns sampling knobs into validated program
data instead of a long parameter list.
Custom capability providers
Section titled “Custom capability providers”Every model capability — generate, embed, transcribe, caption, ocr,
vqa, and the SQL db backend — is a seam the runtime resolves in a fixed
precedence: (1) a user-registered Sema provider, (2) the native candle backend,
(3) the built-in default. A provider is any Sema function tagged
@provides("<cap>") with the capability’s signature — so you can wrap anything
without reimplementing a model in Rust:
import python
@provides("embed") # override the embedderdef my_embed(text: str) -> list[f64] !{proc.run}: return python.call("sentence_transformers_helper", "encode", [text])
@provides("generate") # override text generationdef my_llm(prompt: str, max_tokens: int) -> str !{net.connect}: return http.post(endpoints.llm, prompt).text
@provides("ocr") # override OCRdef my_ocr(path: str) -> str !{ffi.call}: return tesseract.read(path)Registration is by decorator, scanned at load time, so it is visible to
sema check and reflection. Expected signatures include embed(text) -> list[f64],
generate(prompt, max_tokens) -> str, ocr(image) -> str, and the SQL backend
db(op, sql, params) -> any. The override is transparent to callers — ~= and
semantic.* route through your custom embed, tool calling through your custom
generate, every db.* call through your backend.
Run and verify
Section titled “Run and verify”From the sema/ directory:
sema add ./greetings # install a native packagesema check examples/finops-ledgerSEMA_STRICT=1 sema run examples/finops-ledgersema assure examples/finops-ledger --grade goldsema check sees @provides registrations (they are reflected and type-checked)
and flags a misconfigured provider signature before it ever runs.
Variations
Section titled “Variations”- Wrap a remote API as
generate. A@provides("generate")with!{net.connect}routes all agent turns through your endpoint. - Per-tenant config. Drive
configfrom--tenant+ an env prefix so one build serves many tenants — thefinops-ledgershape. - Swap a model without a redeploy. Change
[models] <cap>insema.toml; no code changes, and the language surface stays identical.