TramAI - governed AI workflows for Java and Kotlin

Migrating to TramAI 0.6.0

This guide covers upgrading an existing application from TramAI 0.5.0 to 0.6.0. Five changes can require an edit; the rest of the release is additive or internal.

Start by moving to the new BOM. If you already use tramai-bom, the version bump is the whole change:

dependencies {
    implementation(platform("dev.tramai:tramai-bom:0.6.0"))
}
<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>
ChangeAction required
Unified Spring Boot starterReplace the old sovereign starter coordinate; no code change
Profile-based runtime selectionMove sovereign allowlists under tramai.sovereign.*; drop the old enable flag
JDBC store exceptionsChange catch (SQLException) to catch (IllegalStateException) or catch (RuntimeException)
Multimodal image retrievalReview fetched URLs against the outbound address rules
Internal API annotationsOnly relevant if you reference cross-module @Experimental* declarations
Custom extensionsEnrol the subproject in the module catalogue and pass the relevant TCK

1. One Spring Boot Starter Coordinate

In 0.5.0 the sovereign runtime had its own starter artifact. In 0.6.0 there is a single canonical starter that composes both runtime integrations, and the old standalone sovereign starter is gone — the build fails on the old coordinate, it does not silently fall back.

// build.gradle.kts
dependencies {
    implementation(platform("dev.tramai:tramai-bom:0.6.0"))
    implementation("dev.tramai:tramai-spring-boot-starter")
    implementation("dev.tramai:tramai-spring-provider-openai") // or -anthropic / -ollama
}

tramai-spring-boot-starter brings in tramai-spring-core (standard runtime) and tramai-spring-sovereign (sovereign runtime). Add exactly one tramai-spring-provider-* adapter, and optionally a tramai-spring-secrets-* adapter if you use vault: or aws-secretsmanager: secret references.

@AiService interfaces become Spring beans with no manual wiring. @EnableTramai is not required — it selects a Spring model, not a TramAI runtime profile.

2. Select the Runtime With tramai.profile

Runtime selection is explicit and profile-only. tramai.profile accepts exactly two values, standard and sovereign (case-insensitive); an unsupported value fails at startup naming the supported set. Omitting the property selects standard.

# Standard cloud-connected runtime (default)
tramai:
  profile: standard

For sovereign, the allowlists move under tramai.sovereign.*. This is a complete working configuration:

tramai:
  profile: sovereign
  sovereign:
    allowed-models:
      - local-invoice-model
    allowed-providers:
      - deterministic-local-provider
    allowed-tools:
      - schedule-payment
    allowed-permissions:
      - payment.schedule
    provider-zones:
      deterministic-local-provider: LOCAL
    models:
      local-invoice-model: deterministic-local-provider

Trust-zone values are LOCAL, EU_CLOUD, and GLOBAL_CLOUD. The configuration is validated at startup, and the rules are strict:

RuleFailure if violated
allowed-models non-emptyStartup failure: missing allowed models
allowed-providers non-emptyStartup failure: missing allowed providers
Every allowed provider has a provider-zones entryStartup failure naming the provider with no trust zone
Every provider-zones key is an allowed providerStartup failure: zone configured for a non-allowed provider
Every models key is in allowed-modelsStartup failure: route for an unknown model
Every allowed model has a models routeStartup failure: allowed model with no route
Every models route targets an allowed providerStartup failure: route to a non-allowed provider

Two things changed shape, so delete them from your configuration:

  • The enable flag is gone. tramai.sovereign.enabled is retained only as a compatibility key, and setting it to false is rejected with a deterministic startup failure. tramai.profile is the sole runtime selector. Remove tramai.sovereign.enabled entirely.
  • Sovereign mode is never implicit. In 0.5.0 the sovereign starter could default the application into sovereign mode through an environment post-processor. That post-processor was removed, so a misconfigured deployment now runs standard rather than silently running sovereign. Select sovereign deliberately.

Standard (non-sovereign) Spring settings are unchanged and stay under tramai.*: default-provider, models, fallbacks, resilience.circuit-breaker.*, resilience.retry.*, cost.token-budget.*, cache.*, and security.*.

3. JDBC Store Exceptions Are Sanitized

In 0.5.0, a JDBC store could propagate the driver's java.sql.SQLException to your code, including connection strings, table structures, and vendor diagnostics. In 0.6.0 every raw database operation in the JDBC stores runs through a sanitizing wrapper:

  • Driver failures — connection errors, constraint violations, SQL syntax errors — are mapped to IllegalStateException with redacted detail.
  • Coroutine CancellationException propagates unwrapped, always.
  • Domain exceptions (TramaiException, store exceptions) and argument-validation failures propagate unwrapped.

Update any catch block that named the JDBC exception type:

// 0.5.0
try {
    approvalStore.consume(approvalId, token, actorId)
} catch (e: java.sql.SQLException) {
    logger.error("store failure", e)
    return Failure.Retryable
}

// 0.6.0 — infrastructure failure surfaces as IllegalStateException
try {
    approvalStore.consume(approvalId, token, actorId)
} catch (e: IllegalStateException) {
    logger.error("store failure", e)
    return Failure.Retryable
}

Do not catch Exception and treat everything as retryable. CancellationException must be rethrown so that cancelling a coroutine actually cancels it — if you must catch broadly, call rethrowIfCancellation() from dev.tramai.core.coroutines first.

If you have no catch block around store calls, nothing changes: an unhandled store failure was always an infrastructure error, and it still is.

4. Multimodal Image Retrieval: Outbound Address Rules

Image URLs are now validated at the address level before a connection is made, so redirects and DNS-based bypasses cannot reach an internal host. ImageDownloader.download(url) enforces:

RuleRejected
SchemeAnything other than http and https — file:, ftp:, jar:, data:, gopher:
User info in the URLhttp://user:pass@host/
Hostnamelocalhost
Loopback127.0.0.0/8, ::1
Link-local169.254.0.0/16 (including cloud metadata endpoints), fe80::/10
RFC 1918 private10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fec0::/10
Other reserved rangesCGNAT 100.64.0.0/10, IPv6 unique-local fc00::/7, any-local 0.0.0.0 / ::, multicast
RedirectsEach redirect destination is revalidated; at most 3 redirects
Size20 MB per image, enforced as a bounded read rather than a truncation

The practical consequence: an application that used image URLs pointing at an internal file service, a private S3-compatible endpoint, or a file:// path must switch to a publicly resolvable HTTPS URL, or fetch the bytes itself and pass an ImagePart instead of an ImageUrlContent.

If the validation types are annotated @ExperimentalProviderTransportApi, that annotation describes the API status, not the protection — the checks always run.

5. Internal APIs Are Outside the Compatibility Promise

Declarations annotated @ExperimentalTramaiInternalApi or @ExperimentalProviderTransportApi are internal cross-module contracts. They are excluded from the stable API freeze and may change in any patch release.

If your code references one directly, replace it with the corresponding stable facade under dev.tramai.core.*. If no stable equivalent exists, that is a signal the capability was not intended for application code — wrap the supported entry point instead.

Both annotations are the runtime's mechanism for keeping the stable surface honest: the parts that are not a promise are named, so the parts that are remain trustworthy.

6. Building Extensions

If you develop custom TramAI providers, stores, or plugins as Gradle subprojects in the TramAI repository, two gates apply that did not exist in 0.5.0.

Enrol the subproject in the module catalogue. Every Gradle project must appear exactly once in config/quality/module-catalog.yml with its maturity, visibility, owner, dependency policy, release inclusion, layer, and publishability. A project with no catalogue entry fails the architecture verification, and its layer also determines which dependencies are legal.

Pass the relevant contract suite. ProviderTck in tramai-testing is the offline behavioral contract for a ModelProvider; the harness pins the expected provider identity and capabilities, so a provider passes by matching the contract, not by self-declaring. A custom store implementation should pass the matching TCK:

ExtensionContract suite
Provider adapterProviderTck
Approval storeApprovalStoreTck
Approval continuation storeApprovalContinuationStoreTck
Audit storeAuditStoreTck
Suspended invocation storeSuspendedInvocationStoreTck
Workflow checkpoint storeWorkflowCheckpointStoreTck
Workflow lease storeWorkflowLeaseStoreTck
Sovereign audit outbox storeSovereignOpsAuditOutboxStoreTck
Chat memory storeChatMemoryStoreTck

These suites live in tramai-testing test fixtures and run offline and deterministically.

What Does Not Change

  • The @AiService, @Operation, @AiTool, @AiRange, @AiMinItems, @System / @User, and @ConversationId declaration model.
  • The ModelProvider SPI and the provider adapter behavior.
  • The tramai-standalone builder DSL and the runtime entry points (tramai-standalone, tramai-spring).
  • Structured-output schema generation for Kotlin return types and JavaBean DTOs.
  • Policy enforcement points and PolicyConfiguration.secure() as the deny-by-default baseline.
  • The approval suspension and resume contract, including exactly-once consumption.
  • The BOM coordinate. Only the version changes.
  • A standard Spring application: with no tramai.profile set, the 0.5.0 configuration keeps working after the version bump.

Checklist

  • Bump tramai-bom to 0.6.0 (or every explicit dev.tramai:* version).
  • Replace the sovereign starter coordinate with dev.tramai:tramai-spring-boot-starter.
  • Set tramai.profile explicitly (standard or sovereign); remove tramai.sovereign.enabled.
  • Move sovereign allowlists under tramai.sovereign.* and confirm every startup validation rule holds.
  • Replace catch (java.sql.SQLException) with catch (IllegalStateException) or catch (RuntimeException) around store calls.
  • Confirm CancellationException is rethrown, not swallowed, in your broad catch blocks.
  • Review every image URL used by multimodal operations for scheme, host, and size.
  • Remove references to @ExperimentalTramaiInternalApi / @ExperimentalProviderTransportApi symbols.
  • For extensions: enrol the subproject in config/quality/module-catalog.yml and run the relevant TCK.
  • Re-run your acceptance suite against 0.6.0.

For a per-change description of the release itself, see 0.6.0 — Clarity Is a Runtime Property. For older upgrades, see the Migration Guide.