Skip to main content

Agents

Aigentic provides a powerful DSL for creating and configuring AI agents. The DSL is designed to be expressive, type-safe, and easy to use, allowing you to create sophisticated AI agents with minimal boilerplate code.

Creating an Agent​

The main entry point for creating an agent is the agent function, which takes a lambda with AgentConfig as the receiver:

val myAgent = agent {
geminiModel {
apiKey("your-api-key")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
task("Answer questions about the weather") {
addInstruction("Provide accurate and concise weather information")
addInstruction("If you don't know the answer, say so")
}
addTool(weatherTool)
}

Agent Configuration​

The AgentConfig class provides several methods for configuring an agent:

Setting the Model​

agent {
openAIModel {
apiKey("your-api-key")
modelIdentifier(OpenAIModelIdentifier.GPT5_5)
}
}

Each provider function, like openAIModel { }, sets the agent's model. To use your own Model implementation, pass it to model(...), see Bring your own model.

See Providers for thinkingBudget() and thinkingLevel(), the reasoning-effort options available in generationConfig.

Defining Tasks​

Tasks define what the agent should do. A task has a description and can have multiple instructions:

agent {
task("Summarize text") {
addInstruction("Create a concise summary of the provided text")
addInstruction("Highlight key points and main ideas")
addInstruction("Keep the summary under 200 words")
}
}

Adding Tools​

Tools extend an agent's capabilities by allowing it to perform specific actions:

agent {
// Add a pre-defined tool
addTool(weatherTool)

// Add a tool with inline definition
addTool<WeatherRequest, WeatherResponse>(
name = "getWeather",
description = "Get the current weather for a location",
) { request ->
// Tool implementation
weatherService.getWeather(request.location)
}
}

Adding Context​

Context provides additional information to the agent that applies to all runs of the agent:

agent {
context {
addText("This is some background information the agent should know.")
addBase64(encodedImage, MimeType.PNG)
}
}

Note: Context is built into the agent configuration and applies to every execution. For files that vary per request, use attachments in the start() function instead. This way you can reuse your agent for different requests.

Overriding the System Prompt​

The system prompt defines the agent's behavior, capabilities, and personality. It's the first message sent to the LLM and sets the foundation for how the agent will operate.

Aigentic provides a default system prompt through AgenticSystemPromptBuilder, or StructuredOutputPromptBuilder when the agent has an output type and no tools. The default includes instructions about:

  • The agent's purpose and mode of operation
  • How to use tools
  • Task description and specific instructions
  • Task completion protocol

You only need to override the system prompt when you want to customize the agent's behavior beyond the default settings.


val customSystemPromptBuilder = object : SystemPromptBuilder {
override fun <I : Any, O : Any> buildSystemPrompt(agent: Agent<I, O>): Message.SystemPrompt {
return Message.SystemPrompt("""
You are a specialized customer service agent for a tech company.

Your primary task is: ${agent.task.description}

Follow these specific instructions:
${agent.task.instructions.joinToString("\n") { "- ${it.text}" }}

Always be polite, concise, and helpful. Use a friendly tone.
When you've completed your task, use the ${FINISHED_TASK_TOOL_NAME} tool.
If you're stuck, use the ${STUCK_WITH_TASK_TOOL_NAME} tool.
""".trimIndent())
}
}

// Creating a custom system prompt builder
agent {
systemPrompt(customSystemPromptBuilder)
}

The SystemPromptBuilder interface has a single method buildSystemPrompt(agent: Agent<I, O>) that returns a Message.SystemPrompt. This gives you access to the agent's configuration, including its task, instructions, and tools, allowing you to create a highly customized system prompt.

Configuring Response Format​

You can specify the format for the agent's final response using generic types:

@AigenticParameter
data class Answer(val answer: String)

// Using generic types to specify the output type
val myAgent = agent<Unit, Answer> {
// Agent configuration
}

// If you need to specify both input and output types
@AigenticParameter
data class Question(val text: String)
val myAgent = agent<Question, Answer> {
// Agent configuration
}

// If you don't specify types, the default is Agent<Unit, Unit>
val myAgent = agent {
// Agent configuration
}

An agent without an output type needs at least one tool. Building it without one throws Cannot build Agent, property 'tools' is missing.

When using the generic types approach, the agent will automatically be configured to handle the specified input and output types.

Structured output with Gemini/Vertex AI

When you define an output type (e.g., agent<Input, Output>) and don’t add tools, Aigentic will use the provider’s structured output mode for Gemini and Vertex AI. This helps the model return more complex, nested JSON responses than the normal chat/tool-calling flow.

  • Add tools → normal tool-calling flow (no structured output)
  • No tools → structured output request (Gemini/Vertex AI only)

If you hit Gemini errors like MALFORMED_FUNCTION_CALL, try removing tools and rely on structured output.

Running an Agent​

To run an agent, use the start function which returns an AgentRun:

val agent = agent<String, Answer> {
// Agent configuration
}

val run = agent.start("What is Kotlin?")

Running an Agent with Attachments​

You can provide files and images to agents using the attachments parameter. Aigentic provides convenient factory functions for creating attachments:

val agent = agent {
geminiModel {
apiKey("your-api-key")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
task("Extract information from the provided document") {
addInstruction("Read the document and extract key information")
}
addTool(extractionTool)
}

// Pass a PDF attachment
val run = agent.start(Attachment.Base64.pdf(pdfBase64Content))

// Pass multiple attachments
val run = agent.start(
Attachment.Base64.pdf(invoiceBase64),
Attachment.Base64.jpeg(receiptBase64),
Attachment.Url.png("https://example.com/signature.png")
)

Available attachment factory functions:

  • Base64 attachments: Attachment.Base64.pdf(), jpeg(), png(), webp(), heic(), heif()
  • URL attachments: Attachment.Url.pdf(), jpeg(), png(), webp(), heic(), heif()

Note: The OpenAI and Ollama providers don't support PDF attachments yet. A PDF in the attachments or the agent's context ends the run with Outcome.Fatal before any request is sent. Attach the pages as images instead, or use the Gemini or VertexAI provider. Progress is tracked in #176.

You can also specify input data along with attachments:

@AigenticParameter
data class ProcessingRequest(val instruction: String)

val documentAgent = agent<ProcessingRequest, Answer> {
// Agent configuration
}

val run = documentAgent.start(
ProcessingRequest("Please summarize this document"),
Attachment.Base64.pdf(documentBase64)
)

Processing Agent Results​

The AgentRun contains everything you need to work with the agent's execution:

when (val outcome = run.outcome) {
is Outcome.Finished -> {
val answer = outcome.response?.answer
println("The agent's answer is: $answer")
}
is Outcome.Stuck -> println("Agent is stuck: ${outcome.reason}")
is Outcome.Fatal -> println("Agent encountered an error: ${outcome.message}")
}

// Access execution details
println("Execution time: ${run.finishedAt - run.startedAt}")
println("Token usage: ${run.tokenUsage()}")
Output
🚀 Agent started!
The agent's answer is: Kotlin is a statically typed language for the JVM, JavaScript and native platforms.
Execution time: 1.842016s
Token usage: TokenUsage(inputTokens=312, outputTokens=41, thinkingOutputTokens=0, cachedInputTokens=0)

The response property contains the type you specified when creating the agent, and AgentRun gives you access to timing information, token usage, and the complete conversation history.

start doesn't throw when something goes wrong during the run, like a failed model request or a tool that throws. You get an Outcome.Fatal with the error message instead, and the run is still sent to the platform when you configured one. Cancelling the coroutine still cancels the run.

The error is also logged. By default that prints Error: <message>. To send it to your own logging, pass a Logger from community.flock.aigentic.core.logging: agent.start("What is Kotlin?", logger = myLogger).

Sending runs to the platform​

Add platform { } to send every run to the Aigentic platform. There you can review runs, tag the good ones and replay them as regression tests. Flock hosts the platform and gives access on request.

import community.flock.aigentic.platform.dsl.platform

val agent = agent<String, Answer> {
// Agent configuration
platform {
name("weather-agent")
secret("YOUR_PLATFORM_SECRET")
}
}

This needs the platform module, see Installation.

More Examples​

For more examples and reference implementations, check out the Aigentic Initializr repository. This repository contains various agent configurations and use cases that demonstrate different aspects of the Aigentic DSL:

  • Basic agent configurations
  • Tool integrations
  • Custom system prompts
  • Complex response formats
  • Context handling examples
  • And more...

Feel free to explore the repository and use these examples as a starting point for your own agent implementations.