Skip to content

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:

  1. Correctness + performance in the hot path — SHACL validation, SPARQL execution, reasoning, policy evaluation, provenance writes. This path is touched on every capability invocation.
  2. 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 — pyshacl has 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 maturin for 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 rdflib for kernel functions, accepting performance hit.
  • If a compelling agent-native TS framework emerges with enough semweb tooling, consider inverting (TS primary, Python secondary).