Build with the local Soterias SDK. Get started with Python and TypeScript.
SoteriasDocsBuild
Core concepts

Learned procedures

Matching pilot versions: Python installation package 0.1.4, native 0.1.0rc5, TypeScript and MCP 0.1.0-rc.5.

Soterias keeps a reusable procedure from actual verified executions. A procedure contains typed input slots, a bounded graph, tool/environment assumptions and an separately supplied trusted verifier. Three distinct training inputs, a separate held-out shadow input and a separate canary input must succeed before compiled execution. A claimed success flag or a compiled hit cannot train its own qualification.

The recipe is optimization data. A secured run uses the same Rust host as normal SDK calls: current identity, intent, warrant, tool review, policy, budgets, operation-specific signed approval and consumed broker admission are checked again. READ and EFFECT outputs/approvals are never cached. An uncertain operation ends the run; the graph does not automatically retry it. Task16 recovery remains a separate open acceptance gate.

Python

The full runnable program and readable recipes are in examples/tasks/42-48-procedures. The contributor preparation command makes fresh disposable local UNIT authorities and synthetic engagement evidence. Client programs then run without Cargo, a Soterias CLI, Docker, a database service, or a model server.

Python
from pathlib import Path
from soterias import Procedure, ProcedureMemory
from plans import make_graph, make_verifier

# Trusted application setup supplies an existing configured native host,
# current assumptions, and original action DATA for its fixed broker routes.
procedure = Procedure(make_graph("recalculation", environment),
                      verifier=make_verifier("recalculation"))
memory = ProcedureMemory.create(Path("./private-procedure-memory").resolve())
run = procedure.start(inputs, environment=environment)
state = run.advance(host, environment=environment)

if state == "awaiting_approval":
    # Display run.pending_request on the trusted reviewer surface. The separate
    # TrustedOperationReviewer signs the exact operation after confirmation.
    # Never give its signing key to the agent or treat a model answer as approval.
    state = run.advance(host, environment=environment, approval=signed_approval)

if state == "verified":
    print(run.result)
    print(run.report)       # actual wall/active timing; provider usage or unknown
    original_receipts = run.receipts
    memory.observe(run, phase="training")

# After three distinct training runs and separate verified shadow/canary runs:
qualified = memory.qualify(procedure, environment=environment)
repeat = qualified.start(new_inputs, environment=environment)
state = repeat.advance(host, environment=environment)

This excerpt shows the API; the full program implements both approval pauses, public-only signature verification and independent business-result checks. It does not silently create an enrolled production host. advance_async uses the existing owning executor worker; cancellation does not establish rollback.

Start with learn.py for a complete, self-contained Python example. Run python learn.py with the matching SDK. It needs no configuration, model or account, prints 7200, and records one reused pure expression after three training, one held-out and one canary execution. This ordinary mode provides no access restrictions. Each memory directory is new and private; the program prints its path for inspection or later removal.

The graph's own VERIFY node cannot qualify its result. The application supplies verifier= separately; Rust pins that rule's digest in every qualification record. For protected workflows it receives actual broker operations, the proposed write and the final result. It checks the workpaper before write admission, then checks the actual write and completed result. Changing the rule invalidates prior qualification. Keep trusted rule selection outside agent input.

Workflows

Workflow Actual evidence and calculation Protected mutation
Transaction testing Sample an authorized engagement JSON document; compare invoice/PO amounts, currency, references and delivery evidence; record mismatches as valid findings Separately approved append to the configured SQLite workpaper
Recalculation Read authorized asset cost, salvage and useful life; compute exact minor-unit straight-line depreciation; independently verify the floor-division invariant Separately approved result append
Control testing Inspect approval evidence and payment limits; identify operating failures and preserve the evidence ID Append only to the configured workpaper after separate signed approval

The source is a real host-selected private file with a committed content hash and byte range. A request cannot supply a filesystem path or select another document. The example does not parse PDF/Office files, prove source facts, or implement a full audit methodology. The rounding convention is explicit floor division for the sample; a real accounting policy must define residual/rounding treatment. The current fixed workpaper target is SQLite, not XLSX.

TypeScript and MCP

TypeScript uses Procedure, ProcedureMemory, run.advanceAsync(host, options) and the same Rust-authored graph schema. The example's workflows.mts is strictly compiled; workflows.mjs is its runnable output. The Python and TypeScript programs load the same data recipes and check the same findings.

@soterias/mcp-server/procedures exposes a host-selected procedure through authenticated MCP. The agent receives start_procedure, resume_procedure, supply_procedure_model, get_procedure_outcome and get_procedure_receipt. It cannot submit another graph, a host configuration or an approval boolean. Use the existing authenticated HTTP host and stdio proxy with Claude Code or Hermes. Compatibility requires actual named-client tests; neither a tool listing nor a successful connection proves a complete workflow.

Learning and measurement limits

The compiler normalizes a bounded DAG with lexical tie order. It rejects cycles, missing/unreachable nodes and malformed closed inputs. Pure nodes run only the closed Rust value operators; a tool callback cannot be relabelled pure. Actual reviewed tool categories prevent a READ node from dispatching a write. An EFFECT requires an earlier verifier. POLICY nodes add application preconditions; they do not replace the current broker policy evaluation. APPROVAL nodes are ordering fences; the protected operation performs the real signed-approval check. CHECKPOINT nodes capture the acknowledged pin of the already durable Rust host custody journal. They do not implement crash-resumable graph replay.

Compiled mode can reuse repeated pure expressions within the new run. A MODEL boundary whose output is identical across all qualifying trajectories can reuse that learned plan/result; every downstream guard and verifier still executes. Other MODEL nodes pause for a fresh provider result. Guided mode keeps model boundaries. Typed parameter slots are explicit; arbitrary automatic extraction of plans from unrelated free-form agent transcripts is not implemented by this slice.

Timing uses Rust's monotonic clock. Reports distinguish total elapsed time (including review/provider waits), active interpreter time, actual broker attempts/target entries, reused pure nodes and learned model hits. Model call counts mean application-reported provider completions. Tokens are explicitly provider-reported when supplied, otherwise unknown. A deterministic test fixture is not evidence of real model savings. The accounting examples invoke no model, so they correctly report zero model calls/tokens; no model/token or latency speedup is claimed without a measured equivalent baseline.

The private bounded SQLite memory stores graph/input/result/receipt commitments and observed model DATA. It is local optimization metadata, not signed authority or tamper-proof measurement truth. Hostile code running with the same OS/process privileges is not isolated by this embedded SDK. Broader original build tasks, production enrollment, external connector tests and platform/security gates stay open until their own evidence exists.

Complete Python program

Save as learn.py, then run python learn.py:

Python
"""Learn a small verified procedure. No model, account, broker or server required."""
from pathlib import Path
from tempfile import mkdtemp
from soterias import Procedure, ProcedureMemory

def value(x):
    return {"op": "literal", "value": x}

def input_at(name, pointer=""):
    return {"op": "input", "name": name, "pointer": pointer}

def multiply(left, right):
    return {"op": "multiply", "left": left, "right": right}

environment = dict(policy="1" * 64, tools="2" * 64,
                   credentials="3" * 64, evidence="4" * 64,
                   host_configuration=None)
calculation = multiply(input_at("quantity"), input_at("price"))
graph = {
    "version": 1, "id": "line-total", "environment": environment,
    "inputs": {"quantity": "integer", "price": "integer"},
    "terminal": "result", "nodes": [
        {"id": name, "after": [], "output": "integer",
         "step": {"kind": "pure", "value": calculation}}
        for name in ("calculate", "duplicate")
    ] + [{"id": "result", "after": ["calculate", "duplicate"],
          "output": "integer", "step": {"kind": "verify",
          "condition": value(True),
          "value": {"op": "node", "id": "calculate", "pointer": ""}}}],
}
# The application supplies this separately from the candidate graph.
# Rust builds its context from actual inputs and the completed result.
verifier = {"op": "equal", "left": input_at("context", "/result"),
            "right": multiply(input_at("context", "/inputs/quantity"),
                              input_at("context", "/inputs/price"))}
procedure = Procedure(graph, verifier=verifier)
directory = Path(mkdtemp(prefix="soterias-learn-")) / "memory"
memory = ProcedureMemory.create(directory)
for index in range(5):
    run = procedure.start({"quantity": str(index + 1), "price": "1200"},
                          environment=environment)
    try:
        assert run.advance_pure(environment=environment) == "verified"
        memory.observe(run, phase="training" if index < 3 else
                       "shadow" if index == 3 else "canary")
    finally:
        run.close()
qualified = ProcedureMemory.open(directory).qualify(procedure, environment=environment)
run = qualified.start({"quantity": "6", "price": "1200"}, environment=environment)
try:
    assert run.advance_pure(environment=environment) == "verified"
    assert run.result == "7200"
    assert run.report["independently_verified"] is True
    assert sum(node["reused_pure"] for node in run.report["trace"]) == 1
    print(run.result)
    print(run.report)
    print("Memory:", directory)
finally:
    run.close()

Search guides, examples and API reference.