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.

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"}
};

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"]
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”

Section titled “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}}}

Mistake 1: Using Value builders in inference requests

Section titled “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
}),
});
// 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

Section titled “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 }

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.

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": ...}}

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.