reasoninglayer 0.2.1

Rust client SDK for the Reasoning Layer API
Documentation

reasoninglayer

Rust client SDK for the Reasoning Layer API. Fully typed DTOs for the core resource surface, with HTTP, WebSocket (auto-reconnect, exponential backoff, 10 attempts, 30 s cap), and SSE transports built in. See [reasoninglayer::ws] and [reasoninglayer::sse].

Install

[dependencies]
reasoninglayer = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

TLS backend: rustls by default. Switch with features:

reasoninglayer = { version = "0.1", default-features = false, features = ["native-tls"] }

Rust toolchain

This crate tracks current stable Rust and does not maintain a fixed MSRV. The Rust async ecosystem (tokio, reqwest, the idnaicu_* chain) bumps minimum Rust versions in patch releases, so a fixed MSRV would either be a claim we can't keep, or would force us to lag dep upgrades and miss bug/security fixes. CI runs against the latest stable Rust on every push. If you have a non-current toolchain constraint, open an issue.

Quick start

use std::collections::BTreeMap;
use reasoninglayer::{
    ClientConfig, CreateSortRequest, CreateTermRequest, ReasoningLayerClient, Value,
};

#[tokio::main]
async fn main() -> Result<(), reasoninglayer::Error> {
    let client = ReasoningLayerClient::new(
        ClientConfig::new("http://localhost:8083", "your-tenant-uuid"),
    )?;

    // Create a sort.
    let person = client.sorts().create_sort(
        CreateSortRequest::with_name("person"),
        None,
    ).await?;

    // Create a term of that sort with typed feature values.
    let mut features = BTreeMap::new();
    features.insert("name".into(), Value::string("Alice"));
    features.insert("salary".into(), Value::integer(95_000_i64));

    let resp = client.terms().create_term(
        CreateTermRequest {
            sort_id: person.id.clone(),
            owner_id: "00000000-0000-0000-0000-000000000000".into(),
            features,
        },
        None,
    ).await?;

    println!("created term {}", resp.term.id);
    Ok(())
}

Configuration

Field Type Default Notes
base_url String Required. Trailing / stripped.
tenant_id String Required. Sent as X-Tenant-Id; not overridable per call.
user_id Option<String> None X-User-Id; overridable per call.
namespace_id Option<String> None X-Namespace-Id; overridable per call.
authenticated_user Option<String> None X-Authenticated-User.
bearer_token Option<String> None Authorization: Bearer ….
timeout Duration 30 s Per-call override via RequestOptions.
max_retries u32 3 Per-call override via RequestOptions.
retry_on_503 bool false Retry on Service Unavailable.

Every request also sends X-SDK-Version and X-SDK-Language: rust so the backend can identify the calling SDK.

Per-call options

use std::time::Duration;
use reasoninglayer::RequestOptions;

let opts = RequestOptions::new()
    .with_user_id("admin-user")
    .with_timeout(Duration::from_secs(60))
    .with_retries(0);

client.sorts().list_sorts(Some(&opts)).await?;

Cancellation: drop the future (for example with tokio::select!) to abort the request in flight.

Two serialization formats

The Reasoning Layer API uses two serialization formats depending on the endpoint:

Endpoint family Format Types
Term CRUD, query, fuzzy Tagged ValueDto {"type": "String", "value": "hello"}
Inference (rules, facts, chaining) Untagged TermInputDto Raw "hello" / 42 / null / {name: "?X"}

Use the right type for the endpoint:

  • Term CRUD: build BTreeMap<String, ValueDto> via Value::string(...), Value::integer(...), or .into() conversions from plain Rust values.
  • Inference: build TermInputDto values via the inference builders re-exported from the crate root (see the [builders] module on docs.rs for the full list).

Errors

All fallible operations return Result<T, reasoninglayer::Error>. HTTP errors are Error::Api wrapping an ApiError with a kind discriminant:

use reasoninglayer::{Error, ApiErrorKind};

match client.terms().create_term(request, None).await {
    Ok(resp) => println!("created {}", resp.term.id),
    Err(Error::Api(e)) if e.kind == ApiErrorKind::ConstraintViolation => {
        if let Some(details) = e.constraint_violation() {
            eprintln!("feature {:?} failed constraint {:?}",
                details.feature, details.constraint);
        }
    }
    Err(Error::Api(e)) if e.kind == ApiErrorKind::RateLimit => {
        if let Some(rl) = e.rate_limit() {
            eprintln!("rate limited, retry after {:?}s", rl.retry_after);
        }
    }
    Err(Error::Timeout { timeout }) => eprintln!("timed out after {timeout:?}"),
    Err(e) => eprintln!("other: {e}"),
}

Automatic retry: HTTP 429 is retried with exponential backoff (honouring Retry-After). 503 is retried only when retry_on_503 is set. Transient network errors retry up to max_retries times.

Resource surface

Core resources with fully typed request/response DTOs:

Accessor Alias Highlights
client.sorts() client.types() 28 methods — CRUD, GLB/LUB, hierarchy queries, similarity learning
client.terms() client.records() 9 methods — CRUD, bulk, exists, clear
client.inference() client.rules() 20 methods — rules/facts, backward/forward/tagged, fuzzy, Bayesian, NAF, goals
client.query() find_unifiable, find_by_sort, osf_search, validate_term, validated_unify, nl_query

Additional resources with typed DTOs: health, admin, osfql, context, rl_training, rag, generation, image_extraction, extract, preferences, discovery, functions, ontology, analysis, scenarios, spaces, row, sources, ui, optimize, fuzzy, causal, statistical, communities, visualization, constraints, namespaces, collections, reasoning.

Resources currently exposing serde_json::Value (typed DTOs landing in patch releases): control, reviews, action_reviews, webhook_actions, ilp, cdl, synthetic, utilities, ingestion, execution, proof_engine, conversation, research, cognitive / agents, oversight, neuro_symbolic.

Streams:

// WebSocket (auto-reconnect):
use reasoninglayer::ws::WebSocketConnection;
let url = client.cognitive().agent_events_url("agent-uuid")?;
let mut ws = WebSocketConnection::connect(&url).await?;
while let Some(event) = ws.next_message().await? { /* ... */ }

// SSE:
use futures_util::StreamExt;
use reasoninglayer::sse::stream;
let body = serde_json::json!({"max_iterations": 10});
let mut events = Box::pin(
    stream(&client, reqwest::Method::POST,
           "/inference/forward-chain/stream", Some(&body), None).await?
);
while let Some(event) = events.next().await { /* ... */ }

Development

cargo test              # unit + integration (wiremock) + doc tests
cargo doc --open        # local API docs
cargo clippy -- -D warnings

Releasing

Releases are driven by cargo-release per the configuration in release.toml. The actual publish to crates.io is done by GitLab CI on tag push, not from your laptop, so you never need a local crates.io token.

cargo install cargo-release        # one-time
cargo release patch                # dry-run: shows what would change
cargo release patch --execute      # actually bump, tag, push

Substitute minor, major, or an explicit X.Y.Z for patch as needed. The script bumps Cargo.toml, refreshes Cargo.lock, rewrites CHANGELOG.md (moves [Unreleased] content under a dated [X.Y.Z] heading and inserts a fresh empty [Unreleased]), runs cargo test --all-features and cargo publish --dry-run, then commits, tags vX.Y.Z, and pushes. Pushing the tag triggers the GitLab publish stage which runs the actual cargo publish against crates.io using the masked CARGO_REGISTRY_TOKEN CI/CD variable.

License

MIT.