-
Notifications
You must be signed in to change notification settings - Fork 0
Type Algebra
This page documents every then overload, all composition operator type rules, and the most common type errors you will encounter.
Every agent is a typed function:
Agent<IN, OUT> : IN -> OUT
Every composition operator preserves type safety. If the types at a boundary do not match, the Kotlin compiler rejects the code.
Agent<A, B> : A -> B
A then B : Agent<X,Y> then Agent<Y,Z> -> Pipeline<X,Z>
A / B : Agent<X,Y> / Agent<X,Y> -> Parallel<X,Y> produces List<Y>
A * B : Agent<X,Y> * Agent<*,Z> -> Forum<X,Z>
A.loop { } : (Agent<X,Y> | Pipeline<X,Y>) -> Loop<X,Y> null = stop, X = continue
A.branch { } : Agent<X, Sealed<Y>> -> Branch<X,Z> all variants -> same Z
infix fun <A, B, C> Agent<A, B>.then(other: Agent<B, C>): Pipeline<A, C>The fundamental composition. Output of the first must match input of the second.
val parse = agent<RawText, Spec>("parse") { /* ... */ }
val generate = agent<Spec, Code>("generate") { /* ... */ }
val pipeline: Pipeline<RawText, Code> = parse then generateinfix fun <A, B, C> Pipeline<A, B>.then(other: Agent<B, C>): Pipeline<A, C>Extends an existing pipeline by appending an agent.
val extended: Pipeline<RawText, Review> = (parse then generate) then reviewNot a direct overload. Chain the agent into the pipeline using the Pipeline + Agent form by restructuring:
// Instead of: agent then pipeline
// Use: agent then firstAgent then secondAgentinfix fun <A, B, C> Pipeline<A, B>.then(other: Pipeline<B, C>): Pipeline<A, C>Joins two independently defined pipelines.
val frontend: Pipeline<Spec, Code> = parse then generate
val backend: Pipeline<Code, Deployed> = review then deploy
val full: Pipeline<Spec, Deployed> = frontend then backendinfix fun <A, B, C> Agent<A, B>.then(other: Parallel<B, C>): Pipeline<A, List<C>>
infix fun <A, B, C> Pipeline<A, B>.then(other: Parallel<B, C>): Pipeline<A, List<C>>The parallel fan-out transforms the output type to List<C>. Every agent in the parallel group receives the same input.
val parallel = reviewerA / reviewerB / reviewerC
// Parallel<Code, Review>
val pipeline: Pipeline<Spec, List<Review>> = generator then parallelinfix fun <A, B, C> Parallel<A, B>.then(other: Agent<List<B>, C>): Pipeline<A, C>
infix fun <A, B, C> Parallel<A, B>.then(other: Pipeline<List<B>, C>): Pipeline<A, C>The aggregator must accept List<B> as input. This is how you fan-in after a parallel fan-out.
val aggregator = agent<List<Review>, Report>("agg") { /* ... */ }
val pipeline: Pipeline<Code, Report> = parallel then aggregatorinfix fun <A, B, C> Agent<A, B>.then(other: Loop<B, C>): Pipeline<A, C>
infix fun <A, B, C> Pipeline<A, B>.then(other: Loop<B, C>): Pipeline<A, C>Feeds output into a loop.
val refineLoop = refiner.loop { result -> if (result.score >= 90) null else result.draft }
val pipeline: Pipeline<Spec, Refined> = generator then refineLoopinfix fun <A, B, C> Loop<A, B>.then(other: Agent<B, C>): Pipeline<A, C>
infix fun <A, B, C> Loop<A, B>.then(other: Pipeline<B, C>): Pipeline<A, C>Takes loop output and continues through the chain.
val pipeline: Pipeline<Draft, Published> = refineLoop then formatter then publisherinfix fun <A, B, C> Agent<A, B>.then(other: Branch<B, C>): Pipeline<A, C>
infix fun <A, B, C> Pipeline<A, B>.then(other: Branch<B, C>): Pipeline<A, C>Routes the output through type-based branching.
val afterReview = reviewer.branch {
on<Passed>() then deployer
on<Failed>() then reporter
}
val pipeline: Pipeline<Code, Notification> = coder then afterReviewinfix fun <A, B, C> Branch<A, B>.then(other: Agent<B, C>): Pipeline<A, C>
infix fun <A, B, C> Branch<A, B>.then(other: Pipeline<B, C>): Pipeline<A, C>All branch handlers produce the same OUT type, which feeds into the next stage.
infix fun <A, B, C> Agent<A, B>.then(other: Forum<B, C>): Pipeline<A, C>
infix fun <A, B, C> Pipeline<A, B>.then(other: Forum<B, C>): Pipeline<A, C>Feeds into a multi-agent deliberation group.
Connect forum output to subsequent processing by chaining through a pipeline.
operator fun <A, B> Agent<A, B>.div(other: Agent<A, B>): Parallel<A, B>
operator fun <A, B> Parallel<A, B>.div(other: Agent<A, B>): Parallel<A, B>All agents in a parallel group must share the same <IN, OUT> types.
val parallel = agentA / agentB / agentC
// Parallel<Input, Output>
val results: List<Output> = parallel(input)operator fun <A, B, C> Agent<A, B>.times(other: Agent<*, C>): Forum<A, C>
operator fun <A, B, C> Forum<A, B>.times(other: Agent<*, C>): Forum<A, C>The first agent's input type becomes the forum's input. The last agent's output type becomes the forum's output.
val forum = initiator * analyst * critic * captain
// Forum<Specs, Decision>fun <A, B> Agent<A, B>.loop(next: (B) -> A?): Loop<A, B>
fun <A, B> Pipeline<A, B>.loop(next: (B) -> A?): Loop<A, B>The next function receives the output and returns either the next input (A) to continue looping or null to stop.
val loop = refiner.loop { result ->
if (result.score >= 90) null else result.draft
}
// Loop<Draft, RefinedResult>fun <IN, SEALED, OUT> Agent<IN, SEALED>.branch(
block: BranchBuilder<OUT>.() -> Unit
): Branch<IN, OUT>Inside the builder:
inline fun <reified T : Any> on(): OnClause<T>
infix fun OnClause<T>.then(agent: Agent<T, OUT>)
infix fun OnClause<T>.then(pipeline: Pipeline<T, OUT>)All handlers must produce the same OUT type.
val branch = classifier.branch {
on<Positive>() then celebrator // Agent<Positive, Report>
on<Negative>() then consoler // Agent<Negative, Report>
}
// Branch<Input, Report>Cause: The output of one agent does not match the input of the next in a then chain.
val a = agent<String, Int>("a") { /* ... */ }
val b = agent<Double, String>("b") { /* ... */ }
// ERROR: a outputs Int, b expects Double
val broken = a then bFix: Ensure A.OUT == B.IN. Insert a conversion agent or fix the types.
Cause: The agent's OUT type does not match any skill's output type.
val a = agent<String, Int>("a") {
skills {
skill<String, String>("s", "desc") { // produces String, not Int
implementedBy { it }
}
}
}
// IllegalArgumentException: Agent "a" has no skill producing Int.Fix: Ensure at least one skill's OUT matches the agent's OUT.
Cause: You reused the same agent instance in two compositions.
val a = agent<String, String>("a") { /* ... */ }
val b = agent<String, String>("b") { /* ... */ }
a then b // a is now placed
a then c // ERROR: a is already placedFix: Create a new agent instance. Use a factory function for reuse. See Best Practices.
Cause: Parallel agents have different type signatures.
val a = agent<String, Int>("a") { /* ... */ }
val b = agent<String, String>("b") { /* ... */ }
// ERROR: a is Agent<String, Int>, b is Agent<String, String>
val broken = a / bFix: All parallel agents must share the same <IN, OUT> types. Use a common supertype or sealed interface if needed.
Cause: Runtime error when a sealed variant appears that has no on<X>() handler.
sealed interface Shape
data class Circle(val r: Double) : Shape
data class Square(val s: Double) : Shape
val branch = classifier.branch {
on<Circle>() then circleHandler
// Missing: on<Square>()
}
branch(inputThatProducesSquare) // RuntimeException: No branch defined for SquareFix: Handle all sealed variants in the branch builder.
Cause: The aggregator's input type is not List<B> after a parallel fan-out.
val parallel = reviewerA / reviewerB // Parallel<Code, Review>
// ERROR: aggregator expects Review, not List<Review>
val broken = parallel then agent<Review, Report>("agg") { /* ... */ }Fix: The aggregator must accept List<Review>:
val aggregator = agent<List<Review>, Report>("agg") { /* ... */ }
val pipeline = parallel then aggregator // OKAgent<A,B> then Agent<B,C> -> Pipeline<A,C>
Pipeline<A,B> then Agent<B,C> -> Pipeline<A,C>
Pipeline<A,B> then Pipeline<B,C> -> Pipeline<A,C>
Agent<A,B> then Parallel<B,C> -> Pipeline<A, List<C>>
Pipeline<A,B> then Parallel<B,C> -> Pipeline<A, List<C>>
Parallel<A,B> then Agent<List<B>,C> -> Pipeline<A,C>
Parallel<A,B> then Pipeline<List<B>,C> -> Pipeline<A,C>
Agent<A,B> then Loop<B,C> -> Pipeline<A,C>
Pipeline<A,B> then Loop<B,C> -> Pipeline<A,C>
Loop<A,B> then Agent<B,C> -> Pipeline<A,C>
Loop<A,B> then Pipeline<B,C> -> Pipeline<A,C>
Agent<A,B> then Branch<B,C> -> Pipeline<A,C>
Pipeline<A,B> then Branch<B,C> -> Pipeline<A,C>
Branch<A,B> then Agent<B,C> -> Pipeline<A,C>
Branch<A,B> then Pipeline<B,C> -> Pipeline<A,C>
Agent<A,B> then Forum<B,C> -> Pipeline<A,C>
Pipeline<A,B> then Forum<B,C> -> Pipeline<A,C>
Agent<A,B> / Agent<A,B> -> Parallel<A,B>
Parallel<A,B> / Agent<A,B> -> Parallel<A,B>
Agent<A,B> * Agent<*,C> -> Forum<A,C>
Forum<A,B> * Agent<*,C> -> Forum<A,C>
Agent<A,B>.loop { B -> A? } -> Loop<A,B>
Pipeline<A,B>.loop { B -> A? } -> Loop<A,B>
Agent<A,S>.branch { on<V>()... } -> Branch<A,OUT>
See also: Composition: Pipeline | API Quick Reference | Troubleshooting
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