Skip to content

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"
42
3.14
true
null
["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 variable
constrained("?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 domainFormatBuilderType
Term CRUDTaggedPlain values or Value.*ValueDto
QueriesTaggedPlain values or Value.*ValueDto
Fuzzy operationsTaggedPlain values or Value.*ValueDto
Backward chainingUntaggedpsi(), plain valuesFeatureInputValueDto
Forward chainingUntaggedpsi(), plain valuesFeatureInputValueDto
Add ruleUntaggedpsi(), plain valuesFeatureInputValueDto
Add factUntaggedpsi(), plain valuesFeatureInputValueDto
Fuzzy provingUntaggedpsi(), plain valuesFeatureInputValueDto
Bayesian predictionUntaggedpsi(), plain valuesFeatureInputValueDto
NAF provingUntaggedpsi(), plain valuesFeatureInputValueDto

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 untagged
await 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 inference
await 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 CRUD
await client.terms.createTerm({
sortId: personSortId,
ownerId: userId,
features: {
name: psi("person"), // This is a PsiTermInput, not a ValueDto
},
});
// CORRECT: Use plain values for term CRUD
await 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 shapes
const shape = { type: "Triangular", a: 20, b: 22, c: 24 };
// CORRECT: FuzzyShapeDto uses "kind" discriminator
const 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:

VariantBuilderJSON output
String"hello" (plain){"type": "String", "value": "hello"}
Integer42 (plain){"type": "Integer", "value": 42}
Real3.14 (plain){"type": "Real", "value": 3.14}
Booleantrue (plain){"type": "Boolean", "value": true}
Uninstantiatednull (plain){"type": "Uninstantiated"}
ReferenceValue.reference(uuid){"type": "Reference", "value": "uuid"}
List[...] (plain){"type": "List", "value": [...]}
FuzzyScalarValue.fuzzyScalar(0.5, 0.8){"type": "FuzzyScalar", "value": {"value": 0.5, "membership": 0.8}}
FuzzyNumberValue.fuzzyNumber(shape){"type": "FuzzyNumber", "value": {"shape": {...}}}
SetValue.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:

#VariantBuilder / valueJSON output
1TermRef"!uuid" in psi() or {termId: "uuid"}{"termId": "uuid"}
2ConstrainedVariableconstrained("?X", constraint){"name": "?X", "constraint": {...}}
3Variable"?X" in psi(){"name": "?X"}
4InlineTerm{sortId: "uuid", features: {...}}{"sortId": "uuid", "features": {...}}
5InlineTermByNamepsi("person", features){"sortName": "person", "features": {...}}
6List[...][...]
7String"hello""hello"
8Integer4242
9Real3.143.14
10Booleantruetrue

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.