Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- **Authoritative provider routing plan (PR #229).** Introduced `ProviderRoutingPlan` in `tramai-core` — the single immutable snapshot of configured provider routing (`providers`, `routes`, `defaultProvider`) with typed `ProviderId`/`ModelId` value classes and fail-fast build-time validation. Duplicate provider registration now fails with `ConfigurationException` instead of silently replacing the earlier registration; blank IDs, unknown primary/fallback providers, duplicate fallback routes, and unknown defaults are rejected at construction. `ProviderRegistry` remains a public compatibility façade over the plan with unchanged API and resolution order (`ProviderRoute`/`ResolvedProviderRoute` JVM shapes unchanged). The engine freezes the plan into `EngineComponents.ProviderComponents`; standalone composes through the plan builder; `SovereignTramai.Builder` deleted its shadow routing state (`registeredProviders`, `primaryModelRoutes`, `fallbackRoutes`, `defaultProviderName`, `FallbackRoute`) and now validates the shared plan via `SovereignRoutingValidationPolicy`; Spring resolves property-provider vs bean precedence into one unique provider set before the plan builder (explicit beans still override property-backed providers; genuine duplicate user beans fail deterministically). Routing-related sovereign evidence and artifact-verification targets derive from the same frozen plan. Epic 2.2 is complete.

- Refactored the engine's internal runtime composition into an immutable component snapshot; zero public API change and no execution-semantic changes, except the intentional fail-fast rejection of invalid partial approval composition at the engine component boundary.

- **Explicit runtime lifecycle ownership (PR #226).** `Tramai` and `SovereignTramai` are now `AutoCloseable` and own exactly one lazily-created runtime (one engine) shared by every `create()`/`runtime()` call — previously every `create()` leaked an unreachable engine. Closing is idempotent and concurrency-safe; after close, `create()`/`runtime()` and old proxies fail fast with a fixed `IllegalStateException` before any provider work. `TramaiEngine.close()` cancels once and awaits engine-hierarchy termination (self-close safe), and terminates in-flight suspend invocations; the caller continuation is always resumed exactly once. Spring closes the shared runtime via `destroyMethod = "close"`, so multiple `@AiService` beans share one owned engine. TramAI closes only resources it creates; externally supplied providers/stores/clients/observers remain caller-owned. API surface addition is additive: `Tramai`/`SovereignTramai` gain `close()`; all constructor descriptors remain byte-identical to 0.5.0 (note: adding the `AutoCloseable` supertype is source-compatible but affects compiled negative-`instanceof` checks). Epic 1.3 Runtime Lifecycle Ownership is complete.
Expand Down
4 changes: 2 additions & 2 deletions config/quality/maintainability-deviations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ deviations:
metric: globalMutableState
scope: ":tramai-core"
baseline: 3
allowed: 3
reason: "Provider registration is inherently global. Migration to injectable registry planned for 0.7.0."
allowed: 4
reason: "Provider registration is inherently global. Migration to injectable registry planned for 0.7.0. PR #229's new ProviderRoutingPlan adds two mutable-collection findings in its builder/validation path (the per-model fallback staging list and the duplicate-detection set), matching the pre-existing mutable-collection pattern in the same module; resolved in the 0.7.0 registry migration."
acceptedAt: "2026-07-18"
targetPhase: "0.7.0"
owner: "GionaGranchelli"
Expand Down
38 changes: 24 additions & 14 deletions docs/ROADMAP-0.6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,34 +460,44 @@ The exact API may differ, but each group must have one responsibility and explic

## Epic 2.2: Create one provider-routing plan

> **Status:** ✅ Complete — PR #229 (refactor(routing): introduce authoritative provider routing plan).

**Goal:** Eliminate shadow configuration across standalone, sovereign, Spring, and provider-registry builders.

### Proposed model
### Implemented model

`ProviderRoutingPlan` is the single frozen source of configured provider routing:

```kotlin
data class ProviderRoutingPlan(
@JvmInline value class ProviderId(val value: String)
@JvmInline value class ModelId(val value: String)
data class PlannedProviderRoute(val providerId: ProviderId, val effectiveModelId: ModelId)

class ProviderRoutingPlan private constructor(
val providers: Map<ProviderId, ModelProvider>,
val routes: Map<ModelId, List<ProviderRoute>>,
val routes: Map<ModelId, List<PlannedProviderRoute>>,
val defaultProvider: ProviderId?,
)
```

`ProviderRegistry` is a compatibility façade over the plan (existing public API preserved; additive routing-plan APIs introduced — `ProviderRoutingPlan`, `ProviderId`/`ModelId`, `PlannedProviderRoute`, `ProviderRegistry.from(...)`, `ProviderRegistry.routingPlan`, `Tramai.Builder.buildRoutingPlan()`). The engine freezes the plan into `ProviderComponents`; standalone composes through the plan builder; sovereign validates the same plan via `SovereignRoutingValidationPolicy` (no shadow maps); Spring resolves bean-over-property precedence before the plan builder so the canonical model never observes a duplicate.

### Tasks

1. Add typed provider and model identifiers or validated value classes.
2. Make duplicate provider registration fail rather than silently replace.
3. Validate blank names, unknown providers, duplicate routes, invalid defaults, and fallback loops during construction.
4. Expose an immutable routing-plan snapshot for validation and evidence generation.
5. Apply additional sovereign constraints as validation policies over the same plan.
6. Make Spring construct the same routing plan rather than reimplementing route logic.
7. Remove sovereign builder shadow maps after migration.
1. Add typed provider and model identifiers or validated value classes (`ProviderId`, `ModelId`).
2. Make duplicate provider registration fail rather than silently replace (fail-fast at plan build).
3. Validate blank names, unknown providers, duplicate routes, invalid defaults, and degenerate route structures during construction. (Deliberately NOT recursive fallback routing/graph-cycle detection: `fallbackProvider()` keeps the same effective model on another provider and is not a self-loop.)
4. Expose an immutable routing-plan snapshot for validation and evidence generation.
5. Apply additional sovereign constraints as validation policies over the same plan (`SovereignRoutingValidationPolicy`).
6. Make Spring construct the same routing plan rather than reimplementing route logic (property providers + beans merged into one unique set; no Spring-side route validator).
7. Remove sovereign builder shadow maps after migration (`registeredProviders`, `primaryModelRoutes`, `fallbackRoutes`, `defaultProviderName`, `FallbackRoute` deleted).

### Acceptance criteria

- One authoritative object represents configured provider routing.
- Standalone and sovereign modes differ through validation policy, not duplicated state.
- Evidence generation and runtime execution consume the same immutable plan.
- Invalid routes fail before service creation.
- One authoritative object represents configured provider routing.
- Standalone and sovereign modes differ through validation policy, not duplicated state.
- Evidence generation and runtime execution consume the same immutable plan.
- Invalid routes fail before service creation.

---

Expand Down
25 changes: 25 additions & 0 deletions docs/journal/2026-08-14-provider-routing-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Implementation Status — 2026-08-14

## What's Been Implemented

PR #229: **refactor(routing): introduce authoritative provider routing plan** (Epic 2.2 complete).

- `ProviderRoutingPlan` in tramai-core — single immutable snapshot of providers/routes/defaultProvider, typed `ProviderId`/`ModelId` value classes, fail-fast build-time validation (duplicates, blank IDs, unknown providers, duplicate fallbacks, unknown defaults).
- `ProviderRegistry` reduced to a compatibility façade over the plan (public API + `ProviderRoute`/`ResolvedProviderRoute` JVM shapes unchanged, additive API dump only).
- Engine freezes the plan into `EngineComponents.ProviderComponents`; `TramaiInvocationHandler` resolves candidates from the plan.
- Standalone `Tramai.Builder` mutates the canonical plan builder; validates+freezes once at build.
- Sovereign shadow state deleted (`registeredProviders`, `primaryModelRoutes`, `fallbackRoutes`, `defaultProviderName`, `FallbackRoute`); `SovereignRoutingValidationPolicy` validates the shared plan (incl. offline LOCAL constraints); artifact-verification targets derive from the plan.
- Spring merges property providers + `ModelProvider` beans into one unique set (bean overrides same-id property provider) before the plan builder; genuine duplicate user beans fail deterministically. No Spring-side route validator.
- Docs: ROADMAP-0.6.0.md (Epic 2.2 ✅ Complete), CHANGELOG.md, tramai-spring.md, tramai-sovereign.md.

## What's Missing / Blocked

- Nothing for Epic 2.2. P3 note from agy review (close abandoned `Tramai` if sovereign validation throws) deferred — engine is lazily created, no active leak.
- Pre-existing `examples:governed-workflow` apiCheck drift on master (unrelated to this PR; `buildGovernedNetworkPolicyWorkflow` never dumped).

## Current State

- Branch `refactor/0.6.0-provider-routing-plan`, pushed (fix-round commits applied after initial review).
- Local gates: 757 module tests / 0 failures, apiCheck (tramai modules) PASS, verifyPr PASS (268 tasks), verifyCancellationSafety PASS (292=292).
- agy review: merge-ready, 0 P1/P2.
- PR #229 open: https://github.com/GionaGranchelli/tramAI/pull/229 — CI running, awaiting review.
2 changes: 1 addition & 1 deletion docs/modules/tramai-sovereign.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ val receipts = tramai.verificationReceipts()

## Build-Time Validation

`SovereignTramai.Builder.build()` validates at construction time:
`SovereignTramai.Builder.build()` validates the immutable provider routing plan at construction time:

- Profile configuration is present
- Model registry is present
Expand Down
7 changes: 4 additions & 3 deletions docs/modules/tramai-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,11 @@ The module is activated by either:
│ 1. Binds TramaiProperties from application.yml
│ 2. Resolves secret values (env, file, Vault, AWS)
│ 3. Calls AiToolScanner.fromApplicationContext() → discovers @AiTool beans
│ 4. Registers property-defined providers (Anthropic, OpenAI, Ollama, OpenAI-compatible)
│ 4. Resolves property-defined providers (Anthropic, OpenAI, Ollama, OpenAI-compatible)
│ 5. Collects user-defined ModelProvider beans via ObjectProvider
│ 6. Configures model routing, fallbacks, cache, interceptors
│ 7. Builds and returns Tramai instance
│ 6. Merges providers by id; an explicit bean replaces a same-id property provider
│ 7. Registers the unique provider set, then configures model routing, fallbacks, cache, interceptors
│ 8. Builds and returns Tramai instance
└─ @Bean "aiServiceBeanDefinitionRegistrar" (AiServiceBeanDefinitionRegistrar)
implements BeanDefinitionRegistryPostProcessor
Expand Down
73 changes: 72 additions & 1 deletion tramai-core/api/tramai-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,20 @@ public final class dev/tramai/core/provider/BoundedProviderErrorBody {
public fun toString ()Ljava/lang/String;
}

public final class dev/tramai/core/provider/ModelId {
public static final synthetic fun box-impl (Ljava/lang/String;)Ldev/tramai/core/provider/ModelId;
public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z
public final fun getValue ()Ljava/lang/String;
public fun hashCode ()I
public static fun hashCode-impl (Ljava/lang/String;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Ljava/lang/String;
}

public abstract interface class dev/tramai/core/provider/ModelProvider {
public abstract fun complete (Ldev/tramai/core/model/ModelRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun providerId ()Ljava/lang/String;
Expand All @@ -2006,6 +2020,19 @@ public final class dev/tramai/core/provider/ModelProvider$DefaultImpls {
public static fun supportsCapability (Ldev/tramai/core/provider/ModelProvider;Ldev/tramai/core/provider/ProviderCapability;)Z
}

public final class dev/tramai/core/provider/PlannedProviderRoute {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-yuptCEU ()Ljava/lang/String;
public final fun component2-HAG__Ns ()Ljava/lang/String;
public final fun copy-l0_Qrn8 (Ljava/lang/String;Ljava/lang/String;)Ldev/tramai/core/provider/PlannedProviderRoute;
public static synthetic fun copy-l0_Qrn8$default (Ldev/tramai/core/provider/PlannedProviderRoute;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/tramai/core/provider/PlannedProviderRoute;
public fun equals (Ljava/lang/Object;)Z
public final fun getEffectiveModelId-HAG__Ns ()Ljava/lang/String;
public final fun getProviderId-yuptCEU ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/tramai/core/provider/ProviderCapability : java/lang/Enum {
public static final field STREAMING Ldev/tramai/core/provider/ProviderCapability;
public static final field STRUCTURED_OUTPUT Ldev/tramai/core/provider/ProviderCapability;
Expand Down Expand Up @@ -2035,9 +2062,24 @@ public final class dev/tramai/core/provider/ProviderFailuresKt {
public static synthetic fun safeProviderFailure$default (Ljava/lang/String;Ldev/tramai/core/exception/ProviderFailureCode;Ljava/lang/Integer;ZLjava/lang/Long;ILjava/lang/Object;)Ldev/tramai/core/exception/ProviderException;
}

public final class dev/tramai/core/provider/ProviderId {
public static final synthetic fun box-impl (Ljava/lang/String;)Ldev/tramai/core/provider/ProviderId;
public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z
public final fun getValue ()Ljava/lang/String;
public fun hashCode ()I
public static fun hashCode-impl (Ljava/lang/String;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Ljava/lang/String;
}

public final class dev/tramai/core/provider/ProviderRegistry {
public static final field Companion Ldev/tramai/core/provider/ProviderRegistry$Companion;
public synthetic fun <init> (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ldev/tramai/core/provider/ProviderRoutingPlan;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getRoutingPlan ()Ldev/tramai/core/provider/ProviderRoutingPlan;
public final fun resolve (Ldev/tramai/core/annotations/Operation;)Ldev/tramai/core/provider/ModelProvider;
public final fun resolveCandidates (Ldev/tramai/core/annotations/Operation;)Ljava/util/List;
}
Expand All @@ -2055,6 +2097,7 @@ public final class dev/tramai/core/provider/ProviderRegistry$Builder {

public final class dev/tramai/core/provider/ProviderRegistry$Companion {
public final fun builder ()Ldev/tramai/core/provider/ProviderRegistry$Builder;
public final fun from (Ldev/tramai/core/provider/ProviderRoutingPlan;)Ldev/tramai/core/provider/ProviderRegistry;
public final fun singleProvider (Ldev/tramai/core/provider/ModelProvider;)Ldev/tramai/core/provider/ProviderRegistry;
}

Expand All @@ -2071,6 +2114,34 @@ public final class dev/tramai/core/provider/ProviderRoute {
public fun toString ()Ljava/lang/String;
}

public final class dev/tramai/core/provider/ProviderRoutingPlan {
public static final field Companion Ldev/tramai/core/provider/ProviderRoutingPlan$Companion;
public synthetic fun <init> (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDefaultProvider-TfRv3Xo ()Ljava/lang/String;
public final fun getProviders ()Ljava/util/Map;
public final fun getRoutes ()Ljava/util/Map;
}

public final class dev/tramai/core/provider/ProviderRoutingPlan$Builder {
public fun <init> ()V
public final fun build ()Ldev/tramai/core/provider/ProviderRoutingPlan;
public final fun defaultProvider (Ljava/lang/String;)Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;
public final fun fallbackModel (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;
public final fun fallbackProvider (Ljava/lang/String;Ljava/lang/String;)Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;
public final fun model (Ljava/lang/String;Ljava/lang/String;)Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;
public final fun provider (Ljava/lang/String;Ldev/tramai/core/provider/ModelProvider;Z)Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;
public static synthetic fun provider$default (Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;Ljava/lang/String;Ldev/tramai/core/provider/ModelProvider;ZILjava/lang/Object;)Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;
}

public final class dev/tramai/core/provider/ProviderRoutingPlan$Companion {
public final fun builder ()Ldev/tramai/core/provider/ProviderRoutingPlan$Builder;
}

public final class dev/tramai/core/provider/ProviderRoutingPlanKt {
public static final fun resolve (Ldev/tramai/core/provider/ProviderRoutingPlan;Ldev/tramai/core/annotations/Operation;)Ldev/tramai/core/provider/ModelProvider;
public static final fun resolveCandidates (Ldev/tramai/core/provider/ProviderRoutingPlan;Ldev/tramai/core/annotations/Operation;)Ljava/util/List;
}

public final class dev/tramai/core/provider/ResolvedProviderRoute {
public fun <init> (Ljava/lang/String;Ldev/tramai/core/provider/ModelProvider;Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
Expand Down
Loading
Loading