Fluent DSL and Kotlin DSL for building autonomous agents with planning capabilities on the JVM, featuring annotation-based and programmatic configuration for agentic flows with Spring Boot integration
The Domain Type System provides a flexible type representation that supports both JVM-backed classes and dynamically defined types. It enables type introspection, inheritance modeling, property definitions, and semantic annotations for domain modeling.
Core sealed interface representing any type in the system.
sealed interface DomainType : HasInfoString, NamedAndDescribed {
/** Properties defined on this type only (not inherited) */
val ownProperties: List<PropertyDefinition>
/** Parent types supporting inheritance */
val parents: List<DomainType>
/** Whether instance creation is permitted */
val creationPermitted: Boolean
/** All properties including inherited ones (deduplicated by name) */
val properties: List<PropertyDefinition>
get() {
val propertiesByName = mutableMapOf<String, PropertyDefinition>()
// First add inherited properties
parents.forEach { parent ->
parent.properties.forEach { property ->
propertiesByName.putIfAbsent(property.name, property)
}
}
// Then add own properties (take precedence)
ownProperties.forEach { property ->
propertiesByName[property.name] = property
}
return propertiesByName.values.toList()
}
/** All value properties (simple properties) */
val values: List<ValuePropertyDefinition>
get() = properties.filterIsInstance<ValuePropertyDefinition>()
/** All relationship properties (references to other domain types) */
val relationships: List<DomainTypePropertyDefinition>
get() = properties.filterIsInstance<DomainTypePropertyDefinition>()
/** Label for this type only (simple name, capitalized) */
val ownLabel: String
get() {
val simpleName = name.substringAfterLast('.')
return simpleName.replaceFirstChar { it.uppercase() }
}
/** All labels including from parent types */
val labels: Set<String>
get() {
val allLabels = mutableSetOf<String>()
allLabels.add(ownLabel)
parents.forEach { parent ->
allLabels.addAll(parent.labels)
}
return allLabels
}
/** Get all descendant types from classpath */
fun children(additionalBasePackages: Collection<String> = listOf()): Collection<DomainType>
/** Check if this type is assignable from a class */
fun isAssignableFrom(other: Class<*>): Boolean
/** Check if this type is assignable from another domain type */
fun isAssignableFrom(other: DomainType): Boolean
/** Check if this type is assignable to a class */
fun isAssignableTo(other: Class<*>): Boolean
/** Check if this type is assignable to another domain type */
fun isAssignableTo(other: DomainType): Boolean
}Type Hierarchy Example:
// Define a base class
open class Animal(
val name: String
)
class Horse(
name: String,
val breed: String
) : Animal(name)
// Create JvmType and explore hierarchy
val horseType = JvmType(Horse::class.java)
// Check parents
println(horseType.parents.size) // 1
println(horseType.parents[0].name) // Animal
// Check own vs inherited properties
println(horseType.ownProperties.size) // 1 (breed)
println(horseType.properties.size) // 2 (breed, name)
// Check labels
println(horseType.labels) // [Horse, Animal]
println(horseType.ownLabel) // HorseJVM class-backed domain type with reflection-based property extraction.
data class JvmType @JsonCreator constructor(
@param:JsonProperty("className")
val className: String
) : DomainType {
/** Create from Class */
constructor(clazz: Class<*>) : this(clazz.name)
/** The JVM class */
val clazz: Class<*>
/** Whether creation is permitted (from @CreationPermitted annotation) */
override val creationPermitted: Boolean
get() {
val cpa = clazz.getAnnotation(CreationPermitted::class.java)
return cpa?.value ?: true
}
/** Parent types from superclass and interfaces */
override val parents: List<JvmType>
get() {
val superclass = clazz.superclass
val parentList = mutableListOf<JvmType>()
if (superclass != null && superclass != Object::class.java) {
parentList.add(JvmType(superclass))
}
clazz.interfaces.forEach { parentList.add(JvmType(it)) }
return parentList
}
/** Properties from fields and getter methods */
override val ownProperties: List<PropertyDefinition>
/** Type name (fully qualified class name) */
override val name: String
get() = className
/** Label (simple class name) */
override val ownLabel: String
get() = clazz.simpleName
/** Description from @JsonClassDescription or simple name */
override val description: String
get() {
val ann = clazz.getAnnotation(JsonClassDescription::class.java)
return if (ann != null) {
"${clazz.simpleName}: ${ann.value}"
} else {
clazz.simpleName
}
}
/** Find descendant types via classpath scanning */
override fun children(additionalBasePackages: Collection<String>): Collection<JvmType>
override fun isAssignableFrom(other: Class<*>): Boolean =
clazz.isAssignableFrom(other)
override fun isAssignableFrom(other: DomainType): Boolean =
when (other) {
is JvmType -> clazz.isAssignableFrom(other.clazz)
is DynamicType -> false
}
override fun isAssignableTo(other: Class<*>): Boolean =
other.isAssignableFrom(clazz)
override fun isAssignableTo(other: DomainType): Boolean =
when (other) {
is JvmType -> other.clazz.isAssignableFrom(clazz)
is DynamicType -> false
}
companion object {
/** Create JvmTypes from multiple classes */
fun fromClasses(
classes: Collection<Class<*>>
): Collection<JvmType>
}
}JvmType Creation Examples:
// Basic entity
class Dog(
val name: String,
val breed: String
)
val dogType = JvmType(Dog::class.java)
println(dogType.name) // com.example.Dog
println(dogType.ownProperties.size) // 2
println(dogType.ownProperties[0].name) // name
// With description annotation
@JsonClassDescription("A feline creature")
class Cat(val name: String)
val catType = JvmType(Cat::class.java)
println(catType.description) // Cat: A feline creature
// Nested entities
class Owner(
val name: String,
val dog: Dog
)
val ownerType = JvmType(Owner::class.java)
val dogProperty = ownerType.ownProperties[1] as DomainTypePropertyDefinition
println(dogProperty.type.name) // com.example.Dog
// Collections
class Kennel(
val name: String,
val dogs: List<Dog>
)
val kennelType = JvmType(Kennel::class.java)
val dogsProperty = kennelType.ownProperties[1] as DomainTypePropertyDefinition
println(dogsProperty.cardinality) // LISTInterface Properties:
// JvmType extracts properties from interface getters
interface Vehicle {
fun getWheels(): Int
fun getName(): String
}
val vehicleType = JvmType(Vehicle::class.java)
println(vehicleType.ownProperties.map { it.name }) // [wheels, name]
// Boolean properties with is-prefix
interface BooleanEntity {
fun isActive(): Boolean
fun getName(): String
}
val boolType = JvmType(BooleanEntity::class.java)
println(boolType.ownProperties.map { it.name }) // [active, name]Finding Children:
abstract class TestVehicle
class TestCar : TestVehicle()
class TestMotorcycle : TestVehicle()
val vehicleType = JvmType(TestVehicle::class.java)
val children = vehicleType.children(listOf("com.example"))
val childrenNames = children.map { it.name }.toSet()
// Contains: TestCar, TestMotorcycle
// Does not contain: TestVehicle (itself)Dynamically defined types for non-JVM interoperability.
data class DynamicType(
/** Name of the type (should be unique) */
override val name: String,
/** Human-readable description */
override val description: String = name,
/** Properties defined on this type */
override val ownProperties: List<PropertyDefinition> = emptyList(),
/** Parent types (can be JVM or dynamic) */
override val parents: List<DomainType> = emptyList(),
/** Whether instance creation is permitted */
override val creationPermitted: Boolean = true
) : DomainType {
override fun isAssignableFrom(other: Class<*>): Boolean = false
override fun isAssignableFrom(other: DomainType): Boolean =
other.name == name
override fun isAssignableTo(other: Class<*>): Boolean = false
override fun isAssignableTo(other: DomainType): Boolean =
other.name == name
override fun children(additionalBasePackages: Collection<String>): Collection<DomainType> =
emptySet()
/** Add property to dynamic type */
fun withProperty(property: PropertyDefinition): DynamicType
}DynamicType Examples:
// Basic dynamic type
val personType = DynamicType(
name = "Person",
description = "A human person",
ownProperties = listOf(
ValuePropertyDefinition(
name = "name",
type = "string",
cardinality = Cardinality.ONE
),
ValuePropertyDefinition(
name = "age",
type = "int",
cardinality = Cardinality.ONE
)
)
)
// Dynamic type with relationships
val companyType = DynamicType(
name = "Company",
description = "An organization"
)
val employeeType = DynamicType(
name = "Employee",
description = "A company employee",
ownProperties = listOf(
ValuePropertyDefinition(name = "name", type = "string"),
DomainTypePropertyDefinition(
name = "employer",
type = companyType,
cardinality = Cardinality.ONE
)
)
)
// Add property dynamically
val extendedType = personType.withProperty(
ValuePropertyDefinition(
name = "email",
type = "string",
cardinality = Cardinality.OPTIONAL
)
)
// Dynamic type with JVM parent
val specializedType = DynamicType(
name = "SpecializedEntity",
parents = listOf(JvmType(BaseEntity::class.java)),
ownProperties = listOf(
ValuePropertyDefinition(name = "customField", type = "string")
)
)Sealed interface for property definitions.
sealed interface PropertyDefinition {
/** Property name */
val name: String
/** Human-readable description */
val description: String
/** Cardinality (OPTIONAL, ONE, LIST, SET) */
val cardinality: Cardinality
/** Semantic metadata from @Semantics annotation */
val metadata: Map<String, String>
}Simple value property (scalars, strings, primitives).
data class ValuePropertyDefinition @JvmOverloads constructor(
override val name: String,
/** Type as string (e.g., "string", "int", "boolean") */
val type: String = "string",
override val cardinality: Cardinality = Cardinality.ONE,
override val description: String = name,
override val metadata: Map<String, String> = emptyMap()
) : PropertyDefinitionValuePropertyDefinition Examples:
// Simple string property
val nameProperty = ValuePropertyDefinition(
name = "name",
type = "string",
cardinality = Cardinality.ONE,
description = "Person's full name"
)
// Optional numeric property
val ageProperty = ValuePropertyDefinition(
name = "age",
type = "int",
cardinality = Cardinality.OPTIONAL
)
// List of strings
val tagsProperty = ValuePropertyDefinition(
name = "tags",
type = "string",
cardinality = Cardinality.LIST
)Property that references another domain type (relationship).
data class DomainTypePropertyDefinition @JvmOverloads constructor(
override val name: String,
/** Referenced domain type */
val type: DomainType,
override val cardinality: Cardinality = Cardinality.ONE,
override val description: String = name,
override val metadata: Map<String, String> = emptyMap()
) : PropertyDefinitionDomainTypePropertyDefinition Examples:
class Company(val name: String)
class Employee(
val name: String,
val worksAt: Company
)
val companyType = JvmType(Company::class.java)
val employeeType = JvmType(Employee::class.java)
// Single relationship
val worksAtProperty = employeeType.relationships[0]
println(worksAtProperty.name) // worksAt
println(worksAtProperty.type.name) // Company
println(worksAtProperty.cardinality) // ONE
// Collection relationship
class Manager(
val name: String,
val manages: List<Employee>
)
val managerType = JvmType(Manager::class.java)
val managesProperty = managerType.relationships[0]
println(managesProperty.cardinality) // LISTValue property with type-safe validation rules.
data class ValidatedPropertyDefinition @JvmOverloads constructor(
override val name: String,
val type: String = "string",
override val cardinality: Cardinality = Cardinality.ONE,
override val description: String = name,
override val metadata: Map<String, String> = emptyMap(),
/** Validation rules to apply */
val validationRules: List<PropertyValidationRule> = emptyList()
) : PropertyDefinition {
/** Validate mention against all rules */
fun isValid(mention: String): Boolean {
return validationRules.all { rule -> rule.isValid(mention) }
}
/** Get first validation failure reason */
fun failureReason(mention: String): String? {
return validationRules.firstNotNullOfOrNull { rule ->
if (!rule.isValid(mention)) rule.failureReason(mention) else null
}
}
}ValidatedPropertyDefinition Example:
// Custom validation rule
class LengthConstraint(val maxLength: Int) : PropertyValidationRule {
override val description = "Must not exceed $maxLength characters"
override fun isValid(mention: String): Boolean {
return mention.length <= maxLength
}
override fun failureReason(mention: String): String {
return "Value '$mention' exceeds maximum length of $maxLength"
}
}
class NoVagueReferences : PropertyValidationRule {
override val description = "Must not contain vague references"
override fun isValid(mention: String): Boolean {
val vagueTerms = listOf("something", "thing", "stuff")
return vagueTerms.none { mention.contains(it, ignoreCase = true) }
}
}
// Property with validation
val nameProperty = ValidatedPropertyDefinition(
name = "name",
type = "string",
validationRules = listOf(
NoVagueReferences(),
LengthConstraint(maxLength = 150)
)
)
// Validate values
println(nameProperty.isValid("John Smith")) // true
println(nameProperty.isValid("something vague")) // false
println(nameProperty.failureReason("something vague"))
// "Failed validation: Must not contain vague references"Enum defining property cardinality.
enum class Cardinality {
/** Optional single value (0..1) */
OPTIONAL,
/** Required single value (1) */
ONE,
/** Ordered collection (0..*) */
LIST,
/** Unordered unique collection (0..*) */
SET
}Cardinality Examples:
class Product(
val id: String, // ONE
val description: String?, // OPTIONAL
val tags: List<String>, // LIST
val categories: Set<String> // SET
)
val productType = JvmType(Product::class.java)
productType.ownProperties.forEach { prop ->
println("${prop.name}: ${prop.cardinality}")
}
// id: ONE
// description: ONE (nullable handled separately)
// tags: LIST
// categories: SETInterface for custom validation rules.
interface PropertyValidationRule {
/** Human-readable description of rule */
val description: String
/** Check if mention is valid */
fun isValid(mention: String): Boolean
/** Get failure reason for invalid mention */
fun failureReason(mention: String): String? =
if (!isValid(mention)) "Failed validation: $description" else null
}Custom Validation Rules:
// Email validation
class EmailValidation : PropertyValidationRule {
override val description = "Must be valid email format"
override fun isValid(mention: String): Boolean {
return mention.matches(Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"))
}
override fun failureReason(mention: String): String {
return "Value '$mention' is not a valid email address"
}
}
// Range validation
class RangeValidation(val min: Int, val max: Int) : PropertyValidationRule {
override val description = "Must be between $min and $max"
override fun isValid(mention: String): Boolean {
val value = mention.toIntOrNull() ?: return false
return value in min..max
}
}
// Enum validation
class EnumValidation(val allowedValues: List<String>) : PropertyValidationRule {
override val description = "Must be one of: ${allowedValues.joinToString()}"
override fun isValid(mention: String): Boolean {
return mention in allowedValues
}
}
// Use in property definition
val statusProperty = ValidatedPropertyDefinition(
name = "status",
validationRules = listOf(
EnumValidation(listOf("active", "inactive", "pending"))
)
)Container for domain types with filtering and relationship extraction.
interface DataDictionary : Named {
/** All known domain types */
val domainTypes: Collection<DomainType>
/** All dynamic types */
val dynamicTypes: Collection<DynamicType>
get() = domainTypes.filterIsInstance<DynamicType>().toSet()
/** All JVM types */
val jvmTypes: Collection<JvmType>
get() = domainTypes.filterIsInstance<JvmType>().toSet()
/** Filter domain types by predicate */
fun filter(predicate: (DomainType) -> Boolean): DataDictionary
/** Exclude specific classes */
fun excluding(vararg classes: Class<*>): DataDictionary
/** Exclude collection of classes */
fun excluding(classes: Collection<Class<*>>): DataDictionary
/** Exclude single class (operator) */
operator fun minus(clazz: Class<*>): DataDictionary
/** Exclude multiple classes (operator) */
operator fun minus(classes: Collection<Class<*>>): DataDictionary
/** Get all relationships between types */
fun allowedRelationships(): List<AllowedRelationship>
/** Find domain type matching labels */
fun domainTypeForLabels(labels: Set<String>): DomainType?
companion object {
/** Create from domain types */
@JvmStatic
fun fromDomainTypes(
name: String,
domainTypes: Collection<DomainType>
): DataDictionary
/** Create from classes */
@JvmStatic
fun fromClasses(
name: String,
vararg embabelTypes: Class<*>
): DataDictionary
}
}
data class AllowedRelationship(
val from: DomainType,
val to: DomainType,
val name: String,
val description: String = name,
val cardinality: Cardinality,
val metadata: Map<String, String> = emptyMap()
)DataDictionary Examples:
// Create from classes
val dictionary = DataDictionary.fromClasses(
"my-domain",
Employee::class.java,
Company::class.java,
Department::class.java
)
println(dictionary.domainTypes.size) // 3
// Extract relationships
val relationships = dictionary.allowedRelationships()
relationships.forEach { rel ->
println("${rel.from.ownLabel} -[${rel.name}]-> ${rel.to.ownLabel}")
}
// Employee -[worksAt]-> Company
// Employee -[department]-> Department
// Filter types
val publicTypesOnly = dictionary.filter { type ->
type.name.contains("public")
}
// Exclude types
val filteredDict = dictionary.excluding(Department::class.java)
println(filteredDict.domainTypes.size) // 2
// Using minus operator
val dict2 = dictionary - Department::class.java
val dict3 = dictionary - setOf(Department::class.java, Company::class.java)
// Mixed JVM and dynamic types
val mixedDict = DataDictionary.fromDomainTypes(
"mixed",
listOf(
JvmType(Employee::class.java),
DynamicType(
name = "ExternalEntity",
ownProperties = listOf(
ValuePropertyDefinition("id", "string")
)
)
)
)
// Find type by labels
val type = dictionary.domainTypeForLabels(setOf("Employee", "Person"))Attach semantic metadata to properties for natural language processing.
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Semantics(
/** Semantic properties for this field */
val value: Array<With> = []
)
@Target()
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class With(
/** Key for semantic property */
val key: String,
/** Value for semantic property */
val value: String
)Semantics Annotation Examples:
// Kotlin usage
data class Person(
val name: String,
@field:Semantics([
With("predicate", "works at"),
With("inverse", "employs"),
With("aliases", "is employed by, works for")
])
val worksAt: Company
)
// Extract metadata
val personType = JvmType(Person::class.java)
val worksAtProperty = personType.ownProperties.find { it.name == "worksAt" }
println(worksAtProperty?.metadata)
// {predicate=works at, inverse=employs, aliases=is employed by, works for}
// Java field usage
public class Employee {
private String name;
@Semantics({
@With(key = "predicate", value = "reports to"),
@With(key = "inverse", value = "manages")
})
private Employee manager;
}
// Java interface getter usage
public interface HasEmployment {
@Semantics({
@With(key = "predicate", value = "works at"),
@With(key = "inverse", value = "employs")
})
Company getWorksAt();
}
// Value property semantics
class PersonWithValueSemantics(
@field:Semantics([
With("format", "email"),
With("validation", "required")
])
val email: String
)
// Collection semantics
class KennelWithSemantics(
val name: String,
@field:Semantics([
With("predicate", "houses"),
With("inverse", "lives in")
])
val dogs: List<Dog>
)Control whether new instances of a type can be created.
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CreationPermitted(
/** Whether creation of instances is permitted */
val value: Boolean
)CreationPermitted Examples:
// Default: creation permitted
class Customer(val name: String)
val customerType = JvmType(Customer::class.java)
println(customerType.creationPermitted) // true
// Explicitly permitted
@CreationPermitted(true)
class Order(val id: String)
val orderType = JvmType(Order::class.java)
println(orderType.creationPermitted) // true
// Reference data - creation not permitted
@CreationPermitted(false)
class Country(val code: String, val name: String)
val countryType = JvmType(Country::class.java)
println(countryType.creationPermitted) // false
// Use in application logic
fun canCreateInstance(type: DomainType): Boolean {
return type.creationPermitted
}
// Filter dictionary to only creatable types
val creatableTypes = dictionary.filter { it.creationPermitted }Domain Model with Full Type System:
// Domain classes
@JsonClassDescription("A business organization")
@CreationPermitted(true)
class Company(
val name: String,
val founded: Int
)
@JsonClassDescription("A company department")
class Department(
val name: String,
@field:Semantics([
With("predicate", "belongs to"),
With("inverse", "has department")
])
val company: Company
)
class Employee(
val name: String,
@field:Semantics([
With("format", "email"),
With("validation", "required")
])
val email: String,
@field:Semantics([
With("predicate", "works at"),
With("inverse", "employs")
])
val employer: Company,
@field:Semantics([
With("predicate", "works in"),
With("inverse", "has member")
])
val department: Department,
@field:Semantics([
With("predicate", "manages"),
With("inverse", "reports to")
])
val directReports: List<Employee>
)
// Create data dictionary
val dictionary = DataDictionary.fromClasses(
"company-domain",
Company::class.java,
Department::class.java,
Employee::class.java
)
// Explore types
dictionary.domainTypes.forEach { type ->
println("Type: ${type.name}")
println(" Creation Permitted: ${type.creationPermitted}")
println(" Properties:")
type.properties.forEach { prop ->
println(" - ${prop.name} (${prop.cardinality})")
if (prop.metadata.isNotEmpty()) {
println(" Semantics: ${prop.metadata}")
}
}
}
// Extract relationships
val relationships = dictionary.allowedRelationships()
relationships.forEach { rel ->
val predicate = rel.metadata["predicate"] ?: rel.name
println("${rel.from.ownLabel} '$predicate' ${rel.to.ownLabel}")
}
// Employee 'works at' Company
// Employee 'works in' Department
// Employee 'manages' Employee
// Department 'belongs to' Company
// Type hierarchy
val employeeType = JvmType(Employee::class.java)
println("Labels: ${employeeType.labels}")
println("Values: ${employeeType.values.map { it.name }}")
println("Relationships: ${employeeType.relationships.map { it.name }}")
// Type checking
val companyType = JvmType(Company::class.java)
println(employeeType.isAssignableTo(Any::class.java)) // true
println(companyType.isAssignableFrom(Department::class.java)) // false// Base interfaces
interface HasInfoString {
fun infoString(verbose: Boolean?, indent: Int): String
}
interface NamedAndDescribed {
val name: String
val description: String
}
interface Named {
val name: String
}
// Type implementations
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes(
JsonSubTypes.Type(value = DynamicType::class, name = "dynamic"),
JsonSubTypes.Type(value = JvmType::class, name = "jvm")
)
sealed interface DomainType : HasInfoString, NamedAndDescribed
data class JvmType(val className: String) : DomainType
data class DynamicType(
override val name: String,
override val description: String,
override val ownProperties: List<PropertyDefinition>,
override val parents: List<DomainType>,
override val creationPermitted: Boolean
) : DomainType
// Property definitions
@JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME)
@JsonSubTypes(
JsonSubTypes.Type(value = ValuePropertyDefinition::class, name = "simple"),
JsonSubTypes.Type(value = DomainTypePropertyDefinition::class, name = "domain"),
JsonSubTypes.Type(value = ValidatedPropertyDefinition::class, name = "validated")
)
sealed interface PropertyDefinition {
val name: String
val description: String
val cardinality: Cardinality
val metadata: Map<String, String>
}
data class ValuePropertyDefinition(
override val name: String,
val type: String,
override val cardinality: Cardinality,
override val description: String,
override val metadata: Map<String, String>
) : PropertyDefinition
data class DomainTypePropertyDefinition(
override val name: String,
val type: DomainType,
override val cardinality: Cardinality,
override val description: String,
override val metadata: Map<String, String>
) : PropertyDefinition
data class ValidatedPropertyDefinition(
override val name: String,
val type: String,
override val cardinality: Cardinality,
override val description: String,
override val metadata: Map<String, String>,
val validationRules: List<PropertyValidationRule>
) : PropertyDefinition
interface PropertyValidationRule {
val description: String
fun isValid(mention: String): Boolean
fun failureReason(mention: String): String?
}
enum class Cardinality {
OPTIONAL,
ONE,
LIST,
SET
}Install with Tessl CLI
npx tessl i tessl/maven-com-embabel-agent--embabel-agent-apidocs