TramAI - governed AI workflows for Java and Kotlin

Annotation Reference

TramAI's annotation surface lives in dev.tramai.core.annotations and is small on purpose. All annotations are runtime-retained: the engine reads them through reflection when it builds the service proxy, so there is no code generation step.

The annotation module is part of tramai-core, which is the stable tier of the 0.6.0 release line. See API Stability for the per-module tiers.

@AiService

Marks an interface as a TramAI service contract.

@AiService
interface Summarizer {
    @Operation(prompt = "Summarize the incident.", model = "gpt-4o")
    suspend fun summarize(incident: String): String
}
  • Target: AnnotationTarget.CLASS.
  • Apply it to interfaces, not to implementation classes. TramAI creates the implementation.
  • In Spring, every @AiService interface on the scanned classpath becomes a bean during auto-configuration. In standalone code you create it yourself with tramai.create<Summarizer>().

@Operation

Declares one AI-backed method on an @AiService interface.

AttributeTypeDefaultMeaning
promptString""Prompt template sent as the base user instruction
modelString(required)Logical model name requested by the operation
providerString""Explicit provider id, bypassing registry model resolution
toolsArray<String>[]Tool names available to this operation
maxRetriesInt2Structured-output retries after validation failures
providerRetriesInt3Provider retries after transient transport or API failures
timeoutMillisLong30_000Maximum duration of a single provider attempt
cacheableBooleanfalseAllow the engine to cache successful non-streaming responses
cacheTtlMillisLong60_000Cache TTL when cacheable is enabled
@Operation(
    prompt = "Summarize the incident",
    model = "gpt-4o",
    provider = "openai",
    tools = ["vendor_lookup"],
    maxRetries = 3,
    providerRetries = 2,
    timeoutMillis = 15_000,
    cacheable = true,
    cacheTtlMillis = 60_000,
)
suspend fun summarize(incident: String): IncidentSummary

maxRetries and providerRetries are different budgets. maxRetries counts re-asks after a structured-output validation failure; providerRetries counts re-attempts of the same provider route after retryable transport or API failures. For streaming operations, providerRetries applies only while no token has been emitted to the caller — once output is visible, TramAI does not retry that provider or fall back mid-stream.

@SystemPrompt

Declares a service-wide system prompt applied to every operation on the interface.

@AiService
@SystemPrompt("You are a precise billing assistant.")
interface BillingAnalyzer {
    @Operation(prompt = "Return the invoice status.", model = "gpt-4o")
    suspend fun analyze(invoice: String): String
}
  • Target: AnnotationTarget.CLASS.
  • Use it when every method on the service should share the same role framing. If methods need genuinely different system behavior, split the interface instead.

@System

Defines the system-level instruction for a single operation.

@Operation(prompt = "Summarize account {accountId}.", model = "gpt-4o")
@System("You are reviewing account {accountId}. Never speculate beyond the provided data.")
suspend fun summarize(accountId: String, notes: String): String
  • Target: AnnotationTarget.FUNCTION; value carries the template.
  • The value is sent as the system role message. Multiple @System annotations on the same function are concatenated with newlines in declaration order.
  • {param} markers interpolate method parameter values.
  • If @System is absent, the engine uses the class-level @SystemPrompt. If both are present, @System wins and a warning is logged. If neither exists, a default system message is derived from the interface and method signature.

@User

Defines a user-role message for an operation.

@Operation(model = "gpt-4o")
@User("Analyze invoice {invoiceId} for duplicates.")
@User("Report confidence for every finding.")
suspend fun analyze(invoiceId: String): DuplicateAnalysis
  • Target: AnnotationTarget.FUNCTION; value carries the template with {param} interpolation.
  • Multiple @User annotations become separate user messages, in order.
  • If neither @User nor @Operation(prompt = ...) is present, the engine builds a default user message from the method signature.

@ConversationId

Marks the method parameter that identifies the conversation for multi-turn memory.

@Operation(prompt = "Continue the account review.", model = "gpt-4o")
suspend fun review(@ConversationId sessionId: String, message: String): String
  • Target: AnnotationTarget.VALUE_PARAMETER.
  • Only meaningful when you configure chat memory (the standalone builder's memory(...) or the Spring memory configuration). The annotated argument's toString() is used as the conversation key.
  • A null annotated argument fails fast. If no parameter is annotated, the engine falls back to its ConversationIdProvider (a UUID provider by default), which means each call is its own conversation.

@AiTool

Marks a method for discovery as a portable tool by framework adapters.

@Component
class VendorTools {
    @AiTool(
        name = "vendor_lookup",
        description = "Looks up vendor reliability and payment terms.",
        idempotent = true,
        permission = "vendor.read",
        sideEffectLevel = SideEffectLevel.READ_ONLY,   // NONE | READ_ONLY | WRITE | UNKNOWN
    )
    fun lookupVendor(input: VendorLookupInput): VendorDetails = TODO()
}
  • Target: AnnotationTarget.FUNCTION.
  • name — explicit tool name; empty means the adapter may fall back to the method name.
  • description — injected into model tool definitions. Describes what the tool does, not how.
  • idempotent — whether repeating execution with the same logical input is safe. This describes repetition semantics only; it is not an instruction to retry on failure.
  • sideEffectLevel — SideEffectLevel: NONE, READ_ONLY, WRITE, UNKNOWN. Default UNKNOWN. Use it to describe whether the tool reads data, writes data, or has unknown external effects.
  • permission — the permission required to expose and execute the tool. Default is empty, which preserves the legacy contract: no ToolSecurityMetadata is produced, and the standard runtime exposes the tool as before. Secure profiles such as sovereign TramAI reject tools whose permission is empty.
  • risk — RiskLevel: LOW, MEDIUM, HIGH, CRITICAL. Default HIGH. Used by secure profiles when permission is set.
  • approval — ApprovalMode: AUTO, HUMAN_REQUIRED, HUMAN_REQUIRED_WITH_TIMEOUT. Default HUMAN_REQUIRED. Used by secure profiles when permission is set.
  • managedNetworkEgress — ManagedNetworkEgress: ALLOW, DENY, ALLOWLIST_ONLY. Default DENY. Covers TramAI-managed egress only; it does not sandbox arbitrary networking inside the method.
  • audit — AuditDetail: MINIMAL, DECISION_ONLY, FULL. Default FULL.

The governance defaults are deliberately conservative. Opting into governance never silently weakens the declared posture — if you set permission and say nothing else, you get high risk, human approval, denied managed egress, and full audit detail.

Structured Output Property Annotations

These three apply to non-String return types and shape schema generation and validation. They target PROPERTY, FIELD, and VALUE_PARAMETER.

@AiDescription

Adds a human-readable description to a property exposed in the generated schema.

data class SpendAnalysis(
    @property:AiDescription("Total spend in USD")
    val totalSpend: Double,
)

@AiRange

Constrains a numeric property to a closed range. Both bounds are inclusive and required.

data class Confidence(
    @property:AiRange(min = 0.0, max = 1.0)
    val confidence: Double,
)

@AiMinItems

Declares the minimum allowed element count for a collection property.

data class RecommendationSet(
    @property:AiMinItems(1)
    val actions: List<String>,
)

Use it when an empty collection would be semantically invalid.

Spring Wiring Annotation

@EnableTramai

Optional. In Spring Boot applications auto-configuration is normally sufficient; @EnableTramai is for annotation-driven Spring contexts, where it establishes the application package used for @AiService discovery and loads the profile configurations contributed by modules on the classpath.

It does not select a runtime profile. tramai.profile is the sole runtime selector: missing or standard gives the standard runtime, sovereign gives the sovereign runtime. See Configuration.

Current Boundaries

TramAI does not use dedicated annotations for:

  • streaming selection — it is inferred from the return type (Flow<StreamChunk>)
  • provider-native structured-output toggles
  • tool-calling opt-in per operation — use @Operation(tools = [...]) and @AiTool
  • conversation state — use chat memory plus @ConversationId (see /modules/tramai-memory)