-
Notifications
You must be signed in to change notification settings - Fork 0
Composition Branch
A Branch routes execution based on the runtime type of a sealed class or interface. A source agent produces one of several variants, and the branch dispatches each variant to a dedicated handler agent or pipeline.
classifier.branch {
on<Circle>() then circleHandler
on<Rectangle>() then rectangleHandler
on<Triangle>() then triangleHandler
}This gives you type-safe conditional routing. The source agent's output type should be a sealed interface or sealed class, and each on<Variant>() clause handles one concrete subtype.
class Branch<IN, OUT>(
private val source: Agent<IN, *>,
private val routes: Map<KClass<*>, (Any?) -> OUT>,
)
// Create a Branch from an Agent with sealed output
fun <IN, SEALED, OUT> Agent<IN, SEALED>.branch(
block: BranchBuilder<OUT>.() -> Unit
): Branch<IN, OUT>A Branch<IN, OUT> is invokable. When you call branch(input), it invokes the source agent, inspects the result's runtime class, and dispatches to the matching handler.
Inside the branch { } block, you use a builder DSL:
class BranchBuilder<OUT> {
// Select a variant type to handle
inline fun <reified T : Any> on(): OnClause<T>
inner class OnClause<T : Any> {
// Route this variant to an agent
infix fun then(agent: Agent<T, OUT>)
// Route this variant to a pipeline
infix fun then(pipeline: Pipeline<T, OUT>)
}
}The flow is:
- Call
on<Variant>()to specify which sealed subtype to match - Use
thento wire it to a handler agent or pipeline - Repeat for each variant
All handlers must produce the same OUT type, which becomes the Branch's output type.
A shape classifier routes to different handlers based on the shape variant:
import agents_engine.core.*
import agents_engine.composition.branch.branch
sealed interface Shape
data class Circle(val radius: Double) : Shape
data class Rectangle(val w: Double, val h: Double) : Shape
data class Triangle(val base: Double, val height: Double) : Shape
val classify = agent<String, Shape>("classify") {
skills { skill<String, Shape>("classify") { implementedBy { input ->
when {
input.startsWith("c") -> Circle(input.length.toDouble())
input.startsWith("r") -> Rectangle(2.0, 3.0)
else -> Triangle(4.0, 5.0)
}
} } }
}
val branch = classify.branch {
on<Circle>() then agent<Circle, String>("c") { skills { skill<Circle, String>("c") { implementedBy { "circle r=${it.radius}" } } } }
on<Rectangle>() then agent<Rectangle, String>("r") { skills { skill<Rectangle, String>("r") { implementedBy { "rect ${it.w}x${it.h}" } } } }
on<Triangle>() then agent<Triangle, String>("t") { skills { skill<Triangle, String>("t") { implementedBy { "tri b=${it.base}" } } } }
}
branch("circle") // "circle r=6.0"
branch("rect") // "rect 2.0x3.0"
branch("triangle") // "tri b=4.0"A handler does not have to be a single agent. Use a Pipeline to process a variant through multiple stages:
val area = agent<Circle, Double>("area") {
skills { skill<Circle, Double>("area") { implementedBy { Math.PI * it.radius * it.radius } } }
}
val rounded = agent<Double, String>("round") {
skills { skill<Double, String>("round") { implementedBy { "%.2f".format(Locale.US, it) } } }
}
val branch = classify.branch {
on<Circle>() then (area then rounded) // pipeline for this variant
on<Rectangle>() then agent<Rectangle, String>("r") { skills { skill<Rectangle, String>("r") { implementedBy { "rect" } } } }
on<Triangle>() then agent<Triangle, String>("t") { skills { skill<Triangle, String>("t") { implementedBy { "tri" } } } }
}
branch("circle") // "113.10" (area of circle with radius 6.0)A Branch is a first-class composition primitive. Connect it to other stages with then:
Branch after a pipeline:
val prepare = agent<Int, String>("prepare") {
skills { skill<Int, String>("prepare") { implementedBy { if (it > 0) "circle" else "rect" } } }
}
val branch = classify.branch {
on<Circle>() then agent<Circle, Int>("c") { skills { skill<Circle, Int>("c") { implementedBy { it.radius.toInt() } } } }
on<Rectangle>() then agent<Rectangle, Int>("r") { skills { skill<Rectangle, Int>("r") { implementedBy { (it.w * it.h).toInt() } } } }
on<Triangle>() then agent<Triangle, Int>("t") { skills { skill<Triangle, Int>("t") { implementedBy { (it.base * it.height / 2).toInt() } } } }
}
val pipeline = prepare then branch
pipeline(1) // "circle" -> Circle(6.0) -> 6
pipeline(-1) // "rect" -> Rectangle(2.0, 3.0) -> 6Agent after a branch:
val branch = classify.branch {
on<Circle>() then agent<Circle, Double>("c") { skills { skill<Circle, Double>("c") { implementedBy { it.radius } } } }
on<Rectangle>() then agent<Rectangle, Double>("r") { skills { skill<Rectangle, Double>("r") { implementedBy { it.w * it.h } } } }
on<Triangle>() then agent<Triangle, Double>("t") { skills { skill<Triangle, Double>("t") { implementedBy { it.base * it.height / 2 } } } }
}
val wrap = agent<Double, String>("wrap") {
skills { skill<Double, String>("wrap") { implementedBy { "area=%.1f".format(Locale.US, it) } } }
}
val pipeline = branch then wrap
pipeline("circle") // "area=6.0"
pipeline("rect") // "area=6.0"
pipeline("triangle") // "area=10.0"If the source agent returns a variant that has no matching on<>() clause, the branch throws an IllegalStateException at runtime:
val branch = classify.branch {
on<Circle>() then circleHandler
on<Rectangle>() then rectHandler
// Triangle is not handled
}
branch("triangle")
// throws IllegalStateException: "No branch defined for Triangle."Unlike Kotlin's when on sealed types, the branch {} DSL does not enforce exhaustiveness at compile time. The routes are built dynamically via a MutableMap<KClass<*>, ...>, so the compiler cannot verify that every sealed variant is covered.
To protect yourself:
- Handle all variants explicitly
- Write tests that exercise every variant
- The runtime
IllegalStateExceptionwill catch any gaps during testing
// Recommended: cover every variant
val branch = classify.branch {
on<Circle>() then circleHandler
on<Rectangle>() then rectHandler
on<Triangle>() then triHandler // do not forget this one
}Agents used as handlers inside a branch are tracked and cannot be reused elsewhere:
val circleHandler = agent<Circle, String>("c") { skills { skill<Circle, String>("c") { implementedBy { "circle" } } } }
classify.branch {
on<Circle>() then circleHandler // circleHandler is now placed
// ...
}
// This throws IllegalArgumentException:
// circleHandler then anotherAgentNext: Pipeline | Parallel | Loop | Forum | While Loops
Getting Started
Core Concepts
Composition Operators
LLM Integration
- Model & Tool Calling
- Tool Error Recovery
- Skill Selection & Routing
- Budget Controls
- Observability Hooks
Guided Generation
Agent Memory
Reference