-
Notifications
You must be signed in to change notification settings - Fork 0
Cookbook
Complete, runnable code for 11 patterns. Each recipe is self-contained -- copy, adapt, and ship.
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}")
}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"))
}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)
}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}")
}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)
}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") -> 55Two 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.
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!"}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"))
}
}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=[])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") }
}| # | 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
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