reasoninglayer 0.2.1

Rust client SDK for the Reasoning Layer API
Documentation
# reasoninglayer

Rust client SDK for the [Reasoning Layer](https://github.com/kortexya/reasoninglayer) 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

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

TLS backend: `rustls` by default. Switch with features:

```toml
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 `idna` → `icu_*` 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

```rust
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

```rust
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:

```rust
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:

```rust
// 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

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

### Releasing

Releases are driven by [`cargo-release`](https://github.com/crate-ci/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.

```bash
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.