TramAI

Tool Calling

What it is: TramAI's tool calling lets models invoke typed application functions during an operation. Tools are declared explicitly via @Operation(tools = [...]), registered in the builder, and executed inside an engine-owned loop.

When to use it: When the model needs to query data, perform lookups, or trigger deterministic side effects during a single operation. Tool calling is for bounded, typed function execution — not autonomous agent loops.

How the Tool Execution Loop Works

  1. The model returns one or more tool call requests in its response
  2. TramAI validates and deserializes the tool arguments using the tool's input schema
  3. TramAI executes the matching registered tool with a ToolExecutionContext
  4. TramAI returns the tool result to the model
  5. The model continues toward a final answer (or makes further tool calls)

The engine owns the entire loop. Applications stay inside the normal service method contract — the tool orchestration is invisible to the caller.

Minimum Setup

Step 1: Define a Tool

Implement the TramaiTool<I, O> interface:

data class LookupInput(val query: String)
data class LookupResult(val value: String)

class LookupTool : TramaiTool<LookupInput, LookupResult> {
   override val name: String = "lookup"
   override val description: String = "Looks up tenant data by query"
   override val inputType = LookupInput::class
   override val idempotent: Boolean = true
   override val sideEffectLevel: SideEffectLevel = SideEffectLevel.READ_ONLY

   override suspend fun execute(
       input: LookupInput,
       context: ToolExecutionContext,
   ): LookupResult = LookupResult(
       value = tenantDirectory.lookup(input.query),
   )
}

Step 2: Declare the Operation

Reference tools by name in @Operation(tools = [...]):

@AiService
interface TenantAssistant {
   @Operation(
       prompt = "Use the lookup tool when you need tenant data",
       model = "claude-sonnet-4-20250514",
       tools = ["lookup"],
   )
   suspend fun answer(question: String): String
}

Tool names must match exactly with registered tools. An empty tools array means no tools are available — this is TramAI's security model (explicit-only).

Step 3: Register and Create

val tramai = Tramai {
   provider(
       AnthropicProvider(System.getenv("ANTHROPIC_API_KEY")),
       name = "anthropic",
       default = true,
   )
   model("claude-sonnet-4-20250514", "anthropic")
   tools(LookupTool())
}
val service = tramai.create<TenantAssistant>()
val answer = service.answer("What is the status of tenant ABC?")

Security Model: Explicit-Only Exposure

Tool exposure is always explicit:

  • @Operation(tools = [...]): The only way to make tools available to a specific operation. An empty array means no tools.
  • tools(...) in builder: The only way to register tool implementations at runtime.
  • No automatic discovery. No path-based injection. No hidden tool access.

This means the model can only call tools you explicitly authorize for each operation.

Security Metadata on Tools

Every TramaiTool can carry a security property of type ToolSecurityMetadata:

data class ToolSecurityMetadata(
   val permission: String,
   val risk: RiskLevel,
   val approval: ApprovalMode,
   val managedNetworkEgress: ManagedNetworkEgress,
   val audit: AuditDetail,
   val compatibilityMode: CompatibilityMode = CompatibilityMode.STRICT,
)

The policy engine evaluates this metadata at BEFORE_TOOL_EXECUTION. When no policy engine is configured, TramAI uses a legacy permissive mode (all tools allowed, migration warning logged).

class PaymentTool : TramaiTool<PaymentInput, PaymentResult> {
   override val name: String = "process_payment"
   override val description: String = "Process a customer refund"
   override val inputType = PaymentInput::class
   override val idempotent: Boolean = false
   override val sideEffectLevel: SideEffectLevel = SideEffectLevel.WRITE
   override val security: ToolSecurityMetadata = ToolSecurityMetadata(
       permission = "payment.process",
       risk = RiskLevel.HIGH,
       approval = ApprovalMode.REQUIRED,
       managedNetworkEgress = ManagedNetworkEgress.AUDIT,
       audit = AuditDetail.FULL,
   )
   // ...
}

Tool Interface Reference

interface TramaiTool<I : Any, O : Any> {
   val name: String                    // Unique tool name
   val description: String             // Description for model schema
   val inputType: KClass<I>            // Input type for schema generation
   val idempotent: Boolean             // Safe to retry on failure? (default: false)
   val sideEffectLevel: SideEffectLevel  // NONE, READ_ONLY, WRITE, UNKNOWN (default: UNKNOWN)
   val security: ToolSecurityMetadata?  // Policy metadata (default: null)

   suspend fun execute(input: I, context: ToolExecutionContext): O
}

ToolExecutionContext provides runtime metadata:

data class ToolExecutionContext(
   val operationName: String,
   val modelName: String,
   val attemptNumber: Int,
   val conversationId: String?,
   val idempotencyKey: String?,       // Set when resuming an approved tool
   val timeout: Duration,
   val attributes: Map<String, Any>,
)

Kotlin + Java Examples

Kotlin

data class WeatherInput(val city: String)
data class WeatherResult(val temperature: Double, val conditions: String)

class WeatherTool : TramaiTool<WeatherInput, WeatherResult> {
   override val name = "get_weather"
   override val description = "Get current weather for a city"
   override val inputType = WeatherInput::class
   override val idempotent = true
   override val sideEffectLevel = SideEffectLevel.READ_ONLY

   override suspend fun execute(input: WeatherInput, context: ToolExecutionContext): WeatherResult {
       val data = weatherApi.fetch(input.city)
       return WeatherResult(data.temp, data.conditions)
   }
}

@AiService
interface TravelAssistant {
   @Operation(
       prompt = "Help the user plan their trip",
       model = "gpt-4o",
       tools = ["get_weather"],
   )
   suspend fun planTrip(destination: String): String
}

val tramai = Tramai {
   provider(OpenAiProvider(System.getenv("OPENAI_API_KEY")), name = "openai", default = true)
   model("gpt-4o", "openai")
   tools(WeatherTool())
}
val assistant = tramai.create<TravelAssistant>()

Java

// Java records for input/output
public record WeatherInput(String city) {}
public record WeatherResult(double temperature, String conditions) {}

// Tool implementation
public class WeatherTool implements TramaiTool<WeatherInput, WeatherResult> {
   @Override
   public String getName() { return "get_weather"; }
   @Override
   public String getDescription() { return "Get current weather for a city"; }
   @Override
   public KClass<WeatherInput> getInputType() { return WeatherInput.class; }
   @Override
   public boolean isIdempotent() { return true; }
   @Override
   public SideEffectLevel getSideEffectLevel() { return SideEffectLevel.READ_ONLY; }

   @Override
   public WeatherResult execute(WeatherInput input, ToolExecutionContext context) {
       WeatherData data = weatherApi.fetch(input.city());
       return new WeatherResult(data.getTemp(), data.getConditions());
   }
}

// Service interface
@AiService
public interface TravelAssistant {
   @Operation(
       prompt = "Help the user plan their trip",
       model = "gpt-4o",
       tools = {"get_weather"}
   )
   String planTrip(String destination);
}

// Wiring
Tramai tramai = Tramai.builder()
   .provider(new OpenAiProvider(System.getenv("OPENAI_API_KEY")), "openai", true)
   .model("gpt-4o", "openai")
   .tools(new WeatherTool())
   .build();

TravelAssistant assistant = tramai.create(TravelAssistant.class);

Tool Registration Patterns

PatternHowWhen
StandaloneTramai { tools(MyTool()) }Framework-free apps
Spring Boot@AiTool on @Component methodsSpring applications
Multiple toolstools(ToolA(), ToolB()) or tools(listOf(...))Any number of tools
Duplicate detectionBuilder throws ConfigurationException on duplicate nameSafety check

Spring Boot Registration

In Spring, annotate methods with @AiTool instead of manually implementing the interface:

@Component
class TenantTools {
   @AiTool(
       name = "lookup",
       description = "Looks up tenant data",
       idempotent = true,
       sideEffectLevel = SideEffectLevel.READ_ONLY,
   )
   fun lookup(input: LookupInput): LookupResult = LookupResult(
       value = tenantDirectory.lookup(input.query),
   )
}

Spring rules: @AiTool methods must take exactly one parameter (a data class). The Spring adapter converts these into TramaiTool registrations automatically.

Tool Failure Semantics

TramAI distinguishes four tool outcomes:

OutcomeMeaningRetry?
ToolResult.SuccessTool executed normallyNo
ToolResult.InvalidInputArguments failed deserialization or validationYes (model reformulates)
ToolResult.TransientFailureTemporary error (timeout, network)Yes, if idempotent = true
ToolResult.PermanentFailureNon-recoverable errorNo

Limitations

  • Streaming + tool calling: Not part of the current public contract. Streaming is for raw text only.
  • Autonomous planning: Tool calling is a bounded loop, not an agent framework. No planner, no autonomous recursion.
  • No hidden tools: Tools are explicitly registered and explicitly attached to operations. No path-based or reflection-based discovery in standalone mode.
  • Caching disabled: Tool-enabled operations skip caching even with @Operation(cacheable = true) because tool results may depend on external state.

Next Steps

  • Structured Output — Combine tools with typed return values
  • Spring Boot — Auto-register tools in Spring applications
  • Testing — Test tool-calling services with mock providers
  • Observability — Monitor tool execution latency and failure rates