Skip to main content

Aigentic helps you buildproduction-readyAI agents faster

A Kotlin DSL for AI agents that are typed, testable and work with any model

Intuitive Kotlin DSL

Describe the task, give the agent tools, and get a Kotlin data class back. Here's a complete agent and what happens when it runs.

  • agent<I, O>Typed input in, typed output out. Your data classes tell the model what to return.
  • addToolTools are plain Kotlin lambdas. The agent decides when to call them.
  • run.outcomeEvery run ends as Finished, Stuck or Fatal, so errors come back as values.
Provider
WeatherAgent.kt
@AigenticParameter
data class Weather(val celsius: Double, val summary: String)
@AigenticParameter
data class WeatherRequest(val city: String)
val weatherAgent = agent<String, Weather> {
geminiModel {
apiKey("YOUR_API_KEY")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
task("Answer questions about the weather") {
addInstruction("Use getWeather for live data")
}
addTool("getWeather") { req: WeatherRequest ->
currentWeather(req.city)
}
}
suspend fun main() {
val run = weatherAgent.start("Weather in Amsterdam?")
when (val outcome = run.outcome) {
is Outcome.Finished -> println(outcome.response)
is Outcome.Stuck -> println(outcome.reason)
is Outcome.Fatal -> println(outcome.message)
}
}
Run
  1. Input

    “Weather in Amsterdam?”

  2. Model
    Gemini 3.5 Flashcalls getWeather
  3. Tool call
    getWeather(city = "Amsterdam")"14.0°C, light rain in Amsterdam"
  4. Outcome.Finished
    Weather(celsius = 14.0, summary = "Light rain")

run.tokenUsage()412 input, 38 output tokens

Pick a provider or let it cycle. Each step lights up the code that drives it, and only the model block changes.

Why Aigentic

Most agent libraries help you build the demo. Aigentic is built for what comes after: typed agents in your existing Kotlin code, any provider, and regression tests from real runs. Here's the same weather agent written both ways.

By hand

Simplified sketch
// Simplified: one provider, one tool, happy path only
val tools = buildJsonArray {
addJsonObject {
put("type", "function")
putJsonObject("function") {
put("name", "getWeather")
putJsonObject("parameters") {
put("type", "object")
putJsonObject("properties") {
putJsonObject("city") { put("type", "string") }
}
}
}
}
}
val messages = mutableListOf<ChatMessage>(UserMessage("Weather in Amsterdam?"))
while (true) {
val reply = http.post("https://api.openai.com/v1/chat/completions") {
bearerAuth(apiKey)
setBody(ChatRequest("gpt-5.5", messages, tools))
}.body<ChatResponse>().choices.first().message
messages += reply
val call = reply.toolCalls?.firstOrNull() ?: break
val args = Json.decodeFromString<WeatherRequest>(call.function.arguments)
messages += ToolMessage(call.id, currentWeather(args.city))
}
// Hope the last reply is valid JSON in the right shape
val weather = Json.decodeFromString<Weather>(messages.last().content!!)
  • JSON schemas written by hand
  • Your own tool-call loop
  • Parse the reply and hope it's valid
  • Rewrite it all for another provider

With Aigentic

Same agent
@AigenticParameter
data class WeatherRequest(val city: String)
val weatherAgent = agent<String, Weather> {
openAIModel {
apiKey(apiKey)
modelIdentifier(OpenAIModelIdentifier.GPT5_5)
}
task("Answer questions about the weather") {}
addTool("getWeather") { req: WeatherRequest ->
currentWeather(req.city)
}
}
val run = weatherAgent.start("Weather in Amsterdam?")
  • Schemas come from your data classes
  • Aigentic runs the loop for you
  • A typed Weather back, or Stuck or Fatal
  • Change one block to switch provider

Built by Flock, in the open

Aigentic is open source. Watch the talk for a walkthrough, or start from the Initializr to get a project with the right dependencies.

From first agent to production

Connect your APIs, read documents, chain agents and test the results.

Tools from your API

Already have an API? Point openApiTools at its OpenAPI spec and every operation becomes a tool the agent can call. Add authentication headers once, for every call.

Read about integrations
OrderAgent.kt
val orderAgent = agent<String, Answer> {
geminiModel {
apiKey("YOUR_API_KEY")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
task("Answer questions about customer orders") {
addInstruction("Look up the order before you answer")
}
openApiTools(File("orders-api.json").readText()) {
addHeader(Header.Bearer("YOUR_API_TOKEN"))
}
}
val run = orderAgent.start("Where is order 1042?")

Structured data from documents

Pass a PDF or an image to start() and get a typed data class back. Use @Description to tell the model what a field means. There's no JSON to parse and no tool to write. PDFs work with Gemini and VertexAI today.

Read about attachments
InvoiceExtractor.kt
@AigenticParameter
data class InvoiceLine(val description: String, val total: Double)
@AigenticParameter
data class Invoice(
val invoiceNumber: String,
val lines: List<InvoiceLine>,
@Description("Total amount before tax")
val totalAmount: Double,
val tax: Double,
)
val invoiceExtractor = agent<Unit, Invoice> {
geminiModel {
apiKey("YOUR_API_KEY")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
task("Extract the invoice from the attached PDF") {
addInstruction("Include every line item")
}
}
val run = invoiceExtractor.start(Attachment.Base64.pdf(invoicePdf))
val invoice = (run.outcome as? Outcome.Finished)?.response

Workflows

Chain agents with thenProcess. The output type of one agent is the input type of the next, so the compiler catches a broken chain before you run it. Each agent can use its own model.

Explore workflows
DocumentWorkflow.kt
@AigenticParameter
data class Document(val content: String)
@AigenticParameter
data class Analysis(val topics: List<String>, val tone: String)
@AigenticParameter
data class Summary(val text: String)
val analyzer = agent<Document, Analysis> {
geminiModel {
apiKey("YOUR_API_KEY")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
task("Find the key topics and the overall tone") {}
}
val summarizer = agent<Analysis, Summary> {
openAIModel {
apiKey("YOUR_API_KEY")
modelIdentifier(OpenAIModelIdentifier.GPT5_5)
}
task("Summarize the analysis in under 100 words") {}
}
val workflow = analyzer thenProcess summarizer
val run = workflow.start(Document("Raw document text..."))

Test before you ship

Tag good runs on the Aigentic platform, then replay them as regression tests whenever you change a prompt or a model. Tool calls return the recorded results, so tests never touch your real systems. Flock hosts the platform and gives access on request.

See the testing example
WeatherAgentTest.kt
val weatherAgent = agent<String, Weather> {
// model, task and tools as above
platform {
name("weather-agent")
secret("YOUR_PLATFORM_SECRET")
}
}
val report = regressionTest {
agent(weatherAgent)
tags("validated")
numberOfIterations(3)
}.start()
check(report.failures.isEmpty() && report.errors.isEmpty())
OrderAgent.kt
val orderAgent = agent<String, Answer> {
geminiModel {
apiKey("YOUR_API_KEY")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
task("Answer questions about customer orders") {
addInstruction("Look up the order before you answer")
}
openApiTools(File("orders-api.json").readText()) {
addHeader(Header.Bearer("YOUR_API_TOKEN"))
}
}
val run = orderAgent.start("Where is order 1042?")