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:
- Committed API dumps. The Kotlin Binary Compatibility Validator generates one
api/<module>.apisignature dump per applicable module, committed in the repository.apiCheckfails when a dump and the current public source disagree. - Stability policy per module. Every module declares an
apiStabilityinconfig/quality/module-catalog.yml, and the policy for a change depends on that class. - Real compile-based consumer proofs.
examples/kotlin-consumer-smokeandexamples/java-consumer-smokecompile 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:
| Class | Meaning for consumers | Modules in 0.6.0 |
|---|---|---|
stable | Frozen for the release. Any change — breaking or additive — fails the gate. | tramai-core (annotations, request models, provider registry, exceptions), tramai-bom |
preview | Usable, 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 |
internal | Implementation 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 |
excluded | Not 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
stablemodule whose dump changes at all — even by adding one signature — fails, with no way to authorise the change. Aprevieworexperimentalmodule may change only when an exact, hash-bound entry inconfig/quality/api-migrations.ymlauthorises that specific transition.internalandexcludedmodules 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.ExperimentalTramaiInternalApidev.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()intramai-standalone, theModelProviderSPI,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:
TramaiExceptionand its domain subtypes such asStructuredOutputException,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
stablemodule as safe by construction — the dump is frozen, and any change is a release-level decision, not incidental drift. - For
previewmodules, read the migration entries that shipped with the release before upgrading; they are the authoritative list of consumer-visible changes. - Do not depend on
@ExperimentalTramaiInternalApior@ExperimentalProviderTransportApideclarations, 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.
Related Pages
- API Stability — the consumer-facing stability statement
- Module Architecture — how stability classes are declared in the catalog
- Quality Gates — where
apiCheckand the smoke proofs run - Migration — upgrading between TramAI releases
