Values and Formats
The Reasoning Layer backend uses two different serialization formats for values depending on the endpoint. Understanding when to use each format is the single most important thing to get right when working with this SDK.
The two formats
Tagged format (ValueDto)
Used by term CRUD, queries, and fuzzy operations. Values carry an explicit "type" discriminator:
{"type": "String", "value": "hello"}{"type": "Integer", "value": 42}{"type": "Real", "value": 3.14}{"type": "Boolean", "value": true}{"type": "Uninstantiated"}{"type": "Reference", "value": "550e8400-e29b-41d4-a716-446655440000"}{"type": "List", "value": [{"type": "Integer", "value": 1}, {"type": "Integer", "value": 2}]}For simple types, pass plain JS values directly — the SDK auto-converts them:
// Plain values (no builder needed — auto-converted by SDK):const features = { name: "Alice", // -> {"type": "String", "value": "Alice"} age: 30, // -> {"type": "Integer", "value": 30} score: 0.95, // -> {"type": "Real", "value": 0.95} active: true, // -> {"type": "Boolean", "value": true} pending: null, // -> {"type": "Uninstantiated"}};For complex types that have no plain JS equivalent, use the Value namespace:
import { Value } from '@kortexya/reasoninglayer';
const complexFeatures = { mentor: Value.reference(mentorId), // {"type": "Reference", "value": "uuid"}};Untagged format (FeatureInputValueDto)
Used by homoiconic inference endpoints (backward chaining, forward chaining, fuzzy proving, etc.). Values are raw JSON primitives with no discriminator:
"hello"423.14truenull["550e8400-...", "660f9500-..."]Build untagged inputs with psi() and plain JS values:
import { psi, constrained, guard } from '@kortexya/reasoninglayer';
// Plain values used directly inside psi():const fact = psi("person", { name: "Alice", // "Alice" age: 30, // 30 score: 0.95, // 0.95 active: true, // true});
// Variables — strings starting with "?" are auto-detected:const query = psi("person", { name: "?Name", // {name: "?Name"} role: "?Role", // {name: "?Role"}});
// Term references — strings starting with "!" are auto-detected:const withRef = psi("team", { lead: "!550e8400-...", // {termId: "550e8400-..."}});The untagged format also supports structural variants:
// Constrained variableconstrained("?Salary", guard("gt", 50000))// {name: "?Salary", constraint: {sortName: "guard_constraint", features: {op: "gt", right: 50000}}}
// Inline term by sort name (nested psi-terms)psi("address", { city: "Paris",})// {sortName: "address", features: {city: "Paris"}}
// List["a", "b"]// ["a", "b"]Which format for which endpoint
| Endpoint domain | Format | Builder | Type |
|---|---|---|---|
| Term CRUD | Tagged | Plain values or Value.* | ValueDto |
| Queries | Tagged | Plain values or Value.* | ValueDto |
| Fuzzy operations | Tagged | Plain values or Value.* | ValueDto |
| Backward chaining | Untagged | psi(), plain values | FeatureInputValueDto |
| Forward chaining | Untagged | psi(), plain values | FeatureInputValueDto |
| Add rule | Untagged | psi(), plain values | FeatureInputValueDto |
| Add fact | Untagged | psi(), plain values | FeatureInputValueDto |
| Fuzzy proving | Untagged | psi(), plain values | FeatureInputValueDto |
| Bayesian prediction | Untagged | psi(), plain values | FeatureInputValueDto |
| NAF proving | Untagged | psi(), plain values | FeatureInputValueDto |
The rule of thumb: client.terms.* and client.query.* use plain values or Value.*. client.inference.* uses psi() with plain values.
FuzzyShapeDto uses “kind”, not “type”
Fuzzy membership function shapes use "kind" as their discriminator field, not "type". This is different from every other tagged union in the API:
{"kind": "Triangular", "a": 20, "b": 22, "c": 24}{"kind": "Trapezoidal", "a": 18, "b": 20, "c": 24, "d": 26}{"kind": "Gaussian", "mean": 100, "stdDev": 15}{"kind": "CyclicGaussian", "mean": 180, "stdDev": 30, "period": 360}Build fuzzy shapes with the FuzzyShape namespace and combine with Value.fuzzyNumber():
import { Value, FuzzyShape } from '@kortexya/reasoninglayer';
const temperature = Value.fuzzyNumber(FuzzyShape.triangular(20, 22, 24));// {"type": "FuzzyNumber", "value": {"shape": {"kind": "Triangular", "a": 20, "b": 22, "c": 24}}}
const iq = Value.fuzzyNumber(FuzzyShape.gaussian(100, 15));// {"type": "FuzzyNumber", "value": {"shape": {"kind": "Gaussian", "mean": 100, "stdDev": 15}}}Common mistakes
Mistake 1: Using Value builders in inference requests
// WRONG: Value.reference() produces tagged format, but inference expects untaggedawait client.inference.addFact({ term: psi("person", { name: Value.reference("some-uuid"), // This produces {"type": "Reference", "value": "uuid"} }), // The backend expects just {termId: "uuid"}});
// CORRECT: Use plain values or "!" prefix for inferenceawait client.inference.addFact({ term: psi("person", { name: "Alice", // Plain string is used directly }),});Mistake 2: Using psi() in term CRUD
// WRONG: psi() is for inference, not term CRUDawait client.terms.createTerm({ sortId: personSortId, ownerId: userId, features: { name: psi("person"), // This is a PsiTermInput, not a ValueDto },});
// CORRECT: Use plain values for term CRUDawait client.terms.createTerm({ sortId: personSortId, ownerId: userId, features: { name: "Alice", // Auto-converted to {"type": "String", "value": "Alice"} },});Mistake 3: Confusing “kind” with “type” on fuzzy shapes
// WRONG: Using "type" instead of "kind" for fuzzy shapesconst shape = { type: "Triangular", a: 20, b: 22, c: 24 };
// CORRECT: FuzzyShapeDto uses "kind" discriminatorconst shape = FuzzyShape.triangular(20, 22, 24);// Produces: { kind: "Triangular", a: 20, b: 22, c: 24 }Value fallback stringification
The backend may convert domain-internal value types (BigInteger, DateTime, Geometry, Measurement, etc.) to ValueDto::String using Rust’s debug format. When reading term features, you may encounter strings like:
"DateTime(2024-01-15T10:30:00Z)""BigInteger(99999999999999999999)""Geometry(Point(1.0, 2.0))"These are not first-class ValueDto variants. They are the result of a fallback String(format!("{:?}", value)) conversion in the backend. If you encounter these patterns, the underlying value was a domain type that the SDK does not model with a dedicated variant.
All ValueDto variants
The tagged ValueDto union has 10 variants:
| Variant | Builder | JSON output |
|---|---|---|
| String | "hello" (plain) | {"type": "String", "value": "hello"} |
| Integer | 42 (plain) | {"type": "Integer", "value": 42} |
| Real | 3.14 (plain) | {"type": "Real", "value": 3.14} |
| Boolean | true (plain) | {"type": "Boolean", "value": true} |
| Uninstantiated | null (plain) | {"type": "Uninstantiated"} |
| Reference | Value.reference(uuid) | {"type": "Reference", "value": "uuid"} |
| List | [...] (plain) | {"type": "List", "value": [...]} |
| FuzzyScalar | Value.fuzzyScalar(0.5, 0.8) | {"type": "FuzzyScalar", "value": {"value": 0.5, "membership": 0.8}} |
| FuzzyNumber | Value.fuzzyNumber(shape) | {"type": "FuzzyNumber", "value": {"shape": {...}}} |
| Set | Value.set(lower, upper, sort?) | {"type": "Set", "value": {"lower": [...], "upper": [...], "sortConstraint": ...}} |
All FeatureInputValueDto variants
The untagged FeatureInputValueDto union has 10 structural variants. The ordering matches the Rust serde(untagged) deserialization order:
| # | Variant | Builder / value | JSON output |
|---|---|---|---|
| 1 | TermRef | "!uuid" in psi() or {termId: "uuid"} | {"termId": "uuid"} |
| 2 | ConstrainedVariable | constrained("?X", constraint) | {"name": "?X", "constraint": {...}} |
| 3 | Variable | "?X" in psi() | {"name": "?X"} |
| 4 | InlineTerm | {sortId: "uuid", features: {...}} | {"sortId": "uuid", "features": {...}} |
| 5 | InlineTermByName | psi("person", features) | {"sortName": "person", "features": {...}} |
| 6 | List | [...] | [...] |
| 7 | String | "hello" | "hello" |
| 8 | Integer | 42 | 42 |
| 9 | Real | 3.14 | 3.14 |
| 10 | Boolean | true | true |
The ConstrainedVariable variant must come before Variable in the deserialization order because both have a name field, but the constrained version also has constraint. The SDK builders ensure correct construction.