ADR-0001: Rust kernel + Python surface (split stack)¶
- Status: Accepted
- Date: 2026-04-12
- Supersedes: —
- Superseded by: —
Context¶
Trails needs two things at odds with each other:
- Correctness + performance in the hot path — SHACL validation, SPARQL execution, reasoning, policy evaluation, provenance writes. This path is touched on every capability invocation.
- Rapid iteration on DX — decorators, generators, conventions, CLI. The Rails-move is about patterns, not perf.
Options considered:
- Pure Rust (
axum/actix-style framework). Best perf; worst DX iteration cost. Every generator and decorator experiment costs 3–5× a Python equivalent. Macro-heavy DSLs to emulate Rails ergonomics get gnarly (see Axum extractor debates). - Pure Python (FastAPI-based). Best DX; acceptable perf. Triple-store and SHACL libraries exist (
rdflib,pyshacl,owlready2) but hot-path performance is 10–100× Rust-native Oxigraph. Validation on large payloads stalls requests. - Pure TypeScript (Next.js + Oxigraph-wasm + MCP-native). Best MCP ergonomics; weak semantic-web libraries —
pyshaclhas no TS equivalent with equivalent maturity. Would require reimplementing significant chunks. - Split stack: Rust kernel + Python surface via PyO3. This is the pattern used by Ruff, uv, Polars, Pydantic-core, Turbopack. Rust for the engine, ergonomic language for the steering wheel.
Decision¶
Trails is built as a Rust kernel (trails-core) + Python surface (trails) bridged by PyO3.
- Kernel owns: graph store, SHACL validator, reasoner, policy engine, provenance writer, identity, cost accountant, capability registry.
- Surface owns: decorators, MCP server, HTTP adapter, CLI, generators, bi-modal rendering, testing affordances.
- FFI boundary: PyO3 with abi3 ABI; async-capable via
pyo3-async-runtimes.
Python was chosen over TypeScript for the surface because: - The semantic-web ecosystem is strongest in Python (rdflib, pyshacl, owlready2). The kernel reduces Python dependency for perf-critical paths but Python tooling remains useful for migrations, ontology work, and testing. - The agent-SDK ecosystem is also strongest in Python (Anthropic, OpenAI, LangGraph, CrewAI). Trails' users are the same people using these tools. - The author's existing stack is Python-dominant — friction-free dogfooding.
A TypeScript surface (@trails/core via NAPI) is explicitly planned for v1.5 but is not on the critical path.
Consequences¶
Positive¶
- Correctness + performance in the hot path.
- DX iteration speed for framework-shape experiments (new decorators, new CLI commands).
- Matches a successful pattern users already trust.
- Single binary deploys possible (kernel as static lib).
- Python ecosystem available for migrations and testing.
Negative¶
- Two build systems (Cargo + Python packaging). Mitigated by
maturinfor unified builds. - FFI debugging complexity when errors cross the boundary. Mitigated by structured error types with IRI context, OTLP traces spanning both sides.
- Release coordination — kernel ABI changes force surface re-release. Mitigated by abi3 Python ABI, semver on kernel traits.
- Contributor cost — requires Rust + Python knowledge. Mitigated by clear surface/kernel split; most contributions land on one side.
Revisit conditions¶
- If FFI ceremony during M0 PoC feels prohibitive, revisit: fall back to pure-Python surface with
rdflibfor kernel functions, accepting performance hit. - If a compelling agent-native TS framework emerges with enough semweb tooling, consider inverting (TS primary, Python secondary).