Skip to content

Type Algebra

skobeltsyn edited this page Mar 28, 2026 · 1 revision

Type Algebra Cheat Sheet

This page documents every then overload, all composition operator type rules, and the most common type errors you will encounter.


Core Type Rule

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.


Algebra Summary

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

All then Overloads

Agent + Agent

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 generate

Pipeline + Agent

infix 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 review

Agent + Pipeline

Not 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 secondAgent

Pipeline + Pipeline

infix 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 backend

Agent/Pipeline + Parallel

infix 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 parallel

Parallel + Agent/Pipeline

infix 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 aggregator

Agent/Pipeline + Loop

infix 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 refineLoop

Loop + Agent/Pipeline

infix 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 publisher

Agent/Pipeline + Branch

infix 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 afterReview

Branch + Agent/Pipeline

infix 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.

Agent/Pipeline + Forum

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.

Forum + Agent (via Pipeline)

Connect forum output to subsequent processing by chaining through a pipeline.


Parallel (Fan-Out) Operator

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)

Forum (Deliberation) Operator

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>

Loop Operator

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>

Branch Operator

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>

Common Type Errors

"Type mismatch: inferred type is Agent<X, Y> but Agent<Z, ???> was expected"

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 b

Fix: Ensure A.OUT == B.IN. Insert a conversion agent or fix the types.

"No skill producing X"

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.

"Agent X is already placed in Y"

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 placed

Fix: Create a new agent instance. Use a factory function for reuse. See Best Practices.

"None of the following candidates is applicable" for div operator

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 / b

Fix: All parallel agents must share the same <IN, OUT> types. Use a common supertype or sealed interface if needed.

"No branch defined for X"

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 Square

Fix: Handle all sealed variants in the branch builder.

"Type mismatch for aggregator after Parallel"

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  // OK

Quick Reference Card

Agent<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

Clone this wiki locally