Skip to content

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.

sema add handles PyPI and native Sema packages with one command:

Terminal window
sema add numpy # install a PyPI package into .sema/venv (via uv; pip fallback)
sema list # discovery
sema remove numpy

For 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.

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:

Terminal window
sema add ./greetings # a local Sema package
sema add git+https://example.com/greetings.git

The 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 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 Belief
from std.cache import memoize
from std.document import Report, render
from std.agent_loop import loop_until

Because 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.

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 = 12345
temperature = 0.7
[stream] # long-stream compaction defaults
window = 1000
budget = 4000
[models] # which model backs each capability — swap in your own
embed = "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 | audit
retention_days = 186

Missing 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.

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.tenant
  • args declares the CLI as typed data; the compiler generates parsing, help text, defaults, and completion.
  • config declares 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, a sem descriptor, 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 main

The decorated entry point injects the runtime and uses it:

@LedgerApp
@LedgerOps
def 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.

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 embedder
def my_embed(text: str) -> list[f64] !{proc.run}:
return python.call("sentence_transformers_helper", "encode", [text])
@provides("generate") # override text generation
def my_llm(prompt: str, max_tokens: int) -> str !{net.connect}:
return http.post(endpoints.llm, prompt).text
@provides("ocr") # override OCR
def 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.

From the sema/ directory:

Terminal window
sema add ./greetings # install a native package
sema check examples/finops-ledger
SEMA_STRICT=1 sema run examples/finops-ledger
sema assure examples/finops-ledger --grade gold

sema check sees @provides registrations (they are reflected and type-checked) and flags a misconfigured provider signature before it ever runs.

  • Wrap a remote API as generate. A @provides("generate") with !{net.connect} routes all agent turns through your endpoint.
  • Per-tenant config. Drive config from --tenant + an env prefix so one build serves many tenants — the finops-ledger shape.
  • Swap a model without a redeploy. Change [models] <cap> in sema.toml; no code changes, and the language surface stays identical.