Skip to content

Cookbook

skobeltsyn edited this page Mar 28, 2026 · 1 revision

Cookbook: Recipes for Common Scenarios

Complete, runnable code for 11 patterns. Each recipe is self-contained -- copy, adapt, and ship.


Recipe 1: ETL Pipeline (Parse CSV, Transform, Validate)

Three-stage pipeline: extract records from CSV, transform into domain objects, validate constraints.

import agents_engine.core.agent
import agents_engine.composition.pipeline.then

data class CsvInput(val raw: String)

data class Record(val name: String, val age: Int, val email: String)

data class ValidatedBatch(val valid: List<Record>, val rejected: List<String>)

val extract = agent<CsvInput, List<Record>>("extract") {
    skills {
        skill<CsvInput, List<Record>>("parse-csv", "Parses CSV text into Record objects") {
            implementedBy { input ->
                input.raw.lines()
                    .drop(1) // skip header
                    .filter { it.isNotBlank() }
                    .map { line ->
                        val cols = line.split(",").map { it.trim() }
                        Record(cols[0], cols[1].toInt(), cols[2])
                    }
            }
        }
    }
}

val transform = agent<List<Record>, List<Record>>("transform") {
    skills {
        skill<List<Record>, List<Record>>("normalize", "Normalizes records: trims names, lowercases emails") {
            implementedBy { records ->
                records.map { it.copy(name = it.name.trim(), email = it.email.lowercase()) }
            }
        }
    }
}

val validate = agent<List<Record>, ValidatedBatch>("validate") {
    skills {
        skill<List<Record>, ValidatedBatch>("validate-batch", "Validates age > 0 and email contains @") {
            implementedBy { records ->
                val (good, bad) = records.partition { it.age > 0 && "@" in it.email }
                ValidatedBatch(valid = good, rejected = bad.map { "${it.name}: invalid" })
            }
        }
    }
}

val etl = extract then transform then validate

fun main() {
    val csv = CsvInput("name,age,email\nAlice,30,alice@test.com\nBob,-1,bob\nCharlie,25,charlie@test.com")
    val result = etl(csv)
    println("Valid: ${result.valid}")
    println("Rejected: ${result.rejected}")
}

Recipe 2: LLM Calculator with Tools

An agentic agent that solves arithmetic using tool calls. Uses a mock ModelClient for deterministic testing.

import agents_engine.core.agent
import agents_engine.model.*

fun num(args: Map<String, Any?>, key: String): Double =
    (args[key] as? Number)?.toDouble() ?: error("Missing $key")

val calculator = agent<String, String>("calculator") {
    prompt("You are a calculator. Use the provided tools to evaluate expressions step by step.")
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 10 }

    tools {
        tool("add",      "Add two numbers. Args: a, b")             { args -> num(args, "a") + num(args, "b") }
        tool("subtract", "Subtract b from a. Args: a, b")           { args -> num(args, "a") - num(args, "b") }
        tool("multiply", "Multiply two numbers. Args: a, b")        { args -> num(args, "a") * num(args, "b") }
        tool("divide",   "Divide a by b. Args: a, b")               { args -> num(args, "a") / num(args, "b") }
    }

    skills {
        skill<String, String>("solve", "Evaluate arithmetic expressions using tools") {
            tools("add", "subtract", "multiply", "divide")
        }
    }

    onToolUse { name, args, result ->
        println("  $name(${args.values.joinToString(", ")}) = $result")
    }
}

Test with mock:

@Test
fun `calculator solves addition with mock`() {
    var turn = 0
    val mock = ModelClient { messages ->
        when (turn++) {
            0 -> LlmResponse.ToolCalls(listOf(ToolCall("add", mapOf("a" to 15.0, "b" to 35.0))))
            else -> LlmResponse.Text("The result is 50.0")
        }
    }

    val calc = agent<String, String>("calc") {
        prompt("Calculator")
        model { ollama("test"); client = mock }
        budget { maxTurns = 5 }
        tools { tool("add", "Add a + b") { args -> num(args, "a") + num(args, "b") } }
        skills { skill<String, String>("solve", "Solve") { tools("add") } }
    }

    assertEquals("The result is 50.0", calc("15 + 35"))
}

Recipe 3: Multi-Reviewer Code Pipeline (Parallel Fan-Out + Aggregator)

Three reviewers run in parallel on the same code. An aggregator merges the results.

import agents_engine.core.agent
import agents_engine.composition.pipeline.then
import agents_engine.composition.parallel.div

data class Code(val source: String)
data class Review(val reviewer: String, val passed: Boolean, val notes: String)
data class Report(val allPassed: Boolean, val summary: String)

fun createReviewer(name: String, check: (Code) -> Boolean): Agent<Code, Review> =
    agent<Code, Review>(name) {
        skills {
            skill<Code, Review>("review", "$name review") {
                implementedBy { code ->
                    Review(name, check(code), if (check(code)) "OK" else "Issues found")
                }
            }
        }
    }

val security    = createReviewer("security")    { "eval" !in it.source }
val style       = createReviewer("style")       { it.source.length < 1000 }
val performance = createReviewer("performance") { "sleep" !in it.source }

val parallel = security / style / performance
// Parallel<Code, Review>

val aggregator = agent<List<Review>, Report>("aggregator") {
    skills {
        skill<List<Review>, Report>("merge", "Merges reviews into a report") {
            implementedBy { reviews ->
                Report(
                    allPassed = reviews.all { it.passed },
                    summary = reviews.joinToString("\n") { "${it.reviewer}: ${it.notes}" }
                )
            }
        }
    }
}

val pipeline = parallel then aggregator
// Pipeline<Code, Report>

fun main() {
    val report = pipeline(Code("fun hello() = println(\"Hello\")"))
    println("All passed: ${report.allPassed}")
    println(report.summary)
}

Recipe 4: Quality Gate Loop (Generate, Evaluate, Loop Until Threshold)

A generator and evaluator form a pipeline that loops until quality reaches the threshold.

import agents_engine.core.agent
import agents_engine.composition.pipeline.then
import agents_engine.composition.loop.loop

data class Spec(val description: String, val iteration: Int = 0)
data class Draft(val content: String, val spec: Spec)
data class Evaluation(val quality: Float, val draft: Draft)

val generate = agent<Spec, Draft>("generate") {
    skills {
        skill<Spec, Draft>("gen", "Generates a draft from spec") {
            implementedBy { spec ->
                // Simulate improvement with each iteration
                val quality = "x".repeat(minOf(spec.iteration * 20 + 50, 100))
                Draft("Draft v${spec.iteration}: $quality", spec)
            }
        }
    }
}

val evaluate = agent<Draft, Evaluation>("evaluate") {
    skills {
        skill<Draft, Evaluation>("eval", "Scores draft quality 0-100") {
            implementedBy { draft ->
                val score = minOf(draft.spec.iteration * 20f + 50f, 100f)
                Evaluation(score, draft)
            }
        }
    }
}

val qualityGate = (generate then evaluate).loop { result ->
    if (result.quality >= 90f) null
    else result.draft.spec.copy(iteration = result.draft.spec.iteration + 1)
}

fun main() {
    val result = qualityGate(Spec("Build a REST API"))
    println("Final quality: ${result.quality}")
    println("Iterations: ${result.draft.spec.iteration}")
}

Recipe 5: Sealed Type Router (Classify, Then Branch to Handlers)

A classifier agent produces a sealed type. A branch routes each variant to a specialized handler.

import agents_engine.core.agent
import agents_engine.composition.branch.branch

sealed interface Sentiment
data class Positive(val text: String, val confidence: Double) : Sentiment
data class Negative(val text: String, val confidence: Double) : Sentiment
data class Neutral(val text: String) : Sentiment

data class Response(val message: String)

val classifier = agent<String, Sentiment>("classifier") {
    skills {
        skill<String, Sentiment>("classify", "Classifies text sentiment") {
            implementedBy { text ->
                when {
                    text.contains("great", ignoreCase = true) -> Positive(text, 0.9)
                    text.contains("terrible", ignoreCase = true) -> Negative(text, 0.8)
                    else -> Neutral(text)
                }
            }
        }
    }
}

val celebrator = agent<Positive, Response>("celebrator") {
    skills {
        skill<Positive, Response>("celebrate", "Responds to positive sentiment") {
            implementedBy { Response("Glad to hear! (confidence: ${it.confidence})") }
        }
    }
}

val consoler = agent<Negative, Response>("consoler") {
    skills {
        skill<Negative, Response>("console", "Responds to negative sentiment") {
            implementedBy { Response("Sorry to hear that. We'll improve. (confidence: ${it.confidence})") }
        }
    }
}

val neutral = agent<Neutral, Response>("neutral") {
    skills {
        skill<Neutral, Response>("ack", "Acknowledges neutral feedback") {
            implementedBy { Response("Thanks for your feedback.") }
        }
    }
}

val routed = classifier.branch {
    on<Positive>() then celebrator
    on<Negative>() then consoler
    on<Neutral>()  then neutral
}

fun main() {
    println(routed("This product is great!").message)
    println(routed("This is terrible").message)
    println(routed("It works fine").message)
}

Recipe 6: Agent with Persistent Memory (Fibonacci)

A single agent with no custom tools -- just memory tools and a system prompt that teaches it the algorithm.

import agents_engine.core.MemoryBank
import agents_engine.core.agent
import agents_engine.model.*

val bank = MemoryBank()

val fib = agent<String, Int>("fibonacci") {
    prompt("""You maintain a Fibonacci sequence in memory.
Memory format: "prev|curr". Empty memory means start fresh.

1. Call memory_read
2. If empty -> answer=1, write "0|1"
   If "A|B" -> answer=A+B, write "B|A+B"
3. Call memory_write with the new state
4. Reply with ONLY the answer number""")
    memory(bank)
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 5 }
    skills {
        skill<String, Int>("fib", "Generate next Fibonacci number") {
            tools()   // memory_read, memory_write, memory_search are auto-available
            transformOutput { it.trim().toInt() }
        }
    }
}

// Each call advances the sequence:
// fib("next") -> 1  (bank: "0|1")
// fib("next") -> 1  (bank: "1|1")
// fib("next") -> 2  (bank: "1|2")
// fib("next") -> 3  (bank: "2|3")

// Pre-seed to resume from any point:
// bank.write("fibonacci", "21|34")
// fib("next") -> 55

Recipe 7: Shared Knowledge Across Agents (Product Catalog)

Two agents share the same data source via knowledge lambdas that close over shared state.

import agents_engine.core.agent
import agents_engine.composition.pipeline.then

val catalog = mapOf(
    "products" to """
        | ID  | Name              | Price | Category    | In Stock |
        |-----|-------------------|-------|-------------|----------|
        | P01 | Kotlin In Action  | 45    | Books       | yes      |
        | P02 | Mechanical KB     | 120   | Electronics | yes      |
        | P03 | Espresso Machine  | 299   | Appliances  | no       |
        | P04 | USB-C Hub         | 35    | Electronics | yes      |
        | P05 | Clean Code        | 40    | Books       | yes      |
    """.trimIndent(),
    "policies" to """
        - Only recommend products that are in stock.
        - Budget limit must be respected.
        - Prefer variety across categories when possible.
    """.trimIndent(),
)

val recommender = agent<String, String>("recommender") {
    prompt("Recommend 1-2 products by ID and name. Follow the policies.")
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 5 }
    skills {
        skill<String, String>("recommend", "Recommend products from catalog") {
            tools()
            knowledge("products", "Full product catalog with prices and stock") { catalog["products"]!! }
            knowledge("policies", "Recommendation rules") { catalog["policies"]!! }
        }
    }
}

val validator = agent<String, String>("validator") {
    prompt("Verify recommendations: products exist, are in stock, within budget. Reply VALID or INVALID: reason.")
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 5 }
    skills {
        skill<String, String>("validate", "Validate recommendations against catalog") {
            tools()
            knowledge("products", "Full product catalog with prices and stock") { catalog["products"]!! }
            knowledge("policies", "Recommendation rules") { catalog["policies"]!! }
        }
    }
}

val pipeline = recommender then validator
// pipeline("I want electronics, budget 100 dollars")

Both agents load the same catalog on demand. The recommender picks products; the validator cross-checks them against the same source of truth.


Recipe 8: Sign-In Agent with Knowledge and Tools

An agentic agent that looks up credentials from knowledge and builds a sign-in request using a tool.

import agents_engine.core.agent

val signIn = agent<String, String>("sign-in-agent") {
    prompt("""You are a sign-in assistant. To build a sign-in request you MUST:
1) Call the passwords tool to look up credentials
2) Call build_request with the login and password from that lookup
After build_request returns the JSON, reply with ONLY that JSON.""")
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 6 }

    tools {
        tool("build_request", "Build a JSON request body. Arguments: login (string), password (string).") { args ->
            val entries = args.entries.joinToString(",") { (k, v) -> "\"$k\":\"$v\"" }
            "{$entries}"
        }
    }

    skills {
        skill<String, String>("sign-in", "Build a sign-in request JSON for the given login") {
            tools("build_request")
            knowledge("passwords", "User credentials store. Call this to look up passwords.") {
                """
                john@example.com : s3cretPass!
                alice@corp.net   : Al1c3_2024
                bob@test.org     : b0bRul3z
                """.trimIndent()
            }
        }
    }

    onKnowledgeUsed { name, _ -> println("[knowledge] $name loaded") }
    onToolUse { name, args, result -> println("[tool] $name($args) = $result") }
}

// signIn("Create a sign-in request for john@example.com")
// -> {"login":"john@example.com","password":"s3cretPass!"}

Recipe 9: Testing an Agentic Agent (Mock ModelClient)

Complete test setup with a mock that simulates a multi-turn tool-calling conversation.

import agents_engine.core.agent
import agents_engine.model.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class CalculatorAgentTest {

    private fun num(args: Map<String, Any?>, key: String): Double =
        (args[key] as? Number)?.toDouble() ?: error("Missing $key")

    @Test
    fun `agent calls add tool and returns result`() {
        val toolCalls = mutableListOf<String>()
        var turn = 0

        val mock = ModelClient { messages ->
            when (turn++) {
                0 -> LlmResponse.ToolCalls(listOf(
                    ToolCall("add", mapOf("a" to 10.0, "b" to 20.0))
                ))
                else -> LlmResponse.Text("30")
            }
        }

        val calc = agent<String, String>("calc") {
            prompt("Calculator")
            model { ollama("test-model"); client = mock }
            budget { maxTurns = 5 }
            tools {
                tool("add", "Add a + b") { args ->
                    toolCalls.add("add")
                    num(args, "a") + num(args, "b")
                }
            }
            skills {
                skill<String, String>("solve", "Solve arithmetic") {
                    tools("add")
                }
            }
        }

        val result = calc("What is 10 + 20?")
        assertEquals("30", result)
        assertTrue(toolCalls.contains("add"))
    }

    @Test
    fun `agent respects budget limit`() {
        val infiniteToolCaller = ModelClient { _ ->
            LlmResponse.ToolCalls(listOf(ToolCall("noop", emptyMap())))
        }

        val agent = agent<String, String>("budget-test") {
            model { ollama("test"); client = infiniteToolCaller }
            budget { maxTurns = 3 }
            tools { tool("noop", "No-op") { "ok" } }
            skills { skill<String, String>("s", "s") { tools("noop") } }
        }

        val exception = assertThrows<BudgetExceededException> { agent("go") }
        assertTrue(exception.message!!.contains("3 turns"))
    }
}

Recipe 10: Typed Output from LLM (@Generable + transformOutput)

Use @Generable and @Guide to get structured output from an LLM.

import agents_engine.core.agent
import agents_engine.generation.*

@Generable("Quality assessment of generated code")
data class QualityReport(
    @Guide("Score from 0.0 to 1.0. Below 0.6 means fail.") val score: Double,
    @Guide("One of: excellent, good, acceptable, poor") val grade: String,
    @Guide("List of specific issues, or empty if none") val issues: List<String>,
)

// Check the generated artifacts:
// QualityReport::class.toLlmDescription()   -> markdown with field descriptions
// QualityReport::class.jsonSchema()          -> JSON Schema for constrained decoding
// QualityReport::class.promptFragment()      -> prompt template with field hints

val reviewer = agent<String, QualityReport>("reviewer") {
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 3 }
    skills {
        skill<String, QualityReport>("review", "Reviews code and produces a quality report") {
            tools()
            // Framework auto-injects promptFragment() into system prompt
            // and uses fromLlmOutput<QualityReport>() for parsing
        }
    }
}

// Or use explicit transformOutput for custom parsing:
val reviewerExplicit = agent<String, QualityReport>("reviewer-explicit") {
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 3 }
    skills {
        skill<String, QualityReport>("review", "Reviews code quality") {
            tools()
            transformOutput { raw ->
                fromLlmOutput<QualityReport>(raw)
                    ?: QualityReport(0.0, "poor", listOf("Failed to parse review"))
            }
        }
    }
}

// PartiallyGenerated for streaming:
val partial = PartiallyGenerated.empty<QualityReport>()
    .withField("score", 0.85)
    .withField("grade", "good")

println(partial.has("score"))           // true
println(partial.has("issues"))          // false
println(partial.arrivedFieldNames)      // [score, grade]

val complete = partial.withField("issues", emptyList<String>()).toComplete()
println(complete)  // QualityReport(score=0.85, grade=good, issues=[])

Recipe 11: Error-Resilient Agent (onError with Hybrid Repair)

Uses the onError DSL to handle malformed tool calls with a deterministic fix first, falling through to an LLM-driven repair agent.

import agents_engine.core.agent
import agents_engine.model.*

// Deterministic JSON cleanup
fun tryJsonCleanup(raw: String): String? {
    val cleaned = raw
        .replace(Regex("```json\\s*"), "")
        .replace(Regex("```"), "")
        .replace(Regex(",\\s*}"), "}")
        .replace(Regex(",\\s*]"), "]")
        .trim()
    return if (cleaned.startsWith("{") && cleaned.endsWith("}")) cleaned else null
}

// LLM-driven repair agent (same Agent<String, String> interface)
val jsonFixer = agent<String, String>("json-fixer") {
    skills {
        skill<String, String>("cleanup", "Fixes common JSON issues deterministically") {
            implementedBy { input ->
                tryJsonCleanup(input) ?: error("Cannot fix: not JSON")
            }
        }
    }
}

// The main agent with hybrid error recovery
val coder = agent<String, String>("coder") {
    prompt("Write Kotlin code using the write_file tool.")
    model { ollama("qwen2.5:7b"); temperature = 0.0 }
    budget { maxTurns = 10 }

    tools {
        tool("write_file", "Write code to a file. Args: path, code") { args ->
            val path = args["path"]?.toString() ?: error("Missing path")
            val code = args["code"]?.toString() ?: error("Missing code")
            "Wrote ${code.length} chars to $path"
        }

        // Tool-level defaults for error recovery
        // (applied when the onError DSL is fully implemented)
        // defaults {
        //     onError {
        //         invalidArgs { raw, error ->
        //             // Try deterministic fix first
        //             fix { tryJsonCleanup(raw) }
        //                 ?: fix(agent = jsonFixer, retries = 2)
        //         }
        //         executionError { e ->
        //             retry(maxAttempts = 3)
        //         }
        //     }
        // }
    }

    skills {
        skill<String, String>("write-code", "Write production Kotlin code") {
            tools("write_file")
        }
    }

    onToolUse { name, args, result -> println("$name -> $result") }
}

Index

# Recipe Key Concepts
1 ETL Pipeline then, pure Kotlin skills, data flow
2 LLM Calculator tools, agentic skills, onToolUse, mock testing
3 Multi-Reviewer Pipeline / (parallel), List<OUT>, aggregator, factory functions
4 Quality Gate Loop .loop {}, feedback, termination condition
5 Sealed Type Router .branch {}, on<Variant>(), sealed interfaces
6 Persistent Memory MemoryBank, memory(), auto-injected tools
7 Shared Knowledge knowledge(), shared lambdas, lazy loading
8 Sign-In Agent Knowledge + tools, observability hooks
9 Testing Agentic Agents Mock ModelClient, BudgetExceededException
10 Typed Output @Generable, @Guide, fromLlmOutput, PartiallyGenerated
11 Error-Resilient Agent onError, deterministic fix, LLM repair, defaults

See also: API Quick Reference | Best Practices | Type Algebra | Troubleshooting

Clone this wiki locally