Fluent DSL and Kotlin DSL for building autonomous agents with planning capabilities on the JVM, featuring annotation-based and programmatic configuration for agentic flows with Spring Boot integration
Tools are LLM-invocable functions that extend agent capabilities. The Tool API provides schema definitions for function calling, result handling, and progressive tool disclosure through MatryoshkaTools.
Mark methods as LLM-callable tools using the @LlmTool annotation.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LlmTool(
/** Description of what the tool does */
val description: String = "",
/** Tool name (defaults to method name) */
val name: String = "",
/** Whether to return result directly without further processing */
val returnDirect: Boolean = false,
/** Category for tool organization */
val category: String = ""
) {
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Param(
/** Parameter description */
val description: String,
/** Whether parameter is required */
val required: Boolean = true
)
}Basic Tool Example:
@EmbabelComponent
class CalculatorTools {
@LlmTool(description = "Add two numbers together")
fun add(
@LlmTool.Param(description = "First number")
a: Double,
@LlmTool.Param(description = "Second number")
b: Double
): Double {
return a + b
}
@LlmTool(description = "Calculate percentage")
fun percentage(
@LlmTool.Param(description = "Value to calculate percentage of")
value: Double,
@LlmTool.Param(description = "Percentage (e.g., 20 for 20%)")
percent: Double
): Double {
return value * (percent / 100.0)
}
}Core tool interface for programmatic tool creation.
interface Tool : ToolInfo {
/** Call the tool with JSON input */
fun call(input: String): Result
sealed interface Result {
/** Text result */
data class Text(val content: String) : Result
/** Result with artifact */
data class WithArtifact(
val content: String,
val artifact: Any
) : Result
/** Error result */
data class Error(
val message: String,
val cause: Throwable?
) : Result
}
companion object {
/** Create text result */
@JvmStatic
@JvmOverloads
fun text(content: String): Result
/** Create result with artifact */
@JvmStatic
@JvmOverloads
fun withArtifact(content: String, artifact: Any): Result
/** Create error result */
@JvmStatic
@JvmOverloads
fun error(message: String, cause: Throwable? = null): Result
}
}Programmatic Tool Creation:
val weatherTool = Tool.create(
name = "get_weather",
description = "Get current weather for a location"
) { input ->
try {
val request = objectMapper.readValue(input, WeatherRequest::class.java)
val weather = weatherService.getWeather(request.location)
Tool.Result.text("Weather in ${request.location}: ${weather.description}, ${weather.temp}°F")
} catch (e: Exception) {
Tool.Result.error("Failed to get weather", e)
}
}
data class WeatherRequest(val location: String)Define tool schemas with parameters.
interface Tool.Definition {
/** Tool name */
val name: String
/** Tool description */
val description: String
/** Input schema */
val inputSchema: InputSchema
/** Add parameter to definition */
fun withParameter(parameter: Parameter): Definition
companion object {
/** Create definition builder */
@JvmStatic
fun builder(): DefinitionBuilder
}
}
interface Tool.InputSchema {
/** Parameters in schema */
val parameters: List<Parameter>
/** Convert to JSON schema */
fun toJsonSchema(): String
/** Add parameter */
fun withParameter(parameter: Parameter): InputSchema
companion object {
/** Create input schema builder */
@JvmStatic
fun builder(): InputSchemaBuilder
/** Create schema from Kotlin type */
@JvmStatic
inline fun <reified T> fromType(): InputSchema
}
}
data class Tool.Parameter(
val name: String,
val type: ParameterType,
val description: String,
val required: Boolean = true,
val enumValues: List<String>? = null,
val default: Any? = null,
val items: Parameter? = null, // For ARRAY type
val properties: Map<String, Parameter>? = null // For OBJECT type
) {
companion object {
/** String parameter */
@JvmStatic
@JvmOverloads
fun string(
name: String,
description: String,
required: Boolean = true
): Parameter
/** Integer parameter */
@JvmStatic
@JvmOverloads
fun integer(
name: String,
description: String,
required: Boolean = true
): Parameter
/** Double/Number parameter */
@JvmStatic
@JvmOverloads
fun double(
name: String,
description: String,
required: Boolean = true
): Parameter
/** Number parameter (alias for double) */
@JvmStatic
@JvmOverloads
fun number(
name: String,
description: String,
required: Boolean = true
): Parameter
/** Boolean parameter */
@JvmStatic
@JvmOverloads
fun boolean(
name: String,
description: String,
required: Boolean = true
): Parameter
/** Array parameter */
@JvmStatic
@JvmOverloads
fun array(
name: String,
description: String,
items: Parameter,
required: Boolean = true
): Parameter
/** Object parameter */
@JvmStatic
@JvmOverloads
fun obj(
name: String,
description: String,
properties: Map<String, Parameter>,
required: Boolean = true
): Parameter
/** Enum parameter (string with allowed values) */
@JvmStatic
@JvmOverloads
fun enum(
name: String,
description: String,
values: List<String>,
required: Boolean = true
): Parameter
}
}
enum class Tool.ParameterType {
STRING,
INTEGER,
NUMBER,
BOOLEAN,
ARRAY,
OBJECT
}Custom Tool Definition:
val databaseQueryTool = Tool.of(
name = "query_database",
description = "Execute SQL query on database",
inputSchema = Tool.InputSchema.builder()
.withParameter(Tool.Parameter.string(
"query",
"SQL query to execute",
required = true
))
.withParameter(Tool.Parameter.integer(
"limit",
"Maximum number of results",
required = false
))
.build(),
metadata = Tool.Metadata.builder()
.returnDirect(false)
.build()
) { input ->
val params = parseInput(input)
val results = database.execute(params.query, params.limit)
Tool.Result.withArtifact(
"Query returned ${results.size} rows",
results
)
}Complex Parameter Types:
// Array parameter
val batchProcessTool = Tool.of(
name = "batch_process",
description = "Process multiple items in batch",
inputSchema = Tool.InputSchema.builder()
.withParameter(Tool.Parameter.array(
name = "items",
description = "Items to process",
items = Tool.Parameter.string("item", "Item to process")
))
.build()
) { input ->
val items = parseItems(input)
val results = items.map { processor.process(it) }
Tool.Result.text("Processed ${results.size} items")
}
// Object parameter with nested properties
val createUserTool = Tool.of(
name = "create_user",
description = "Create new user account",
inputSchema = Tool.InputSchema.builder()
.withParameter(Tool.Parameter.obj(
name = "user",
description = "User information",
properties = mapOf(
"name" to Tool.Parameter.string("name", "User's full name"),
"email" to Tool.Parameter.string("email", "User's email"),
"age" to Tool.Parameter.integer("age", "User's age", required = false),
"admin" to Tool.Parameter.boolean("admin", "Is admin user", required = false)
)
))
.build()
) { input ->
val user = parseUser(input)
userService.create(user)
Tool.Result.text("Created user: ${user.name}")
}
// Enum parameter
val setLogLevelTool = Tool.of(
name = "set_log_level",
description = "Set application log level",
inputSchema = Tool.InputSchema.builder()
.withParameter(Tool.Parameter.enum(
name = "level",
description = "Log level to set",
values = listOf("DEBUG", "INFO", "WARN", "ERROR")
))
.build()
) { input ->
val level = parseLogLevel(input)
logger.setLevel(level)
Tool.Result.text("Log level set to $level")
}Create tools from existing objects and methods.
companion object Tool {
/** Create tool from method */
fun fromMethod(
instance: Any,
method: Method,
objectMapper: ObjectMapper
): Tool
/** Create tools from all @LlmTool methods in instance */
fun fromInstance(
instance: Any,
objectMapper: ObjectMapper
): List<Tool>
/** Safely create tools (catches errors) */
fun safelyFromInstance(
instance: Any,
objectMapper: ObjectMapper
): List<Tool>
}Tool Factory Example:
@EmbabelComponent
class FileOperations {
@LlmTool(description = "Read file contents")
fun readFile(
@LlmTool.Param(description = "Path to file")
path: String
): String {
return File(path).readText()
}
@LlmTool(description = "List files in directory")
fun listFiles(
@LlmTool.Param(description = "Directory path")
directory: String
): List<String> {
return File(directory).list()?.toList() ?: emptyList()
}
}
// Create all tools from instance
val fileOps = FileOperations()
val tools = Tool.fromInstance(fileOps, objectMapper)Progressive tool disclosure pattern - reveal inner tools after outer tool is invoked.
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class MatryoshkaTools(
/** Name of the container tool */
val name: String,
/** Description of the tool group */
val description: String,
/** Whether to remove container tool after invocation */
val removeOnInvoke: Boolean = true,
/** Parameter name for category selection */
val categoryParameter: String = "category"
)
interface MatryoshkaTool : Tool {
/** Inner tools to be revealed */
val innerTools: List<Tool>
companion object {
/** Create from annotated instance */
fun fromInstance(
instance: Any,
objectMapper: ObjectMapper
): MatryoshkaTool
/** Create from annotated class */
fun fromAnnotatedClass(
clazz: Class<*>,
objectMapper: ObjectMapper
): MatryoshkaTool
}
}MatryoshkaTool Example:
@MatryoshkaTools(
name = "file_operations",
description = "File system operations organized by category"
)
class FileSystemTools {
@LlmTool(
description = "Read file contents",
category = "read"
)
fun readFile(
@LlmTool.Param(description = "File path")
path: String
): String {
return File(path).readText()
}
@LlmTool(
description = "Write content to file",
category = "write"
)
fun writeFile(
@LlmTool.Param(description = "File path")
path: String,
@LlmTool.Param(description = "Content to write")
content: String
) {
File(path).writeText(content)
}
@LlmTool(
description = "Search for files",
category = "search"
)
fun searchFiles(
@LlmTool.Param(description = "Directory to search")
directory: String,
@LlmTool.Param(description = "Pattern to match")
pattern: String
): List<String> {
return File(directory).walkTopDown()
.filter { it.name.contains(pattern) }
.map { it.absolutePath }
.toList()
}
}
// Usage
val matryoshkaTool = MatryoshkaTool.fromInstance(FileSystemTools(), objectMapper)Tools that trigger replanning based on results. Replanning allows agents to dynamically adjust their plans based on tool execution outcomes.
/** Tool that triggers replanning after execution */
class ReplanningTool(
override val delegate: Tool
) : DelegatingTool() {
override fun call(input: String): Result {
val result = delegate.call(input)
// Triggers replanning after returning result
return result
}
}
/** Tool that conditionally triggers replanning */
class ConditionalReplanningTool<T>(
override val delegate: Tool,
val decider: ReplanDecider<T>
) : DelegatingTool() {
override fun call(input: String): Result {
val result = delegate.call(input)
// Conditionally triggers replanning based on decider
return result
}
}
/** Functional interface for replan decisions */
fun interface ReplanDecider<T> {
/** Decide whether to replan based on result */
fun shouldReplan(context: ReplanContext<T>): ReplanDecision
}
/** Context for replanning decisions */
data class ReplanContext<T>(
/** Tool result */
val result: Tool.Result,
/** Parsed artifact of type T */
val artifact: T?,
/** Original tool input */
val input: String,
/** Tool that was executed */
val tool: Tool
) {
/** Helper to check if result was successful */
fun isSuccess(): Boolean = result !is Tool.Result.Error
/** Helper to check if result has artifact */
fun hasArtifact(): Boolean = result is Tool.Result.WithArtifact
/** Get content from result */
fun getContent(): String = when (result) {
is Tool.Result.Text -> result.content
is Tool.Result.WithArtifact -> result.content
is Tool.Result.Error -> result.message
}
}
/** Decision result for replanning */
data class ReplanDecision(
/** Whether to trigger replanning */
val shouldReplan: Boolean,
/** Optional reason for replanning */
val reason: String? = null,
/** Optional blackboard updates */
val blackboardUpdates: Map<String, Any> = emptyMap()
) {
companion object {
/** No replanning needed */
fun noReplan(): ReplanDecision = ReplanDecision(false)
/** Trigger replanning */
fun replan(reason: String? = null): ReplanDecision =
ReplanDecision(true, reason)
/** Replan with blackboard updates */
fun replanWith(
reason: String? = null,
updates: Map<String, Any>
): ReplanDecision =
ReplanDecision(true, reason, updates)
}
}
/** Functional interface for blackboard updates during replanning */
fun interface ReplanningToolBlackboardUpdater<T> {
/** Compute blackboard updates from result */
fun computeUpdates(result: T): Map<String, Any>
}
companion object Tool {
/** Tool that always triggers replanning */
fun replanAlways(tool: Tool): ReplanningTool
/** Tool that replans based on condition */
fun <T> conditionalReplan(
tool: Tool,
decider: ReplanDecider<T>
): ConditionalReplanningTool<T>
/** Tool that replans when predicate is true */
fun <T> replanWhen(
tool: Tool,
predicate: (T) -> Boolean
): ConditionalReplanningTool<T>
/** Tool that replans and adds value to blackboard */
fun <T> replanAndAdd(
tool: Tool,
blackboardUpdater: ReplanningToolBlackboardUpdater<T>
): ConditionalReplanningTool<T>
}Basic Replanning Example:
val dataFetchTool = Tool.create(
name = "fetch_data",
description = "Fetch data from API"
) { input ->
val data = apiClient.fetch(input)
Tool.Result.withArtifact("Fetched ${data.size} records", data)
}
// Replan if fetch returns more than 1000 records
val replanningFetchTool = Tool.replanWhen(dataFetchTool) { result ->
val artifact = (result as? Tool.Result.WithArtifact)?.artifact
(artifact as? List<*>)?.size ?: 0 > 1000
}Advanced Replanning with Context:
data class SearchResult(val results: List<String>, val hasMore: Boolean)
val searchTool = Tool.create(
name = "search",
description = "Search database"
) { input ->
val results = database.search(input)
Tool.Result.withArtifact(
"Found ${results.size} results",
SearchResult(results, results.size >= 100)
)
}
// Conditional replanning with full context
val smartSearchTool = Tool.conditionalReplan(searchTool) { context: ReplanContext<SearchResult> ->
when {
// Replan if search returned too many results
context.artifact?.hasMore == true ->
ReplanDecision.replan("Too many results, refine search")
// Replan if no results found
context.artifact?.results?.isEmpty() == true ->
ReplanDecision.replanWith(
reason = "No results found, try broader search",
updates = mapOf("lastSearchFailed" to true)
)
// Continue without replanning
else -> ReplanDecision.noReplan()
}
}Replanning with Blackboard Updates:
data class ApiResponse(val data: List<Item>, val nextPageToken: String?)
val paginatedFetchTool = Tool.create(
name = "fetch_page",
description = "Fetch paginated data"
) { input ->
val response = apiClient.fetchPage(input)
Tool.Result.withArtifact(
"Fetched page with ${response.data.size} items",
response
)
}
// Replan and update blackboard with pagination info
val autoPageTool = Tool.replanAndAdd(paginatedFetchTool) { response: ApiResponse ->
if (response.nextPageToken != null) {
mapOf(
"nextPageToken" to response.nextPageToken,
"hasMorePages" to true,
"itemsFetched" to response.data.size
)
} else {
mapOf("hasMorePages" to false)
}
}Always Replan Pattern:
// Tool that always triggers replanning (for iterative refinement)
val iterativeAnalysisTool = Tool.replanAlways(
Tool.create(
name = "analyze_data",
description = "Analyze data and determine next steps"
) { input ->
val analysis = analyzer.analyze(input)
Tool.Result.text("Analysis complete: ${analysis.summary}")
}
)
// Use case: Agent analyzes data, then decides next action based on resultsOrganize related tools into groups.
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ToolGroup(
/** Role or purpose of the tool group */
val role: String
)
interface ToolGroupConsumer {
fun withToolGroup(group: ToolGroup): Unit
}Tool Group Example:
@ToolGroup(role = "customer-service")
@EmbabelComponent
class CustomerServiceTools {
@LlmTool(description = "Look up customer by ID")
fun getCustomer(
@LlmTool.Param(description = "Customer ID")
customerId: String
): Customer {
return customerRepository.findById(customerId)
}
@LlmTool(description = "Get customer order history")
fun getOrders(
@LlmTool.Param(description = "Customer ID")
customerId: String
): List<Order> {
return orderRepository.findByCustomerId(customerId)
}
}Generate tool input schemas from Kotlin types using reflection.
object TypeBasedInputSchema {
/** Generate schema from Kotlin type */
inline fun <reified T> generate(): Tool.InputSchema
/** Generate schema from class */
fun fromClass(clazz: Class<*>): Tool.InputSchema
/** Generate parameter from type */
inline fun <reified T> generateParameter(
name: String,
description: String,
required: Boolean = true
): Tool.Parameter
}Type-Based Schema Example:
data class SearchRequest(
val query: String,
val maxResults: Int = 10,
val includeMetadata: Boolean = false,
val filters: List<String> = emptyList()
)
// Generate schema from data class
val searchTool = Tool.of(
name = "search",
description = "Search with structured parameters",
inputSchema = Tool.InputSchema.fromType<SearchRequest>()
) { input ->
val request = objectMapper.readValue(input, SearchRequest::class.java)
val results = searchEngine.search(request)
Tool.Result.text("Found ${results.size} results")
}
// Or use inline
val analyticsTool = Tool.of(
name = "analytics",
description = "Run analytics query",
inputSchema = TypeBasedInputSchema.generate<AnalyticsQuery>()
) { input ->
val query = objectMapper.readValue(input, AnalyticsQuery::class.java)
val report = analytics.run(query)
Tool.Result.withArtifact("Analysis complete", report)
}Complex Type Schema:
data class Address(
val street: String,
val city: String,
val zipCode: String
)
data class UserProfile(
val name: String,
val email: String,
val age: Int?,
val address: Address,
val tags: List<String>,
val metadata: Map<String, String>
)
// Automatically generates nested object schema
val createProfileTool = Tool.of(
name = "create_profile",
description = "Create user profile with nested data",
inputSchema = TypeBasedInputSchema.generate<UserProfile>()
) { input ->
val profile = objectMapper.readValue(input, UserProfile::class.java)
profileService.create(profile)
Tool.Result.text("Profile created for ${profile.name}")
}
// Generated schema includes:
// - name: string (required)
// - email: string (required)
// - age: integer (optional, nullable)
// - address: object (required) with properties:
// - street: string (required)
// - city: string (required)
// - zipCode: string (required)
// - tags: array of strings (required)
// - metadata: object with string values (required)Additional metadata for tool configuration.
interface Tool.Metadata {
/** Whether to return result directly without further LLM processing */
val returnDirect: Boolean
/** Provider-specific metadata */
val providerMetadata: Map<String, Any>
/** Category for tool organization */
val category: String?
/** Priority hint for tool selection */
val priority: Int?
companion object {
/** Create metadata builder */
@JvmStatic
fun builder(): MetadataBuilder
/** Default metadata */
@JvmStatic
fun default(): Metadata = builder().build()
}
}Tool with Metadata:
val terminatorTool = Tool.create(
name = "end_conversation",
description = "End the conversation with final response",
metadata = Tool.Metadata.builder()
.returnDirect(true) // Return immediately, don't continue
.category("control")
.priority(100)
.providerMetadata(mapOf(
"priority" to "high",
"terminal" to true
))
.build()
) { input ->
Tool.Result.text("Conversation ended: $input")
}
// Tool with category for organization
val debugTool = Tool.create(
name = "debug_state",
description = "Debug agent state",
metadata = Tool.Metadata.builder()
.category("debugging")
.priority(0) // Low priority
.build()
) { input ->
val state = agentStateManager.getCurrentState()
Tool.Result.text("Current state: $state")
}Using Metadata for Tool Selection:
// Filter tools by category
fun getToolsByCategory(tools: List<Tool>, category: String): List<Tool> =
tools.filter { it.metadata.category == category }
// Sort tools by priority
fun sortToolsByPriority(tools: List<Tool>): List<Tool> =
tools.sortedByDescending { it.metadata.priority ?: 0 }
// Example usage
val allTools = listOf(searchTool, debugTool, terminatorTool)
val controlTools = getToolsByCategory(allTools, "control")
val prioritizedTools = sortToolsByPriority(allTools)Wrap instances for tool extraction.
interface ToolObject {
fun tools(): List<Tool>
companion object {
fun from(instance: Any): ToolObject
}
}Tool Object Example:
@EmbabelComponent
class MathOperations {
@LlmTool(description = "Calculate square root")
fun sqrt(
@LlmTool.Param(description = "Number")
n: Double
): Double = Math.sqrt(n)
}
@Action(description = "Answer math question")
fun answerMath(question: String, context: ActionContext): String {
val mathTools = ToolObject.from(MathOperations())
return context.promptRunner()
.withToolObject(mathTools)
.generateText(question)
}AgenticTool enables tools that use an LLM to orchestrate other tools, creating autonomous sub-agents within your agent system.
/** Tool that uses an LLM to orchestrate other tools */
class AgenticTool(
/** Tool definition */
override val definition: Tool.Definition,
/** Tool metadata */
override val metadata: Tool.Metadata,
/** LLM for orchestration */
val llm: LlmProvider,
/** Tools available to the agent */
val tools: List<Tool>,
/** System prompt creator */
val systemPromptCreator: SystemPromptCreator
) : Tool {
/** Create new AgenticTool with different LLM */
fun withLlm(llm: LlmProvider): AgenticTool
/** Create new AgenticTool with additional tools */
fun withTools(vararg tools: Tool): AgenticTool
/** Create new AgenticTool with different tools */
fun withTools(tools: List<Tool>): AgenticTool
/** Create new AgenticTool with custom system prompt */
fun withSystemPrompt(systemPrompt: String): AgenticTool
/** Create new AgenticTool with custom system prompt creator */
fun withSystemPromptCreator(creator: SystemPromptCreator): AgenticTool
/** Add tool object (extracts @LlmTool methods) */
fun withToolObject(toolObject: Any): AgenticTool
/** Add multiple tool objects */
fun withToolObjects(vararg toolObjects: Any): AgenticTool
override fun call(input: String): Result {
// LLM orchestrates tools to complete the task
val agentProcess = createAgentProcess(input)
val result = llm.executeWithTools(agentProcess, tools)
return result
}
companion object {
/** Create default system prompt for agentic tool */
@JvmStatic
fun defaultSystemPrompt(description: String): String =
"""
You are an autonomous agent tool: $description
Use the available tools to complete the task.
Break down complex requests into steps.
Use tools iteratively until the task is complete.
""".trimIndent()
}
}
/** Type alias for system prompt creator */
typealias SystemPromptCreator = (AgentProcess) -> StringBasic AgenticTool Example:
// Create tools for the agent to use
val webSearchTool = Tool.create(
name = "web_search",
description = "Search the web"
) { query ->
val results = searchEngine.search(query)
Tool.Result.text("Found ${results.size} results: ${results.take(3)}")
}
val extractTool = Tool.create(
name = "extract_info",
description = "Extract specific information from text"
) { input ->
val extracted = nlpService.extract(input)
Tool.Result.text("Extracted: $extracted")
}
// Create agentic tool that orchestrates search and extraction
val researchAgent = AgenticTool(
definition = Tool.Definition.builder()
.name("research_agent")
.description("Research a topic by searching and extracting key information")
.withParameter(Tool.Parameter.string(
"topic",
"Topic to research"
))
.build(),
metadata = Tool.Metadata.builder().build(),
llm = llmProvider,
tools = listOf(webSearchTool, extractTool),
systemPromptCreator = { process ->
"""
You are a research assistant. Given a topic:
1. Search the web for relevant information
2. Extract key facts and insights
3. Summarize your findings
Be thorough and cite your sources.
""".trimIndent()
}
)Advanced AgenticTool with Dynamic Tools:
@EmbabelComponent
class DataAnalysisTools {
@LlmTool(description = "Load dataset from file")
fun loadDataset(
@LlmTool.Param(description = "Path to dataset")
path: String
): Dataset = dataLoader.load(path)
@LlmTool(description = "Calculate statistics")
fun calculateStats(
@LlmTool.Param(description = "Dataset to analyze")
dataset: Dataset
): Statistics = statisticsEngine.compute(dataset)
@LlmTool(description = "Generate visualization")
fun visualize(
@LlmTool.Param(description = "Data to visualize")
data: Statistics
): Chart = chartGenerator.create(data)
}
// Create agentic tool with tool objects
val dataAnalystAgent = AgenticTool(
definition = Tool.Definition.builder()
.name("data_analyst")
.description("Analyze datasets and create visualizations")
.withParameter(Tool.Parameter.string(
"task",
"Analysis task to perform"
))
.build(),
metadata = Tool.Metadata.builder().build(),
llm = llmProvider,
tools = emptyList(),
systemPromptCreator = AgenticTool::defaultSystemPrompt
)
.withToolObject(DataAnalysisTools())
.withSystemPrompt(
"""
You are a data analyst. Perform thorough analysis:
1. Load the relevant dataset
2. Calculate appropriate statistics
3. Create meaningful visualizations
4. Provide insights and recommendations
""".trimIndent()
)Nested AgenticTools (Agent Hierarchy):
// Low-level agents for specific tasks
val fileAgent = AgenticTool(
definition = Tool.Definition.builder()
.name("file_agent")
.description("Manage file operations")
.build(),
llm = llmProvider,
tools = listOf(readFileTool, writeFileTool, listFilesTool),
systemPromptCreator = AgenticTool::defaultSystemPrompt
)
val networkAgent = AgenticTool(
definition = Tool.Definition.builder()
.name("network_agent")
.description("Handle network operations")
.build(),
llm = llmProvider,
tools = listOf(httpGetTool, httpPostTool, downloadTool),
systemPromptCreator = AgenticTool::defaultSystemPrompt
)
// High-level agent that delegates to specialized agents
val orchestratorAgent = AgenticTool(
definition = Tool.Definition.builder()
.name("orchestrator")
.description("Coordinate complex multi-step operations")
.build(),
llm = llmProvider,
tools = listOf(fileAgent, networkAgent, databaseTool),
systemPromptCreator = { process ->
"""
You are a coordinator agent. You have access to specialized agents:
- file_agent: For file system operations
- network_agent: For network/web operations
- database tools: For data storage
Delegate tasks to the appropriate specialized agent.
Coordinate their outputs to complete complex workflows.
""".trimIndent()
}
)AgenticTool with Context-Aware Prompts:
class CustomSystemPromptCreator : SystemPromptCreator {
override fun invoke(process: AgentProcess): String {
val userContext = process.blackboard.get("userContext") as? UserContext
val expertiseLevel = userContext?.expertiseLevel ?: "beginner"
return """
You are a coding assistant for a $expertiseLevel developer.
Adjust your explanations and code suggestions based on their level:
- Beginner: Provide detailed explanations and simpler solutions
- Intermediate: Balance explanation with efficiency
- Expert: Focus on optimal, idiomatic solutions
Use the available tools to help with coding tasks.
""".trimIndent()
}
}
val adaptiveCodeAgent = AgenticTool(
definition = Tool.Definition.builder()
.name("code_assistant")
.description("Adaptive coding assistant")
.build(),
llm = llmProvider,
tools = listOf(searchCodeTool, analyzeCodeTool, suggestRefactoringTool),
systemPromptCreator = CustomSystemPromptCreator()
)Java and Kotlin-friendly functional interfaces.
/** Kotlin functional interface */
fun interface Tool.Function {
fun invoke(input: String): Result
}
/** Java functional interface */
@FunctionalInterface
interface Tool.Handler {
fun handle(input: String): Result
}Functional Tool Example:
// Kotlin lambda
val tool1 = Tool.of("timer", "Get current time", Tool.Function { input ->
Tool.Result.text("Current time: ${LocalTime.now()}")
})
// Java-style
val tool2 = Tool.create("counter", "Count items", Tool.Handler { input ->
val items = parseItems(input)
Tool.Result.text("Count: ${items.size}")
})Parent interface providing tool definition and metadata.
/** Base interface for tool information */
interface ToolInfo {
/** Tool definition including name, description, and schema */
val definition: Tool.Definition
/** Tool metadata for configuration */
val metadata: Tool.Metadata
}All tool implementations extend ToolInfo, providing access to their definition and metadata without executing them.
Using ToolInfo:
fun analyzeTools(tools: List<ToolInfo>) {
tools.forEach { tool ->
println("Tool: ${tool.definition.name}")
println("Description: ${tool.definition.description}")
println("Parameters: ${tool.definition.inputSchema.parameters.size}")
println("Return Direct: ${tool.metadata.returnDirect}")
println("Category: ${tool.metadata.category}")
println()
}
}
// Check if tool has specific parameter
fun hasParameter(tool: ToolInfo, paramName: String): Boolean =
tool.definition.inputSchema.parameters.any { it.name == paramName }
// Get tools by return direct flag
fun getDirectReturnTools(tools: List<ToolInfo>): List<ToolInfo> =
tools.filter { it.metadata.returnDirect }Base class for tools that wrap or delegate to other tools.
/** Tool that delegates to another tool */
abstract class DelegatingTool : Tool {
/** The wrapped/delegated tool */
abstract val delegate: Tool
override val definition: Tool.Definition
get() = delegate.definition
override val metadata: Tool.Metadata
get() = delegate.metadata
override fun call(input: String): Tool.Result {
return delegate.call(input)
}
}DelegatingTool provides a base for creating tool wrappers that add behavior around existing tools (logging, validation, transformation, replanning, etc.).
Custom DelegatingTool Example:
/** Tool that logs all calls */
class LoggingTool(
override val delegate: Tool,
val logger: Logger
) : DelegatingTool() {
override fun call(input: String): Tool.Result {
logger.info("Calling tool: ${definition.name}")
logger.debug("Input: $input")
val startTime = System.currentTimeMillis()
val result = delegate.call(input)
val duration = System.currentTimeMillis() - startTime
logger.info("Tool ${definition.name} completed in ${duration}ms")
return when (result) {
is Tool.Result.Error -> {
logger.error("Tool failed: ${result.message}", result.cause)
result
}
else -> {
logger.debug("Result: $result")
result
}
}
}
}
// Usage
val searchTool = Tool.create("search", "Search database") { /* ... */ }
val loggedSearchTool = LoggingTool(searchTool, LoggerFactory.getLogger("tools"))Validation DelegatingTool:
/** Tool that validates input before execution */
class ValidatingTool(
override val delegate: Tool,
val validator: (String) -> ValidationResult
) : DelegatingTool() {
override fun call(input: String): Tool.Result {
val validation = validator(input)
if (!validation.isValid) {
return Tool.Result.error(
"Invalid input: ${validation.errors.joinToString(", ")}"
)
}
return delegate.call(input)
}
}
data class ValidationResult(
val isValid: Boolean,
val errors: List<String> = emptyList()
)
// Usage
val createUserTool = Tool.create("create_user", "Create user") { /* ... */ }
val validatedTool = ValidatingTool(createUserTool) { input ->
val user = parseUser(input)
val errors = mutableListOf<String>()
if (user.email.isBlank()) errors.add("Email is required")
if (user.name.length < 2) errors.add("Name too short")
ValidationResult(errors.isEmpty(), errors)
}Caching DelegatingTool:
/** Tool that caches results */
class CachingTool(
override val delegate: Tool,
val cache: MutableMap<String, Tool.Result> = ConcurrentHashMap(),
val ttlMs: Long = 300000 // 5 minutes
) : DelegatingTool() {
private data class CacheEntry(
val result: Tool.Result,
val timestamp: Long
)
private val entryCache = ConcurrentHashMap<String, CacheEntry>()
override fun call(input: String): Tool.Result {
val now = System.currentTimeMillis()
val cached = entryCache[input]
if (cached != null && (now - cached.timestamp) < ttlMs) {
return cached.result
}
val result = delegate.call(input)
entryCache[input] = CacheEntry(result, now)
return result
}
}
// Usage
val expensiveTool = Tool.create("analyze", "Expensive analysis") { /* ... */ }
val cachedTool = CachingTool(expensiveTool, ttlMs = 600000) // 10 min cacheRetry DelegatingTool:
/** Tool that retries on failure */
class RetryTool(
override val delegate: Tool,
val maxRetries: Int = 3,
val delayMs: Long = 1000
) : DelegatingTool() {
override fun call(input: String): Tool.Result {
var lastError: Tool.Result.Error? = null
repeat(maxRetries) { attempt ->
val result = delegate.call(input)
if (result !is Tool.Result.Error) {
return result
}
lastError = result
if (attempt < maxRetries - 1) {
Thread.sleep(delayMs * (attempt + 1)) // Exponential backoff
}
}
return Tool.Result.error(
"Failed after $maxRetries attempts: ${lastError?.message}",
lastError?.cause
)
}
}
// Usage
val unreliableTool = Tool.create("fetch_api", "Fetch from API") { /* ... */ }
val reliableTool = RetryTool(unreliableTool, maxRetries = 5, delayMs = 500)interface ToolInfo {
val definition: Tool.Definition
val metadata: Tool.Metadata
}
abstract class DelegatingTool : Tool {
abstract val delegate: Tool
}
class ReplanningTool(
override val delegate: Tool
) : DelegatingTool()
class ConditionalReplanningTool<T>(
override val delegate: Tool,
val decider: ReplanDecider<T>
) : DelegatingTool()
interface ToolGroupRequirement {
val group: String
val required: Boolean
}
data class ReplanContext<T>(
val result: Tool.Result,
val artifact: T?,
val input: String,
val tool: Tool
)
data class ReplanDecision(
val shouldReplan: Boolean,
val reason: String? = null,
val blackboardUpdates: Map<String, Any> = emptyMap()
)
fun interface ReplanDecider<T> {
fun shouldReplan(context: ReplanContext<T>): ReplanDecision
}
fun interface ReplanningToolBlackboardUpdater<T> {
fun computeUpdates(result: T): Map<String, Any>
}
typealias SystemPromptCreator = (AgentProcess) -> StringInstall with Tessl CLI
npx tessl i tessl/maven-com-embabel-agent--embabel-agent-apidocs