Skip to content
docs
Arxo ↗

Python

For LLMs10 sections

The facade is arxo at version 0.2.0 with the canon package arxo-canon-bgb-fristen at 0.1.1, which carries the canon de.bgb.fristen at 0.1.0. You need Python 3.12 or newer. Execution is local through the bundled WebAssembly engine; this release has no serve mode, no registry fetch, and no remote execution, so the only call that reaches a host is explain — and a package opened offline refuses even that.

Two routes, different promises:

Terminal
# Reproduce the pinned example: the versions in the compatibility record.
pip install arxo==0.2.0 arxo-canon-bgb-fristen==0.1.1
Terminal
# Install current versions: newest compatible pair, not the pinned one.
pip install arxo arxo-canon-bgb-fristen

The first route installs the pair listed on the compatibility page. The second may install newer releases; re-run the probe on the compatibility page when a version changes.

The frist scenario from the section map, checked with truth:

Python
from arxo import open
fristen = open("de.bgb.fristen@0.1.0")
r = fristen.truth("frist_ende", ["frist", "2026-03-20"], {
"legalTime": "2026-09-17",
"timezone": "Europe/Berlin",
"deadlinePolicy": "urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG",
"answers": [
{"predicate": "frist_ereignis", "args": ["frist", "2026-03-06"]},
{"predicate": "frist_dauer_tage", "args": ["frist", "14 calendar_day"]},
],
})
r.evaluationStatus # "COMPUTED"
r.truthStatus # "TRUE_ONLY"
r.sources # [] on this canon build
r.hashes # {"program": ..., "semantic": ..., "result": ...}

Remove frist_dauer_tage and the answer is COMPUTED with NEITHER; why_not over the partial case returns two blockers, one per candidate rule, each with the premises it was waiting for.

The rest of the page is the detail behind this call: open and how it resolves a spec, the LawPackage methods, the Answer fields, the errors, and the operations Python does not ship.

Python
def open(spec: str, options: dict[str, Any] | None = None) -> LawPackage:
Python
from arxo import open
fristen = open("de.bgb.fristen@0.1.0")

The spec always carries a version; a versionless spec is refused before anything is resolved:

Python
def parse_spec(spec: str | None) -> dict[str, str]:
Python
parse_spec("de.bgb.fristen@0.1.0") # {"name": "de.bgb.fristen", "version": "0.1.0"}
parse_spec("de.bgb.fristen") # PackageNotFoundError: open() requires name@version

Resolution order is an installed canon package, then a checkout tree passed as local, then the disk cache. When none of the three sources has the package, open raises PackageNotFoundError (“registry fetch is not in this release”). The loading chapter details each source and the hash check.

There is no serve mode in this release: open always returns a LawPackage with a locally loaded engine. (TypeScript has serve and router paths that send questions over the network; Python does not ship them.)

Methods are wrappers over ask; they do not evaluate twice. The snake_case names are canonical; whyNot is kept as an alias for the JavaScript name.

Python
class LawPackage:
def passport(self) -> Passport: ...
def explain(self, answer: Any, node_id: str | None = None) -> dict[str, Any]: ...
def ask(self, request: dict[str, Any]) -> Answer: ...
def truth(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ...
def why_not(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ...
def whyNot(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ...
def collect(self, predicate: str, args: Any, case_input: dict[str, Any]) -> Answer: ...
def deadline(self, op_args: dict[str, Any], case_input: dict[str, Any]) -> Answer: ...
def calc(self, term: Any, case_input: dict[str, Any]) -> Answer: ...
def positions(self, case_input: dict[str, Any]) -> Answer: ...

ask takes a request dict with kind and caseInput:

Python
fristen.ask({
"kind": "truth",
"predicate": "frist_ende",
"args": ["frist", "2026-03-20"],
"caseInput": case_input,
})

Supported kinds are truth, why_not, collect, calendar_op, term, and positions; anything else raises TypeError. collect names the free position with ?name in the argument list, or takes an explicit free (and optionally a comprehension) when args is a dict. deadline sends {"op": "deadline", **op_args} as a calendar_op; calc sends the term as a term.

Python
@dataclass
class Answer:
evaluationStatus: str
sources: Any
proof: Any
hashes: dict[str, str]
issues: Any
document: bytes
via: Literal["local", "mcp"]
truthStatus: Any = None
value: Any = None
# Blocker graph of a `why_not` answer; None on other kinds.
whyNot: Any = None
whyNotTermErrors: Any = None
missingInputs: Any = None
positions: Any = None

document is the canonical bytes of the evaluation document — what you archive or hand to a reviewer. hashes carries program, semantic, and result. The blocker graph of a why_not answer is read from the value map into whyNot; on other kinds it is None.

Python
fristen.passport().in_model("frist_ende") # True
fristen.passport().inModel("frist_ende") # True, JavaScript name

in_model says whether the relation exists in the shipped world at all — “not in the model” is a different answer from “the law is silent”. explain calls the host’s law_explain over the document the answer carries and returns its text with via: "mcp". It needs the host: an offline package raises TransportError, and an answer without a proof graph is refused.

Python
class LawClientError(Exception): ... # common ancestor of transport failures
class IntegrityError(LawClientError): ...
class ProtocolError(LawClientError): ...
class RpcError(LawClientError): ...
class TransportError(LawClientError): ...
class ValidationError(LawClientError): ...
class FactError(Exception): ... # path, nearest
class PackageNotFoundError(Exception): ... # package_name, version

Four different situations, three different treatments:

  • Absent fact: a well-formed fact simply not sent. No exception: the answer is a value, COMPUTED with NEITHER (removing frist_dauer_tage from the first call shows it).
  • Malformed fact: wrong shape or arity (frist_ereignis: expected 2 argument(s), got 0), an object without kind, an unsupported value, a non-object case, a missing legalTime. Raises FactError with the failing path.
  • Unknown relation: a predicate the canon never declared (unknown relation "no_such_relation"). Raises FactError.
  • Missing package: no source has the pinned canon. Raises PackageNotFoundError naming the package and version.

A fifth case belongs to the application, not the SDK: a required form field the user left empty. The example refuses that with its own validation error before any fact is built — the engine never sees it. The results chapter tables every class with the condition that raises it.

Python has no questions, no unfold, no focused_truth, no verbalize, no serve mode, no router, and no registry fetch. The workaround for the missing manifest is to read the inventory at author time from the TypeScript side — questions() on the same pinned canon returns the facts, questions, parameters, and labels a form or a picker is built from — or to read the canon’s shipped index directly. Author-time means the manifest never travels with the Python answer; it is read once while the form is written, and the running code sends only facts the manifest already described.

  • The evaluator is the bundled law_wasm_core.wasm, hosted through wasmtime; pass wasmPath in the options to load another build.
  • The calendar comes from the installed canon when it ships one, else from the calendar option.
  • A case already lowered (assertions with kind: 'assertion') is passed through; otherwise the facade assembles the case from answers and wraps each bare value by its declared type.
  • The only network call in this release is explain through the MCP client made on first use — unless the package was opened offline, which raises TransportError instead. Queries (truth, why_not, collect, deadline, calc, positions) never leave the process: there is no serve path to send them through.

Back to the SDK map. For an application, read on in this order: loading and pinning for reproducible versions, facts and context for the call shape, and results and errors for the answer shape.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.