CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-api

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

Overview
Eval results
Files

tools.mddocs/

Tools

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.

Capabilities

Tool Annotation

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)
    }
}

Tool Interface

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)

Tool Definition

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")
}

Tool Factory Methods

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)

MatryoshkaTool

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)

Replanning Tools

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 results

Tool Groups

Organize 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)
    }
}

TypeBasedInputSchema

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)

Tool Metadata

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)

Tool Object Wrapper

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

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) -> String

Basic 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()
)

Functional Tool Interfaces

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}")
})

ToolInfo Interface

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 }

DelegatingTool Interface

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 cache

Retry 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)

Types

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) -> String

Install with Tessl CLI

npx tessl i tessl/maven-com-embabel-agent--embabel-agent-api

docs

actions-goals.md

agent-definition.md

builtin-tools.md

chat.md

conditions.md

events.md

human-in-the-loop.md

index.md

invocation.md

io-binding.md

llm-interaction.md

models.md

planning-workflows.md

platform-management.md

runtime-context.md

state-management.md

streaming.md

subagents.md

tools.md

type-system.md

typed-operations.md

validation.md

tile.json