TramAI - governed AI workflows for Java and Kotlin

API and Binary Compatibility

TramAI 0.6.0 is the first release with an enforced compatibility contract over its public surface. This page explains what is frozen, what is allowed to move, how the project proves it, and how you should pin and upgrade.

What Is Protected

Three mechanisms back the consumer promise:

  1. Committed API dumps. The Kotlin Binary Compatibility Validator generates one api/<module>.api signature dump per applicable module, committed in the repository. apiCheck fails when a dump and the current public source disagree.
  2. Stability policy per module. Every module declares an apiStability in config/quality/module-catalog.yml, and the policy for a change depends on that class.
  3. Real compile-based consumer proofs. examples/kotlin-consumer-smoke and examples/java-consumer-smoke compile actual consumer sources against the stable core surface on the minimal consumer classpath.

The first two are consumed by the api-architecture check inside ./gradlew verify060Architecture. The third is produced by two fail-soft compile tasks whose marker output is read as typed evidence by that same check — so a broken fixture fails the architecture gate, not just a fixture task.

The Stability Classes

apiStability in the module catalog is the authoritative classification:

ClassMeaning for consumersModules in 0.6.0
stableFrozen for the release. Any change — breaking or additive — fails the gate.tramai-core (annotations, request models, provider registry, exceptions), tramai-bom
previewUsable, but the shape may still change. A change requires an exact migration entry.The runtime, provider, framework, security, persistence, and higher-capability modules — tramai-engine, tramai-structured, tramai-orchestration, tramai-standalone, all provider adapters, the tramai-spring* modules, tramai-security, tramai-sovereign, tramai-persistence-*, tramai-rag, tramai-embedding, tramai-memory, tramai-vectorstore-*, tramai-scheduler, tramai-observability, tramai-platform
internalImplementation or support surface. Not covered by the compatibility gate. Some of these modules are published, some are not.tramai-testing (published, internal stability), tramai-dashboard, tramai-mcp, tramai-memory-store, tramai-server, tramai-spring-consumer-boundary, tramai-spring-consumer-selective
excludedNot part of the API surface at all.Every examples:* project, including the two consumer smoke fixtures

tramai-dashboard, tramai-mcp, tramai-memory-store, and tramai-server are internal and not published in 0.6.0 — there is no coordinate to depend on.

What "frozen" means precisely

The gate compares two pairs, and both must hold:

  • Source ↔ committed dump. The committed dump must represent the current source. A developer who changes an API and forgets to regenerate the dump fails immediately.
  • Base branch ↔ current dump. This comparison is what drives policy. A stable module whose dump changes at all — even by adding one signature — fails, with no way to authorise the change. A preview or experimental module may change only when an exact, hash-bound entry in config/quality/api-migrations.yml authorises that specific transition. internal and excluded modules have no compatibility gate, so ordinary internal refactors do not fail.

The stable freeze applies to the signature dump, which is why an additive change on tramai-core still needs a deliberate release decision rather than slipping through as "just another overload".

Stability inversion

An API may not expose a type that is weaker-classified than itself: stable may only reference stable, preview may reference stable or preview, and so on. Inversions introduced by a change fail the gate; pre-existing ones are surfaced as non-blocking warnings and tracked separately.

Internal Markers Are Outside the Freeze

Two annotations mark declarations that are technically public (so they can cross module boundaries during composition) but are not application-facing API and may move in any release:

  • dev.tramai.core.observation.secondary.ExperimentalTramaiInternalApi
  • dev.tramai.core.provider.transport.ExperimentalProviderTransportApi

They are registered as BCV non-public markers, which removes them from the dump entirely. That registration is the only supported way to opt a declaration out of the stable freeze: an unmarked new public declaration still enters the dump and still fails the check. Do not treat either annotation as a way to publish an API you intend consumers to call.

Migration Entries

config/quality/api-migrations.yml authorises preview/experimental API transitions. Each entry binds a module, the SHA-256 of the base dump, the SHA-256 of the current dump, the target release, a rationale, and consumer-facing migration text:

# A landed entry from the 0.5.0 line — retained as history, authorises nothing further.
migrations:
  - module: ":tramai-orchestration"
    fromSha256: "c5d9c58e…"
    toSha256: "64c1ff59…"
    targetVersion: "0.5.0"
    rationale: "Intentional additive API; no signature changed."
    migration: "Applications executing createTableSql() alone must additionally execute createSequenceTableSql()."

The registry is fail-closed. Duplicate entries, orphans that match no real transition, hashes that disagree with the actual dumps, and (for an active entry) a targetVersion that is not the current project version all fail the gate. A landed entry is valid history but authorises nothing, so it cannot be reused to smuggle a later change through. stable modules can never use an entry at all.

The practical upshot for you: when a preview module you depend on changes shape, the migration field of the authorising entry tells you exactly what to change in your code.

The Workflow API Stability Boundary

Module-level classification answers "can this coordinate change?". The workflow-facing API boundary answers a narrower question: "which TramAI workflow APIs is an application safe to build against today?" It classifies workflow APIs into Stable, Preview, Internal, and Deferred, independently of module maturity.

The stable workflow surface includes:

  • AI service declaration: @AiService, @Operation, @System, @SystemPrompt, @User, @AiDescription, @AiTool, @ConversationId, and typed request/response data classes.
  • Provider declaration: Tramai.builder() / Tramai.create() in tramai-standalone, the ModelProvider SPI, ProviderRegistry, and the provider adapters.
  • Structured output constraints: @AiRange, @AiMinItems.
  • Deterministic testing: the mock providers and assertion helpers in tramai-testing.
  • Policy and DLP: PolicyEngine, PolicyDecision, PolicyContext, EnforcementPoint, and the data classification levels.
  • Approval gateway contracts: ApprovalGateway, ApprovalRequestResult, SovereignWorkflowResult, and the Java-friendly facades around them.
  • Exceptions: TramaiException and its domain subtypes such as StructuredOutputException, ProviderException, PolicyViolationException, ApprovalSuspendedException.

Explicitly preview: workflow orchestration patterns, runtime evidence export, advanced approval ergonomics, the approval control-plane surface, provider routing ergonomics, custom structured-output validators, tool governance APIs, and the MCP adapter surface.

Explicitly internal: engine proxy dispatch and retry internals, structured-output schema internals, concrete JDBC stores, lease and outbox internals, resume credential custody, and the Gradle verification tasks themselves.

The public, consumer-facing version of that split lives on API Stability.

Consumer Compile Proofs

The two fixtures are the shape of the proof — the compilation is the assertion. If a stable annotation or attribute disappears or changes incompatibly, they stop compiling.

examples/kotlin-consumer-smoke/build.gradle.kts declares the minimal consumer classpath:

dependencies {
    // The minimal consumer classpath: only the stable core contract surface.
    implementation(project(":tramai-core"))
}

and the fixture exercises exactly that surface:

@AiService
interface GreetingService {
    @AiTool(description = "greet")
    fun greet(
        @AiDescription("The user name") name: String,
        @AiRange(min = 1.0, max = 10.0) enthusiasm: Int,
    ): String

    @AiTool(description = "summarize")
    fun summarize(
        @AiDescription("Items to summarize") @AiMinItems(1) items: List<String>,
    ): List<String>
}

The Java fixture (examples/java-consumer-smoke) mirrors it in Java, because a Kotlin-first library still has to be callable from Java. Two guard details matter if you extend them: the producers (verifyKotlinConsumerCompatibility, verifyJavaConsumerCompatibility) are fail-soft — they record a marker JSON with the real compiler exit code and the discovered class count instead of throwing, so the architecture report is still written — and an empty source set is treated as failure, because a fixture with no sources would otherwise "pass" vacuously.

Pinning and Upgrading

Pin tramai-bom and drop the version from every TramAI module coordinate:

dependencies {
    implementation(platform("dev.tramai:tramai-bom:0.6.0"))
    implementation("dev.tramai:tramai-standalone")
    implementation("dev.tramai:tramai-openai")
    implementation("dev.tramai:tramai-structured")
    testImplementation("dev.tramai:tramai-testing")
}
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>dev.tramai</groupId>
      <artifactId>tramai-bom</artifactId>
      <version>0.6.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

For Spring Boot the same pattern applies with the unified starter:

dependencies {
    implementation(platform("dev.tramai:tramai-bom:0.6.0"))
    implementation("dev.tramai:tramai-spring-boot-starter")      // standard + sovereign
    implementation("dev.tramai:tramai-spring-provider-openai")   // provider adapter for Spring
}

Upgrade discipline that follows from the contract:

  • Treat a minor or patch bump of a stable module as safe by construction — the dump is frozen, and any change is a release-level decision, not incidental drift.
  • For preview modules, read the migration entries that shipped with the release before upgrading; they are the authoritative list of consumer-visible changes.
  • Do not depend on @ExperimentalTramaiInternalApi or @ExperimentalProviderTransportApi declarations, and do not depend on internal/not-published modules at all.
  • If you compile against api/ dumps in your own build, remember they are the signature authority for TramAI's own modules, not a stability promise for preview surfaces.