# Changelog

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Each entry's `BREAKING` items are surfaced inline. PR links point at the GitHub PR that landed the change. Linear issue IDs are not cited.

**Write each bullet and each paragraph on ONE line. Do not hard-wrap.** This file is not only read here: `release.yml` extracts the version's section verbatim as the GitHub Release body, and GitHub renders release bodies with GFM hard-breaks — every newline inside a paragraph becomes a `<br>`. A bullet wrapped at 80 columns therefore arrives on the release page as a stack of half-width lines while its neighbours fill the column. The damage is invisible while reading the file, because `.md` files render the same newline as a *soft* break. Reflowed once in `#261`; `0.16.3` was hard-wrapped again immediately afterwards because this paragraph did not exist yet.

## About the pre-0.3.0 entries

The ten releases below `0.3.0` were reconstructed on 2026-08-01 from Maven Central, which is the only surviving record of them: no git tag in this repository points at any of those versions, and this file began at 0.3.0.

They are deliberately thin. Each states what Central can prove -- the publish date and the exact set of artifacts carrying that version -- and nothing more. Reconstructing feature lists from artifact contents would be guesswork, and a plausible invention is worse here than an honest gap: these entries exist to make `@since` tags verifiable, and a verifiable date plus a verifiable module list does that.

The numbering is not chronological. `0.1.0` was published first, in December 2025; the `0.0.x` series followed two months later, with `0.1.1` between them. The module sets move the same way -- the earliest releases carry modules that no longer exist in the reactor (`tnsai-cli`, `tnsai-acp`, `tnsai-agui`, `tnsai-rag`, `tnsai-research`, `tnsai-personal-ai`, the `tnsai-store-*` family) -- and the layout settles into today's from 0.2.1 onward.

## [0.16.4] - 2026-09-07

### Added

- `LLMConfigurationSource` puts an external settings layer in front of `@RoleSpec.llm()` and `@AgentSpec.llm()`. Register one with `SCOPBridge.llmConfigurationSource(...)` and it supplies `provider`, `model`, `temperature`, `maxTokens`, `endpoint` or `apiKeyEnv` per agent, by the agent's own name. Two implementations ship: `LLMConfigurationSource.folder(path)` reads `<folder>/<AgentName>.json`, and `LLMConfigurationSource.environment()` reads `TNSAI_LLM_<AGENT>_<FIELD>` through `EnvLoader`, extending to provider, model, temperature and maxTokens the System-property-before-environment precedence that `LLMConfiguration` already applied to base URL and API key. The source is re-read on every resolution, so an edited file is visible to the next one; a source that exists but cannot be read throws `LLMConfigurationException` rather than falling back to the annotations, because answering with a model other than the configured one is worse than not answering. Values from a source are validated at the boundary, and only from a source — annotation values are never re-validated, because rejecting a shipped `@LLMSpec(maxTokens = -1)`, which resolution has always read as "let the provider decide", would turn a working deployment into a hard failure. A source's `provider` must name one `LLMConfiguration` can actually route (an unvalidated string would let the source pick the transport, and `LLMSpec.Provider.HUGGINGFACE` has no endpoint of its own, so it would fall back to the Ollama default carrying whatever `apiKeyEnv` came with it), `temperature` must lie in the 0.0-2.0 range `@LLMSpec` documents (`1e40` is a finite double that narrows to `Infinity` as a float), `maxTokens` must be non-negative and is parsed with `BigDecimal.intValueExact()` so that `4294967297` fails instead of silently narrowing to `1`, blank strings are refused because blank is not "unset", unknown JSON keys are an error rather than a no-op, and a file over 1 MiB is refused. A source is trusted infrastructure: `apiKeyEnv` names any environment variable or System property the process can see and `endpoint` decides where that value is sent, so each resolution logs both when the source sets them ([#280](https://github.com/TnsAI-Framework/TnsAI/pull/280)).


### Changed

- `SCOPBridge.resolveLLMSpec` no longer returns the first annotation tier that declares a model whole. Settings are now combined in two groups. **Routing** — `provider`, `model`, `endpoint`, `apiKeyEnv` — is taken as a unit from the highest-precedence tier that declares a `model` — exactly the tier that used to win outright, so no configuration that resolved before now fails to, because those four only mean anything together: a model belongs to a provider, an endpoint speaks one provider's wire format, and a key is issued by one provider. **Tuning** — `temperature` and `maxTokens` — merges field by field across every tier, so a tier declaring only a temperature now contributes it where before the whole tier was skipped because `hasModel()` was false. Precedence is unchanged (external source, then Role, then Agent, then Playground) and the terminal rule stands: if no layer supplies a model the resolution still yields `Optional.empty()` and logs the same warning. A tier declaring `model` without `provider` therefore resolves to `@LLMSpec`'s default provider rather than a lower tier's, which is what that annotation means on its own. A member counts as declared when it differs from the default `@LLMSpec` declares for it — the only way to tell the two apart, since `@AgentSpec.llm()` and `@RoleSpec.llm()` both default to `@LLMSpec()`; grouping routing is also what makes the corollary harmless, because a member explicitly set to its own default (`provider = OLLAMA`) can no longer be overridden by a different tier's value. The external source is the one layer that overlays every field individually, which is what makes a config file able to name just a model without restating provider and temperature. ([#280](https://github.com/TnsAI-Framework/TnsAI/pull/280), [#281](https://github.com/TnsAI-Framework/TnsAI/pull/281)).


### Fixed

- The Docker server smoke test fails closed without a token instead of passing on an unauthenticated bind, and now verifies authenticated health, readiness and API access plus the negative auth cases. Manual recovery images are rebuilt only from an immutable release tag whose commit SHA and Maven version match the requested image tag, the published version tag is pulled with an empty temporary Docker config so its exact digest is verified as an anonymous consumer rather than as the publisher, and every readiness request stays inside the remaining wall-clock deadline. ([#279](https://github.com/TnsAI-Framework/TnsAI/pull/279)).
- This file states its own one-line rule, so release bodies stop regressing to hard-wrapped text. `release.yml` extracts a version's section verbatim as the GitHub Release body and GFM renders every newline inside a paragraph as a `<br>`, so an 80-column bullet arrives on the release page as a stack of half-width lines. `0.16.1` and `0.16.2` were written correctly; `0.16.3` was hard-wrapped again immediately after `#261` reflowed the file, because the rule lived only in a merged PR description. It is now written down where the next author reads it. ([#278](https://github.com/TnsAI-Framework/TnsAI/pull/278)).


### Known issues

- A configured `endpoint` or `apiKeyEnv` is dropped on the Core SPI client path: `LLMClientProvider.create` carries neither, so with `tnsai-llm` on the classpath an endpoint pin — the control an operator sets to keep traffic off the public internet — is silently ignored and the request goes to the provider's default. This predates the external source layer but becomes reachable from configuration with it, so `SCOPBridge` now logs a warning when that path is taken with either configured. Tracked separately; the real fix is an additive `LLMClientProvider` change.

## [0.16.3] - 2026-09-04

### Fixed

- The release workflow's japicmp report steps no longer gate the Maven Central deploy. They observe only — rendering the summary and uploading the raw reports — but sat between the japicmp pre-flight and `Deploy to Maven Central` without `continue-on-error`, so a failure in either skipped the deploy, and the artifact name being stable per version made "re-run failed jobs" hit a 409 and die before reaching Deploy. Both now carry `continue-on-error`, the artifact overwrites, and the summary path is asserted rather than falling back to a shared `/tmp` (ADR-005, [#275](https://github.com/TnsAI-Framework/TnsAI/pull/275)).
- `RoleRagBinding.invalidate(role, source)` refreshes one exact canonical knowledge source without resetting sibling sequential cursors. Ranked bindings containing that source retire as coherent snapshots (documents, result cache, hierarchy/temporal indexes, and graph corpus); bindings scoped away from it retain identity. Null, blank, disabled, and unknown source names fail loudly, while the existing role-wide overload keeps its behavior. Deterministic Role-owned Qdrant/pgvector scopes remain restart-compatible: the narrow overload does not clear an owner another process may share, and rebuilt snapshots exclude remote rows outside their current corpus.

- Built-in TEXT, MARKDOWN, JSONL and other structured-text extraction no longer links the optional `jdk.compiler` module during provider initialization. Java 21 JRE deployments can discover the real provider and ingest TEXT/JSONL; explicit SOURCE_CODE uses a bounded structural fallback identified by `parser=structural` and `fallbackReason=jdk-compiler-unavailable`, while a full JDK continues to use the compiler-backed Java parser.

- `ContentExtractorRegistry` keeps the exact provider-construction or contract-inspection throwable in the diagnostic cause chain. Its public exception message remains framework-controlled, names the provider when it is known, and does not copy provider exception content.

## [0.16.2] - 2026-09-03

Empty JSONL no longer takes down sibling sources, and a sequential cursor no longer forks when a sibling runtime path sits on the same Role.

### Fixed

- An empty or blank-line-only JSONL file is 0 records, not a format error. Declared `JSONL` used to fail structural validation (`JSON Lines content is blank`), which aborted `RoleRagBinding.build` and dropped sibling sources in the same Role — a blank `transcript.jsonl` made identity retrieval fail closed. Malformed JSONL (non-object line, truncated JSON) still fails.

- Sequential cursor identity follows the sequential source, not every builder source on the Role. A sibling runtime path (`episode`) no longer forks `@Sequential(source = "questions")` from `SequentialUnitReader.forTarget(this, "questions")`, so the question list does not restart every turn. Distinct templates of the sequential source itself still do not share units.

## [0.16.1] - 2026-09-03

Sequential hosts can see when the cursor is empty, and SCOPBridge can pass a run-time folder into `@Retrieval`. Neither needs to count the source file or call retrieve from the action body.

### Added

- `SequentialUnitReader` exposes `size()`, `remaining()`, and `isExhausted()`. `@Sequential` still consumes `next()` before the action body, so `remaining() == 0` after the last delivered unit is the lifecycle signal to stop scheduling, and `isExhausted()` (last `next()` returned empty) is the signal to return an application sentinel such as `DONE`. Hosts no longer count the source file.

- `SCOPBridge.executeAction` can take a `Map<String, Path>` of `@KnowledgeSource` name → run-time location, and `resolvedSourcePaths(...)` sets the same map for subsequent calls. The map is copied onto the dispatch context under `RetrievalSpi.RESOLVED_SOURCE_PATHS_KEY`; `DefaultRetrievalSpi` converts it through `RoleRagBinding.locateDeclaredSources` — the same locate-and-override rules as `forRole(Class, Map)` — so `@Retrieval` on a source with empty `path` grounds without the action body calling the factory. Unknown names fail at dispatch. Omitting the map keeps annotation paths only.

### Fixed

- `ResearchRole` Javadoc no longer teaches pre-chunking Phase 1 RAG (hash-only embeddings, whole-file ingest, deferred rerank / expansion / cache). It now matches the live `RoleRagBinding` / `FileIngestionService` path and names every strategy that warns on the hash-embedding fallback (`SEMANTIC`, `HYBRID`, `HIERARCHICAL`, `TEMPORAL`); a source-bound test keeps that list honest.

## [0.16.0] - 2026-09-02

Completes the move to `dev.tnsai`: the old coordinate is no longer served at all. Alongside it, a dependency refresh carrying one security fix, and a release pipeline that can now be rehearsed without publishing.

### Added

- `UpgradeTnsAI_0_16_0` OpenRewrite recipe (`dev.tnsai.rewrite`). Rewrites consumer coordinates to `dev.tnsai` and **marks** every call site of the APIs 0.16.0 removes or changes — the `KnowledgeBase` bridge, the two getters, `ChatKnowledgeBinding.snapshot`, the six-argument `IdempotencyResolver.execute` — rather than guessing a rewrite, since each needs a name, a binding or a policy only the consumer can choose. The recipe description states the decision at each marker. 0.15.0 remains without a recipe; its entry above already records why.

- A `@KnowledgeSource` can now name a location that does not exist until run time. `RoleRagBinding.forRole(Class, Map<String, Path>)` takes the Role's declarations as written and supplies the resolved path for the named ones; `KnowledgeSourceConfig.withPath(String)` is the copy-with-new-location it is built on. The declaration keeps everything it can say — name, format, selectors, unit — and leaves `path` empty. No template language enters the annotation, and the resolved path takes part in the binding key, so two runs with different folders get two bindings.

- `AgentBuilder.idempotencyStore(...)` exposes the store behind `@Idempotent`-guarded tools, so the durable `RedisIdempotencyStore` / `PostgresIdempotencyStore` implementations are reachable from the supported builder path instead of the hardcoded per-process default.

- `AgentBuilder.liveChatKnowledge(String)` declares that chat grounds on a knowledge source the application supplies at runtime, rather than one described by a `KnowledgeSourceConfig`. Initialization resolves nothing for such a source and creates no retrieval engine on its account; the agent starts ungrounded until `Agent.setChatKnowledgeBinding` supplies a `ChatKnowledgeBinding.live(...)`.

  This is for corpora the application owns and keeps mutating — a chat session's uploaded documents, a workspace index. Previously the only way to ground chat on one was the legacy `Agent.setKnowledgeBase` bridge, which bypasses validation entirely.

  The binding must be owned by the agent and carry exactly the declared name, enforced by the same check the declarative path uses, so chat still never grounds on a source nobody named. The declarative `@ChatKnowledge` path, builder-over-annotation precedence, and action-level `addKnowledgeSource`/`knowledgeSources`/`retrieval` are unchanged.

- `SCOPBridge.prepareConversationForDispatch` creates a transport-neutral, request-local conversation copy with freshly rendered `@State` values. Static persona instructions stay stable; multiple owners use caller order and only explicitly annotated fields are exposed.

### Changed
- `RoleRagBinding.invalidate(Class)` keeps the retired generation's vectors — keyed by embedding-provider compatibility and chunk-content hash, held softly, replaced on the next invalidation — and the rebuild embeds only chunks whose content or provider changed. Measured with two static sources (60 chunks) and one appended transcript line: 1 embed on rebuild instead of 61; a sibling per-run binding of the same Role rebuilds with 0 embeds instead of 30. Loading, chunking, BM25 and every other index are still rebuilt, so the role-wide coherence guarantee is unchanged. No per-source invalidation API is added — see ADR-003.
- Scoped `RoleRagBinding` indexes (per-source `@Retrieval(sources = ...)` and `strategyFor(..., sources)`) now reuse the embeddings computed by the full build instead of re-embedding every document once per source scope; a stalled embedding provider can no longer block scoped index construction.

- Dependency refresh across the reactor:
  - `software.amazon.awssdk` 2.44.12 → 2.54.1 ([#219](https://github.com/TnsAI-Framework/TnsAI/pull/219))
  - `com.fasterxml.jackson` 2.22.1 → 2.22.2 and 3.2.1 → 3.2.2 ([#212](https://github.com/TnsAI-Framework/TnsAI/pull/212))
  - `com.squareup.okhttp3` 5.4.0 → 5.5.0 ([#213](https://github.com/TnsAI-Framework/TnsAI/pull/213))
  - `io.opentelemetry` 1.64.0 → 1.65.0 ([#221](https://github.com/TnsAI-Framework/TnsAI/pull/221))
  - `io.micrometer:micrometer-registry-prometheus` 1.16.5 → 1.17.1 ([#220](https://github.com/TnsAI-Framework/TnsAI/pull/220))
  - `org.mongodb:mongodb-driver-sync` 5.9.2 → 5.10.0 ([#215](https://github.com/TnsAI-Framework/TnsAI/pull/215))
  - `com.github.pengrad:java-telegram-bot-api` 9.6.0 → 10.1.0 ([#216](https://github.com/TnsAI-Framework/TnsAI/pull/216))
  - `ch.qos.logback:logback-classic` 1.6.2 → 1.6.3 ([#214](https://github.com/TnsAI-Framework/TnsAI/pull/214)) — see **Security**
- Build-only: `org.openrewrite:rewrite-bom` 8.89.1 → 8.90.4 ([#235](https://github.com/TnsAI-Framework/TnsAI/pull/235)), `spotbugs-maven-plugin` 4.10.3.0 → 4.10.4.0 ([#217](https://github.com/TnsAI-Framework/TnsAI/pull/217)), `actions/setup-java` 5 → 6 ([#236](https://github.com/TnsAI-Framework/TnsAI/pull/236)). No effect on published artifacts.

- **BREAKING (API):** `IdempotencyResolver.execute(...)` takes an additional `boolean strictStoreWrites` and the six-argument form is gone. In-repo callers are updated; external callers pass `false` to keep the previous behaviour, or `true` to have a failed store write surface instead of being logged and swallowed. No OpenRewrite recipe ships for this: the new argument encodes a policy decision per call site, so it cannot be filled in mechanically without choosing that policy for the caller.

### Removed

- **BREAKING (API):** the legacy `KnowledgeBase` bridge is removed — ten declared members across five types (thirteen consumer-visible, as japicmp counts: interface defaults plus `METHOD_REMOVED_IN_SUPERCLASS`):

  - `AgentBuilder.knowledgeBase(...)`, `AgentBuilder.knowledgeBaseTopK(...)`
  - `Agent.setKnowledgeBase/getKnowledgeBase`, `Agent.setKnowledgeBaseTopK/getKnowledgeBaseTopK`
  - `AgentOrchestrator.setKnowledgeBase/setKnowledgeBaseTopK` and their getters
  - `AgentChatOrchestrator`'s `getKnowledgeBase`/`getKnowledgeBaseTopK` SPI defaults
  - `ChatKnowledgeBinding.snapshot(...)` and `ChatKnowledgeBinding.SNAPSHOT_SOURCE`

  **The `KnowledgeBase` type itself is not removed**, nor is anything else in `com.tnsai.knowledge`. What goes is the *bridge* — the public entry points that attached a corpus to chat outside the canonical retrieval configuration. `KnowledgeBaseRetriever`, `knowledgeBaseConfig` and the `ChatKnowledgeBinding.create/live` factories all stay; `live(...)` is now the documented way to bind such a corpus.

  See **Migration** for the replacement.

- **BREAKING (coordinates):** the `io.github.tansuasici` relocation POMs are no longer published ([#240](https://github.com/TnsAI-Framework/TnsAI/pull/240)). 0.15.1 shipped relocation stubs under the old groupId so that existing builds kept resolving; from 0.16.0 the old coordinate is not served for this version at all. A build still declaring `io.github.tansuasici` and bumping to 0.16.0 fails to resolve. See **Migration**.

### Fixed
- STRUCTURAL and LLM chunk context no longer embeds the absolute file path of the chunk into the indexed text; it names the document as `<source>:<path relative to the source root>` instead. The absolute path differs between machines and between runs, so every enriched chunk carried run-specific tokens that shifted BM25 length statistics and vector norms and made rankings near a margin flip from one run to the next — the `RetrievalQualityRegressionTest` gate failed and passed on the same commit. Persistent vector backends holding STRUCTURAL or LLM sources should re-ingest; the display body and provenance origin are unchanged.

- Owner-aware SCOP LLM dispatch now refreshes dynamic state even when optional chat retrieval is disabled or unavailable. Stored history is not mutated, prior `Current State` sections are replaced rather than duplicated, and the same preparation contract is available to streaming consumers. Existing applications that persist an unmarked pre-0.16 system prompt must rebuild it once instead of relying on ambiguous heading parsing.
- Containerized builds no longer leave root-owned files in the shared runner workspace. That was the most frequent cause of release-run failure — three of the six recorded runs died at the checkout step because of it ([#239](https://github.com/TnsAI-Framework/TnsAI/pull/239)). CI only; no library change. Other causes of those failures are tracked separately and not all of them are closed.

- The `idempotencyHint = REQUIRED` reliability gate now matches its own documentation: the key-strategy check is an allowlist (`HASH_INPUT` or `EXPLICIT`, as `Tool`'s Javadoc states) instead of a denylist, an `EXPLICIT` policy is rejected at the gate when the tool target does not implement `IdempotencyKeySupplier` (with a message naming the fix), a failed store write after the guarded body ran surfaces as `IdempotencyException` instead of a warn line for REQUIRED tools, and running REQUIRED on the default in-memory store logs a startup-visible warning naming `AgentBuilder.idempotencyStore(...)`.

### Security

- `logback-classic` 1.6.3 carries the upstream response to [CVE-2026-19880](https://www.cve.org/cverecord?id=CVE-2026-19880), which affects `MDCBasedDiscriminator` as used by `SiftingAppender`. TnsAI does not configure a `SiftingAppender` itself; the bump matters for applications that do ([#214](https://github.com/TnsAI-Framework/TnsAI/pull/214)).

### Migration

Re-ingest FILE sources declared with `chunkContext = STRUCTURAL` or `LLM` into persistent vector backends (Qdrant, pgvector): the indexed text no longer contains the absolute file path, so the stored vectors and BM25 statistics of those chunks are stale.

Consumers already on `dev.tnsai` need no action.

Consumers still on `io.github.tansuasici` must change the groupId before taking 0.16.0. The artifact IDs and the Java packages (`com.tnsai.*`) are unchanged, so this is a coordinate edit and nothing more:

```xml
<dependency>
  <groupId>dev.tnsai</groupId>
  <artifactId>tnsai-bom</artifactId>
  <version>0.16.0</version>
  <type>pom</type>
  <scope>import</scope>
</dependency>
```

Releases up to and including 0.15.1 remain resolvable under the old groupId; only new versions stop appearing there.


### Migrating off the `KnowledgeBase` bridge

A builder that attached a corpus directly:

```java
AgentBuilder.create()
    .knowledgeBase(corpus)
    .knowledgeBaseTopK(5)
    .build();
```

now declares an application-supplied chat source and binds the corpus at runtime:

```java
Agent agent = AgentBuilder.create()
    .liveChatKnowledge("my-corpus")     // @since 0.16.0
    .build();

agent.setChatKnowledgeBinding(ChatKnowledgeBinding.live(
        agent.getId(), "my-corpus", 5, corpus));
```

The declared name and the binding's name must match, and the binding must be owned by that agent — the agent rejects a mismatch rather than grounding chat on a source nobody named. Retrieval behaviour is unchanged: the same keyword, fail-open, uncached path the bridge used internally.

`Agent.getKnowledgeBase()`/`getKnowledgeBaseTopK()` have no replacement by design. They reported orchestrator state that retrieval never consulted; the values that matter live on the binding, as `getChatKnowledgeBinding()` → `config().sources()` and `config().topK()`.
## [0.15.1] - 2026-08-26

First publish under `dev.tnsai`. Same code as 0.15.0; only the Maven coordinates change.

### Changed

- **BREAKING (coordinates):** Maven groupId moves from `io.github.tansuasici` to `dev.tnsai` (the reverse of [tnsai.dev](https://tnsai.dev)). Artifact IDs stay (`tnsai-bom`, `tnsai-core`, …). Java packages stay `com.tnsai.*`. 0.15.0 and earlier on `io.github.tansuasici` stay immutable. This release also publishes relocation POMs at `io.github.tansuasici:*:0.15.1` so a version bump follows with a Maven warning. Mechanical POM rewrite: `dev.tnsai.rewrite.ChangeMavenGroupId`.

### Migration

```xml
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>dev.tnsai</groupId>
      <artifactId>tnsai-bom</artifactId>
      <version>0.15.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
```

Or let OpenRewrite rewrite the groupId:

```sh
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=dev.tnsai:tnsai-rewrite:0.15.1 \
  -Drewrite.activeRecipes=dev.tnsai.rewrite.ChangeMavenGroupId
```

## [0.15.0] - 2026-08-26

RAG ingest is first-class: units, overlapping windows, optional chunk context, and `RagDiagnostics`. Chat knowledge is an explicit binding. Contract-net no longer fabricates proposals. **Upgrading rebuilds every RAG index.**

### Added

- `AgentBuilder.chatKnowledge(sourceName)` explicitly binds one canonical builder `KnowledgeSourceConfig` to chat using the builder's `RetrievalConfig` and top-K. This enables runtime-configured final `ConfigurableAgent` instances without class annotations while rejecting unknown, duplicate, disabled, or legacy-conflicting sources. Existing action-level `knowledgeSources` and `retrieval` semantics remain unchanged; chat never guesses among multiple sources.
- `SCOPBridge.principal`, `liabilitySink`, `authorityScope`, and `getInstance(principal, sink, scope)` are the public accountability wiring surface for `executeAction`. There is no silent no-op sink.
- `RagDiagnostics` reports what a Role's `@KnowledgeSource` and `@Retrieval` declarations resolved to: the embedding in use and whether it is the bundled hash fallback, installed or absent reranker / query-expander / graph-store providers without throwing, and per-source format, document and chunk counts, and ingest fingerprint. `of(Role)` never ingests; `of(Role, true)` is the explicit flag that constructs the binding.
- `@KnowledgeSource.unit` (`KnowledgeUnit`, default `AUTO`) declares how a file becomes documents — `DOCUMENT`, `LINE`, `HEADING`, `CHUNK`. `AUTO` is the existing size-driven hybrid and remains byte-identical to the previous ingest path. `LINE` emits one position-addressed unit per non-blank line, `HEADING` shares the Markdown `#{1,6}` boundary rule, `DOCUMENT` requires the file to fit one retrieval unit, and `CHUNK` applies the shared chunker even to small files. Invalid unit/format pairs fail before indexing. A non-`AUTO` unit is a fingerprint field, so changing the declaration rebuilds and re-embeds that source without globally bumping `CHUNKER_VERSION`.
- Opt-in contextual retrieval on `@KnowledgeSource.chunkContext`. `NONE` (default) omits the extra fingerprint fields. `STRUCTURAL` prepends origin/path/lines before embedding and BM25; `LLM` uses an installed `ChunkContextGenerator` (TnsAI ships none), receives bounded adjacent-chunk surrounding text, and caches by SHA-256 of the complete request plus provider template identity. The cache is bounded and cleared on Role invalidation. Generation is admitted, interruptible, and times out. Prefixes cap at `ChunkContext.MAX_PREFIX_CHARS`. Sequential reads, chat snapshot/live evidence, and both renderer overloads restore `ChunkContext.DISPLAY_BODY_METADATA_KEY`. A generator failure mid-batch does not publish a partial index. `unit = LINE` rejects any non-`NONE` context.
- `NormalizedDocumentChunker` overlaps fixed-size windows by 15 lines (and the matching character fraction) so a sentence that straddles an arbitrary 100-line cut is still present in one chunk. Semantic boundaries — markdown headings, source-code symbols — do not overlap. `LINE` and `HEADING` units also suppress overlap: they emit position-addressed slices, not repeated windows. `CHUNKER_VERSION` is now `3`; existing indexes must be rebuilt.
- `NormalizedDocumentChunker.MAX_LINES_PER_DOCUMENT` states the document ceiling that was previously implicit in `MAX_CHUNKS_PER_DOCUMENT * MAX_LINES_PER_CHUNK`. Its value is unchanged at 102,400 lines. The legacy per-document (1,024), per-source (4,096), and repeated-metadata (65,536) caps remain the base policy. Overlapping windows receive separate fourfold expansion budgets of 4,096, 16,384, and 262,144 respectively, so `LINE`/`HEADING` and other overlap-free modes do not inherit the higher resource allowance. The conservative bound follows from each new line window intersecting at most two legacy line windows and character overlap at most doubling each intersection.

### Changed

- **BREAKING:** `MultiRoundContractNet` can no longer fabricate proposals: the `Math.random()`-based default `requestProposal()` is deleted and the builder requires a caller-supplied `ProposalRequester` (contractor, task, round, target score, timeout) — constructing without one fails fast with a message naming the missing input. `ContractNetAdapter` likewise needs a requester: the no-arg default registration now fails fast in `negotiate()` with a reason naming the missing input, and the one-arg constructor takes the source. `proposalTimeout` is now handed to the requester. There is no `tnsai-rewrite` recipe: the mechanically rewritable half of this break (subclass overrides of `requestProposal`) has no possible subject — the constructor has always been private — and the remaining break requires a caller-supplied function, which a recipe cannot synthesize. Two answer guards ship with the change, both warn-logged: a proposal whose `contractorId` names a different contractor than the one asked is dropped, and — symmetrically, since the award decision trusts the proposal's economics — a proposal whose `taskId` belongs to a different task is dropped. The protocol now enforces the task's monotonic session deadline across proposal request, one-read snapshot/validation, evaluation, and pre-award callbacks; per-request waits are capped by both `proposalTimeout` and the remaining session budget, and a missing, zero, or negative `proposalTimeout` now fails at build time. Proposal economics must be finite and non-negative, confidence must be finite and within `[0,1]`, and capabilities must be non-null. Requesters, evaluators, and pre-award listeners run on interruptible virtual workers. Contract award is the single success commit point; `onContractAwarded` listeners are post-commit, best-effort observers and cannot turn committed success into timeout or interruption failure. Adapter-generated negotiation task IDs are now UUIDs rather than millisecond timestamps.

### Fixed

- `QueryExpanderRegistry`'s absent-provider error now names the registered providers, that TnsAI ships no built-in expander, and two paths out: install a provider module or apply the complete configuration remedy for the declared `strategy` + `queryExpansion` pair. The wording skeleton is shared with `RerankerRegistry` so a third unbundled SPI seam inherits it.
- `@Retrieval(strategy = SEMANTIC)` no longer ranks by token overlap without saying so. When a vector-dependent strategy (`SEMANTIC`, `HYBRID`, `HIERARCHICAL`, `TEMPORAL`) runs on the bundled hash embedding because no `EmbeddingFunction` provider is installed, the Role warns once. `KEYWORD`, `GRAPH` and `REASONING` stay silent — they do not depend on vectors. Acknowledge the fallback deliberately with `-Dtnsai.rag.acknowledgeHashEmbedding=true`.
- A mistyped `rerankerModel` or `queryExpansionModel` no longer reaches the first query as a retrieval failure. Both resolve at the start of the retrieval — before any index work and outside the `onFailure` guard — and report `PROVIDER_UNAVAILABLE`, so a `CONTINUE` or `USE_CACHE` policy cannot absorb a deployment fault. Failures a provider raises *while* ranking or expanding still follow `onFailure`. `supports()` must be answerable without a network call.
- `SCOPBridge.executeAction` requires explicit principal, liability sink and authority scope through public `principal` / `liabilitySink` / `authorityScope` (or `getInstance(principal, sink, scope)`). Missing wiring fails before retrieval or the action body, so a `@Sequential` cursor cannot advance and still return an accountability error. There is no silent no-op sink.
- `SCOPBridge.sendToLLM` grounds from an exact-owner `ChatKnowledgeBinding` even when the owner has no `@ChatKnowledge`, matching builder `chatKnowledge` with the annotated SCOP path. The bundled server attaches the session `RagService` corpus through `Agent.setKnowledgeBase` instead of rewriting the user message, so built-in development chat uses the same snapshot binding as annotations.
- Sequential cursors isolate builder-source templates, wait on a cancellation-aware lock so a cancelled waiter cannot consume a unit, and store load-only bindings in a `ClassValue` so disposable Role classloaders are not strongly retained.
- `ContentExtractorRegistry` timeout now cancels the worker `Future` (`cancel(true)`), waits a bounded grace for the provider to release admission, and admits an immediate retry when the worker honours interrupt. A provider that ignores interrupt is quarantined so a stuck extractor cannot occupy ingest admission indefinitely.
- FILE-backed GRAPH retrieval now keys its process registry by the complete canonical source configuration and a lifecycle-unique binding owner. Two live programmatic engines cannot overwrite or lazily resolve each other's graph corpus, and close, invalidation, or a cache-race loser removes only its exact registration. Neo4j persists the lifecycle namespace on nodes and edges, fences every read by it, and deletes only that namespace when its registration retires; an invalidation racing source loading cannot publish the withdrawn snapshot afterward. The per-Role registry uses `ClassValue`, so retired corpora no longer pin disposable Role classloaders.
- Provider-created `RetrievalEngine` instances now give Qdrant and pgvector indexes a source-configuration-sensitive, lifecycle-unique full SHA-256 owner. Two live engines cannot cross-search colliding document IDs, and closing one engine clears only its own remote corpus. Compatible embedding providers share one bounded physical collection/table while backend namespace compatibility now includes the first validated embedding's actual dimension, and backend checks reject incompatible restarts. Lifecycle-owned rows/points carry monotonic renewable expiry leases (minimum one minute), isolated and cancellable virtual-thread heartbeats, bounded remote calls, and a one-lease clock-skew grace window so a slow backend cannot starve unrelated owners or let peers scavenge a live idle engine; pgvector publishes each row and lease atomically. A later process can still scavenge corpus data orphaned by a JVM or pod crash. Failed provider cleanup stops the abandoned engine's heartbeat and is retried only by the private provider runtime; the public `RetrievalEngine` close hook remains an opaque, exactly-once callback. Expiry sweeps are lifecycle-rate- limited rather than query-triggered; pgvector expiry columns and Qdrant owner payloads are indexed, and Qdrant serializes point, lease, and cleanup writes with strong server ordering so an ambiguously timed-out request cannot later regress a newer lease. Owner-wide renewal runs only at the heartbeat cadence; reads stay write-free inside the cadence, while an overdue foreground read or upsert performs one rate-limited catch-up renewal before expiry scavenging so a live corpus cannot delete itself. Each leased upsert publishes its own expiry atomically, avoiding corpus-size write amplification. Qdrant uses server-side conditional filters so a late older write or a clock rollback cannot shorten a newer published lease. A leased first insert now reserves an inactive zero-vector point; content, embeddings, and activation are published only through existing-point operations guarded by a monotonic fence and collision-resistant operation token, so concurrent processes cannot mix content and embeddings and a delayed first activation cannot insert an ID absent at cleanup. Publication revalidates its original lease between stages and uses a bounded all-replica verification retry when tombstone GC removes a reservation concurrently. Leased removals, owner cleanup, and expiry scavenging leave sanitized, vectorless fence records that reject older existing-point writes. The fences are physically reclaimed after a two-lease replay horizon, and a cleared lifecycle owner rejects new mutations, bounding retained records without reopening the stale-write window. PostgreSQL relation names, including unscoped user-supplied bases, are independently UTF-8-byte bounded and hashed. The internal ownership protocol is ASCII-only and versioned, so PostgreSQL `TEXT` never receives reserved NUL delimiters. Role-bound and public literal scopes remain deterministic.

### Migration

**Upgrading rebuilds every RAG index, in every deployment.** This 0.15.0 minor release forces a full re-ingest and re-embed; plan for the ingest cost and the embedding-provider spend before rolling it out.

`CHUNKER_VERSION` goes `2` → `3`, and that field is part of the canonical fingerprint payload for *every* source. So the rebuild is not limited to the corpora whose chunk layout actually moved:

- **Unstructured FILE sources** (`AUTO`/`CHUNK` fixed-size windows) genuinely chunk differently — windows now overlap by 15 lines.
- **`LINE` and `HEADING` units** stay hard-cut; overlap is suppressed.
- **Semantic-boundary corpora** (markdown headings, source-code symbols) also stay hard-cut at those boundaries.

Nothing needs to be done by hand — ingest rebuilds on the next binding. Drop or invalidate the Role binding, or delete the staged index, to force it earlier.

Direct `PgvectorVectorIndex.configured()` users with an unscoped table base longer than PostgreSQL's 63-byte identifier limit must re-ingest once. Older versions relied on PostgreSQL's silent identifier truncation; 0.15.0 derives a stable ASCII-safe hashed relation name instead. Existing ASCII identifiers at or below the limit keep their exact relation name.

The migration is behavioural, so binary compatibility checks can pass while every deployment still pays a full re-index.

#### Contract-net proposal source

`MultiRoundContractNet.builder().build()` without a proposal source now throws `IllegalStateException` naming the missing input. Supply one: `.proposalRequester((contractor, task, round, targetScore, timeout) -> ...)`. `NegotiationExecutor`'s default CONTRACT_NET registration carries no source; `negotiate()` through it fails fast with a reason naming the missing input — register `new ContractNetAdapter(requester)` instead.

Custom requesters, evaluators, and pre-award listeners must not depend on the caller's `ThreadLocal`, MDC, or thread affinity: they now execute on virtual workers and should respond to interruption. Treat `onContractAwarded` as an asynchronous observer of an already committed contract; place transactional award work before calling the protocol or behind an idempotent external sink.

## [0.14.1] - 2026-08-19

Patch so `@Retrieval` on a SCOP Conversation object does what the annotation says. Role consumers are unchanged.

### Fixed
- `@KnowledgeSource` + `@Retrieval` on a non-`Role` target dispatched through `SCOPBridge.executeAction` now run before the method body. `_rag_context` is written, and a blank LOCAL return surfaces the retrieved text as narration. A non-blank method return still wins. No second retrieval engine ([#193](https://github.com/TnsAI-Framework/TnsAI/pull/193)).

## [0.14.0] - 2026-08-18

The release that makes declared runtime behaviour match the code. Previously inert fields now run or fail loud — PII guardrails, tenant context, FeatureFlags, approval tokens, `@LLMSpec` Phase 1, `@VectorMemory` providers. RAG gained optional adapters (Qdrant, pgvector, Neo4j, Docling, Office) and one ingestion path. `@Param` is gone; `UpgradeTnsAI_0_14_0` strips it.

### Fixed
- `BackwardChainingPlanner` no longer returns an empty plan for a goal it could not reach. An empty plan now means one thing only: every goal was already satisfied. Every other outcome raises `PlanningFailureException`, whose `reason()` separates an `UNREACHABLE` goal from a `CYCLIC_PRECONDITION`, an exhausted depth budget, and an exhausted search budget; `goalName()` and `maxDepth()` are always populated. Cycles are detected on repeated target/precondition/state frames rather than action names, so a valid multi-helper continuation is no longer mistaken for a loop. `PlanExecutor.think()` and `getPlan()` surface the failure instead of a silent no-op.
- Tool registry name keys now use one trim + `Locale.ROOT` normalization contract for registration, lookup, removal, and search, preventing default-locale failures such as Turkish `I` → `ı` while preserving original display names and distinct Unicode spellings. Registrations that would silently shadow an existing normalized alias are rejected atomically.
- `@Contract` is now the sole runtime pre/postcondition gate. The STRIPS-style `@ActionSpec.precondition/postcondition` fields remain planner metadata and no longer change dispatch behavior based on whether `tnsai-quality` happens to be present; explicit `@InvariantCheck` still validates `@State` invariants after execution.


- `@LLMSpec` Phase 1 fields `endpoint`, `apiKeyEnv`, `timeoutMs`, `frequencyPenalty`, and `presencePenalty` now reach OpenAI and Ollama clients instead of being silently dropped. Other providers fail loud if those fields are set. `fallbackModel` / `streaming` / `systemPrompt` remain Phase 2.
- `AgentOrchestrator` no longer check-then-acts conversation start outside the manager lock, and no longer double-records START/END snapshots. `ensureConversation()` starts at most once under the lifecycle lock.
- `ServerShellTools` drains stdout and stderr on separate threads before `waitFor`, matching `ServerGitTools`. Commands that emit more than the OS pipe buffer no longer hang until timeout.
- Sandbox stdout/stderr capture is capped by `ResourceLimits.maxOutputBytes` (default 4 MiB). Overflow kills the child and sets `SandboxResult.outputTruncated` so a looping write cannot OOM the host JVM. `ProcessSandbox` and `ContainerSandbox` share the same pump.

### Added

- `@InputGuardrail` validator and sanitizer declarations now execute before action dispatch. Built-in `PiiInputValidator` / `PiiInputSanitizer` can reject or replace common email, phone, IBAN, Luhn-valid payment-card, US SSN, and checksum-valid Turkish identity values without including raw matches in diagnostics. Detection is heuristic rather than jurisdiction-complete. Guardrail regexes now run with bounded pattern/input sizes and reject backreferences or ambiguous quantified constructs before matching. Comments-mode/multi-token escapes and more than one non-fixed, non-possessive backtracking quantifier are outside the bounded subset; multiple alternation groups in one pattern also fail closed.
- `FeatureFlag` SPI (`isEnabled(flag, FlagContext)`) with an env default (`TNSAI_FLAG_<NAME>=on|off|0-100`, optional `_ALLOW` tenant/agent list). Unset new flags are off. `TNSAI_FLAG_GOAP=off` skips planner auto-discovery; unset keeps today's classpath discover. No SaaS vendor.
- `tnsai-server` honours an opt-in `Idempotency-Key` header on POST/PUT/PATCH. The first 2xx is stored in the existing `IdempotencyStore` (in-memory by default; Redis/Postgres adapters reuse the same SPI) and replayed on retry. Same key + different body is 409. In-flight duplicates wait rather than run the handler twice. Missing header is unchanged.
- `DoclingTools` adds optional advanced document parsing through either the official Docling MCP server or a locally installed `docling` CLI. The immutable `DocumentResult` preserves Markdown, tables, formulas, figures, and lossless Docling JSON; `PdfTools.pdfToImage` can use configured Docling page exports when PDFBox cannot open a document.
- `SpiLoader` provides immutable, service-type and thread-context-classloader scoped provider snapshots with explicit service or global invalidation. Core and intelligence SPI discovery paths reuse those weakly retained snapshots without pinning application classloaders across redeploys. Lifecycle-sensitive mutable providers, such as agent-scoped memory stores, continue to receive fresh instances.
- POJO `@Tool` methods can declare `timeoutMs` for an independently enforced invocation deadline. Expiry raises the existing retryable `ToolTimeoutException`; the default keeps current agent-level behavior.

- `SelectiveReembedIndex` keeps a graph of content hashes and re-embeds only dirty chunks. Duplicate payloads share one vector. Changing one source does not re-embed the rest of the corpus. Qdrant/pgvector stay the production `VectorIndex` backends.

- `SmartDocumentSegmenter` splits long papers on headings, keeps figure captions with their section, resolves `Section X.Y` references, and attaches a cumulative first-sentence digest of prior sections. Markdown is read directly; PDF/DOCX use the installed content extractors. This is an analysis API, not a replacement RAG chunker.

### Changed

- MCP transports now cap inbound messages before JSON parsing and no longer log raw request/response bodies, protecting document-bearing Docling calls from response amplification and debug-log disclosure.

- **BREAKING:** The unused coordination `Blackboard`, `KnowledgeSource`, and `KnowledgeEntry` types are removed. Shared working memory is `com.tnsai.communication.SharedBlackboard`. The only public `KnowledgeSource` type is the RAG annotation `com.tnsai.annotations.KnowledgeSource`.

- Chat grounding always uses `ChatKnowledgeBinding` / `RetrievalEngine`. `AgentBuilder.knowledgeBase(...)` and `Agent.setKnowledgeBase(...)` install a snapshot binding (`source=knowledge-base`) instead of a parallel `KnowledgeBase.search` path in `AgentChatOrchestrator`. The builder methods remain; Sona on Central 0.13.0 keeps compiling.

- File ingestion ignore rules are one contract on declarative and Server adapters: nested `.gitignore` / `.tnsignore` apply under their own directory, a leading `/` stays anchored, and a trailing `/` excludes a directory tree rather than a same-named file. Admission caps (`FileIngestionService.Limits`) are the same policy object on `FileIngestionService` and `FileIndexer`.

- **BREAKING:** Explicit `format=PDF` (and the other office formats) with unreadable content now fail as `DocumentProcessingException` at extraction after the office extractor is registered. Callers that caught `IllegalArgumentException` for a missing backend must catch the typed processing exception.

- **BREAKING:** `@VectorMemory(provider = "qdrant"|"pgvector"|"milvus"|…)` no longer silently falls back to TF-IDF when no backend is registered. Agent initialization throws `IllegalStateException` with a migration hint. `provider = "inmemory"` (the default) still works and now searches through a real `VectorMemoryStore` via `VectorStoreProvider`.

### Fixed

- Backward chaining can apply more than one helper for the same target precondition (for example `a && b` via two setters) instead of returning an empty plan.
- Tenant-scoped agents now bind `AgentBuilder.tenantId(...)` to a nested, automatically cleared `TenantContext` for public turns and lifecycle work. `TenantAware` memory stores fail closed when invoked without the configured tenant, while unscoped agents retain their existing behavior.
- Approval tokens now bind the executor principal and action before authorization, and `approve` / `reject` / `consume` share one atomic lifecycle. An expired approved token cannot be consumed, a consumed single-use token cannot be resurrected by a concurrent approve, and both `ActionExecutor` and `InMemoryApprovalTokenStore` require a successful `consume()` before dispatch. Multi-use tokens stay reusable. Authorization failures do not log token-bound identifiers.
- `sql_query` rejects modifying CTEs and `SELECT INTO`, and opens the JDBC session read-only so a write cannot commit even if a dialect accepts a `WITH … DELETE` prefix.
- `@Resilience` timeouts run on a dedicated daemon pool and cancel a real `Future`, so the worker is interrupted instead of continuing on `ForkJoinPool.commonPool()` after the caller already failed.
- `DefaultGroupEventBus.unsubscribeAll(agentId)` now removes `subscribeFor` / `subscribeFrom` registrations for that agent instead of always returning 0.
- `@Contract` JEXL bindings now flatten public record components one level, so `from.balance` works on `transfer(Transfer t)`. A null nested component fails with a field path instead of a raw JEXL/NPE stack.

- `PaymentBroker.noOp()` is the null-object on the SPI, matching `ContextManagerHandle.noOp()` / `FeedbackCollector.noOp()`. Agent init no longer constructs `NoOpPaymentBroker` at call sites.

- `LLMRoleExecutor` throws `ActionExecutionException` when the action context has no `LLMClient`. The previous `return null` was logged as a fallback but `@Fallback` never ran and liability recorded neither success nor failure.

- Role-bound Qdrant and pgvector indexes derive their owner from the binding scope instead of a per-process UUID, so a restart still sees the previous corpus. A `putIfAbsent` race loser no longer `clear()`s the remote store.

- `ContentExtractorRegistry.discover()` and `FileIngestionService` share one `META-INF/services` snapshot. The office extractor is no longer hand-appended beside ServiceLoader, so consumers see the same formats as the ingestion path. Extraction is also wall-clock bounded.

- `ReflectiveNeo4jSession` now iterates Neo4j `Result` as an `Iterator` (the real driver type). Identity keys keep letters outside ASCII so a CJK heading cannot empty-id a node, and Turkish İ/ı forms stay distinct. FILE graph corpora are keyed by binding identity, not Role class alone.

### Added

- `VectorStoreProvider` SPI plus a bundled `inmemory` implementation. Remote names fail loudly until an optional adapter registers itself.

- Package-private, default-disabled controlled RAG write-back prototype. It accepts only allow-listed user-confirmed, tool-verified, or eval-gated evidence; isolates mutable entries by the complete `RetrievalScope`; carries source-turn, agent, confidence, expiry, and supersession provenance; and proves next-query keyword retrieval without changing the public core API, shared retrieval cache, or default provider behavior.

- `InMemoryGraphStore` plus an optional `Neo4jGraphStoreProvider`. FILE `@KnowledgeSource` Roles get a zero-config in-process graph with chunk↔entity provenance and identity collapsing (`Auth Service` / `the auth service`). Neo4j opens only when `tnsai.graph.neo4j.uri` is set and `org.neo4j.driver` is on the runtime classpath — it is not bundled into `tnsai-core` and is not a required intelligence dependency. `KnowledgeTools` triples remain a separate default and no longer claim FILE Roles.

- Opt-in multilingual embedding bench in `tnsai-evaluation`: 50 TR + 50 EN same-fact pairs and 10 cross-lingual probes, scored with recall@5, MRR, p50 embed latency and bytes/doc. Default `mvn test` stays offline; `mvn -pl tnsai-evaluation -Pembedding-bench test` remeasures through local Ollama. Measured 2026-08-15 (see `tnsai-evaluation/src/main/resources/embedding-bench/RESULTS.md`):

  | model | dim | TR R@5 | EN R@5 | CROSS R@5 | bytes/doc |
  | --- | ---: | ---: | ---: | ---: | ---: |
  | token-bag-384 | 384 | 0.94 | 0.94 | 0.20 | 1536 |
  | me5-small | 384 | 0.40 | 0.68 | 0.30 | 1536 |
  | **bge-m3@384** | 384 | **1.00** | **1.00** | 0.90 | 1536 |
  | bge-m3 | 1024 | 1.00 | 1.00 | **1.00** | 4096 |

  Installable default: **bge-m3@384**. Quality override: native **bge-m3** (CROSS 1.00). Loser: **me5-small** — lost to the token bag on TR (0.40 vs 0.94) and EN (0.68 vs 0.94); keep only as a size experiment, not for Turkish retrieval. The production hash default is not changed.

- Optional PDF, DOCX, XLSX, PPTX and EPUB extractors register through the existing `ContentExtractor` SPI. PDFBox and Apache POI stay `<optional>true</optional>` on `tnsai-intelligence`; EPUB uses the JDK zip API plus jsoup. Missing backends keep the typed `UNSUPPORTED_FORMAT` error on both single-file and directory paths.

- `HybridRAGStrategy` RRF-fuses a third knowledge-graph stream when a `GraphStore` is available. `@Retrieval(strategy = HYBRID)` stays the name; a missing store keeps the BM25+vector fuse and records `HybridGraphStream.SKIPPED_NO_GRAPH_STORE` instead of silently becoming VECTOR-only. Hits carry `retrievalStreams` (`bm25` / `vector` / `graph`) plus existing GraphRAG provenance.

- Optional Qdrant vector backend. `VectorMemoryStore` accepts a `VectorIndex`; `QdrantVectorMemoryStore` talks REST through `QdrantTransport` when `tnsai.vector.qdrant.url` (or `TNSAI_VECTOR_QDRANT_URL`) is set. Role bindings namespace the collection per Role and source set and tag points with a scope-derived owner, so a restart still sees the previous corpus. `clear()` deletes those points instead of dropping the collection. Transport failures on `size()` and search surface as errors rather than an empty index. The official Qdrant client is not a `tnsai-core` or required `tnsai-intelligence` dependency, and no new parent-POM module is added.

- Optional pgvector backend on the same `VectorIndex` seam. `PgvectorVectorMemoryStore` talks JDBC through `PgvectorTransport` when `tnsai.vector.pgvector.url` (or `TNSAI_VECTOR_PGVECTOR_URL`) is set. HNSW and IVFFlat are selectable; the first upsert creates the extension + scoped table. The owner is derived from the Role scope so a restart still sees the previous corpus. `clear()` deletes those rows, not the table. Qdrant and pgvector cannot both be configured in one process. The PostgreSQL driver is not a compile or required intelligence dependency, and no new parent-POM module is added.

- `EmbeddingFunctions.matryoshka` derives an L2-normalized prefix from any core `EmbeddingFunction`, allowing Matryoshka-compatible models to build smaller in-memory indexes without a second provider. `VectorMemoryStore` pins its first admitted dimension and rejects mixed add/query vectors with `EmbeddingDimensionMismatchException` before mutation or search.

- **BREAKING:** `@Retrieval` and `RetrievalConfig` gain `REASONING` and `navigatorModel`. Annotation users can select the already-shipped `ReasoningRAGStrategy`; a blank model or an identifier no `TreeNavigator` serves fails at strategy selection, above `@Retrieval.onFailure`. `RetrievalConfig` grows one record component (builder default remains blank). Consumer `switch` statements over `Retrieval.Strategy` stop being exhaustive (ADR-017 A).

- File ingestion now stages a complete generation before mutating TF-IDF or BM25. A source-level format or extraction failure discards the staged replacement, so file N cannot publish chunks from files 1..N-1 of the new attempt. Incremental identity is a pipeline fingerprint — content hash plus declared/detected format, extractor id/version, chunker contract, and include/exclude selectors — so a parser or selector upgrade reindexes even when file bytes are unchanged. `IngestionResult` gains `fingerprints`; the previous eight-argument form is gone. `KnowledgeBase.replaceDocuments` is the atomic swap; `InMemoryKnowledgeBase` and `BM25Stream` hold their locks across the generation.

- `@AgentSpec.knowledge()` and `@AgentSpec.retrieval()` are the annotation counterparts of `AgentBuilder.addKnowledgeSource` / `retrieval()`. Empty `knowledge` and an all-default `@Retrieval` are no-ops; builder-explicit values still win. Duplicate source names fail initialization.

- **BREAKING:** `@KnowledgeSource` and `KnowledgeSourceConfig` gain `include` and `exclude` globs so a mixed directory can declare a strict format without moving files. Empty include admits every regular non-symlink file; empty exclude admits every include match; exclude always wins. Selectors run before extension/content validation, so an excluded mismatch cannot fail binding. The canonical record is now nine components; the seven-argument constructor remains and selects empty selectors. Invalid `*`, `**`, `?` syntax fails at construction with the source name and the offending selector.

- `KnowledgeToolsGraphStoreProvider` — when `tnsai-tools` is on the classpath, `@Retrieval(strategy = GRAPH)` reads live `KnowledgeTools` triples. Each tool instance still keeps its own bag; GraphRAG unions every live instance on retrieve, so `kg_add_triple` is visible without a second graph or a tools↔intelligence Maven edge. Without the tools artifact the provider stays silent and GRAPH remains a missing-capability error.

- `ReasoningRAGStrategy` — vectorless retrieval that descends a document tree with a model choosing the branch, for corpora where the passage that answers a question shares no vocabulary with it. It is the opposite walk from `HIERARCHICAL`, which takes similarity hits and expands upward through deterministic parent edges; both read the same `HierarchyIndex`, only the direction and the selector differ. The navigator sees titles and summaries, never content, because a traversal costs one model call per level.

  Every bound belongs to the runtime rather than the provider: `maxNodesVisited` caps the descent across all roots, `maxDepth` caps any single path, and an id the index does not have ends that path. None of these fail the request — each lands on the last valid node and records why in `hierarchyStopReason`, because a partial answer from a real subtree beats failing over one bad step. `RAGContext.metadataFilters` and `maxResults` are applied; there is no score model, so landed nodes carry a constant score and `minScore` admits them all. Selecting it through `@Retrieval` comes with the enum value, which is a separate change.

- `TreeNavigator` SPI — the contract a provider implements to drive vectorless, model-guided descent of a document tree, for corpora where the passage that answers a question shares no vocabulary with it. A navigator is handed the node it is standing on and that node's children as titles and summaries, and answers with the branch to open or with a stop; it never sees full content, because a traversal costs one model call per level. Resolution follows `QueryExpander` and `Reranker` exactly: providers are discovered through `ServiceLoader`, exactly one must claim a model, and both none and several fail loudly rather than degrading silently or depending on classpath order. TnsAI bundles no provider — inference, credentials, transport and the cost of a traversal stay in the provider module. The strategy that consumes this lands next; design and slicing are in `tasks/specs/2026-08-12-reasoning-rag/spec.md`.

- Core RAG now defines a dependency-light `ContentExtractor` SPI with immutable byte input, normalized `ExtractedDocument`/structural block provenance, typed document-processing failures, extractor versions for index fingerprints, and deterministic ServiceLoader discovery. Missing providers, invalid declarations, and duplicate format claims fail with source-aware diagnostics; parser libraries remain in optional provider modules.

- File-backed knowledge sources now pass bundled textual formats through the common extractor SPI before indexing. Markdown, HTML, CSV/TSV, JSON/JSONL, YAML and XML produce deterministic structural blocks with bounded provenance; malformed explicit formats fail closed, HTML active or hidden content is excluded, YAML aliases are denied, and XML parsing has no DTD, entity or network access. TEXT retains bounded UTF-8 content.

- SOURCE_CODE ingestion now detects its concrete language and emits stable symbol blocks through the canonical extractor/chunker pipeline. Java uses the JDK compiler parser for nested types, methods, signatures, imports and calls; TypeScript/JavaScript, Kotlin, Python, Go and Rust use a bounded structural fallback whose use and reason remain visible in chunk metadata. Declarative and Server indexing therefore share symbol IDs, content, language and line provenance without a new parser dependency.

- File-backed RAG ingestion now keeps small documents intact and splits large normalized documents into stable retrieval chunks. Extractor block paths and line metadata survive indexing, so `topK` and `contextWindow` select relevant sections instead of whole files. The server file indexer uses the same core language/heading/fallback implementation, while the existing public `CodeChunker` API delegates to that canonical implementation.

- **BREAKING:** `@Retrieval.queryParam` names the action parameter that carries the retrieval query, and `RetrievalConfig` gains the matching component and builder setter. Blank — the default — keeps the existing rule, so **no annotation, builder or runtime behaviour changes**: retrieval still binds to the first `String` parameter in declaration order. That rule is right for a single-parameter action and positional for any other, so `answer(String tenantId, String question)` retrieved on the tenant id, and reordering the two silently moved the binding without failing, logging, or looking any different in the answer. Naming a parameter the action does not declare now fails at dispatch, above the `onFailure` boundary, so a typo cannot be absorbed into `CONTINUE` and answered ungrounded; a declared parameter carrying no usable value skips retrieval as before. Actions with more than one `String` parameter that do not set it warn once, naming the parameter retrieval actually chose. Declared on a type, the binding applies to every action the Role dispatches, so each must declare that parameter.

  The break is confined to `new RetrievalConfig(...)`: the canonical constructor takes a twentieth component, `queryParam`, positioned after `sources` to mirror the annotation. Callers using `RetrievalConfig.from(...)`, `RetrievalConfig.builder()` or the annotation are unaffected. This is the same shape as `KnowledgeSourceConfig` gaining `format` earlier in this release. `CanonicalRagConfigTest` enforces one component per annotation element, so the alternative — carrying the binding outside the model — would have left the builder path silently unable to express it.

- `SCOPBridge` now offers an additive owner-aware chat dispatch overload that validates `@ChatKnowledge` against its named `@KnowledgeSource`, carries a caller-supplied scoped live binding and immutable retrieval context, and injects canonical fenced evidence into an outbound-only conversation copy. `ChatKnowledgeBinding` adds exact-owner `create`/`live` overloads that keep a weak identity reference, preventing equal owner names or identifiers from authorizing cross-owner evidence. The existing three-argument dispatch path remains unchanged.

- Declarative RAG now discovers one application-provided `EmbeddingFunction` through `META-INF/services` and uses it consistently for SEMANTIC/HYBRID indexes and embedding-based deduplication. With no installed provider, the deterministic 128-slot hash embedding remains byte-for-byte the default; ambiguous provider sets fail during binding construction. Provider initialization and calls are fail-fast, malformed or oversized vectors fail above retrieval fallback policies, and provider-owned arrays are defensively copied.

- **BREAKING:** file-backed knowledge sources now separate location from content through the canonical `DocumentFormat` contract. `@KnowledgeSource`, `KnowledgeSourceConfig`, and its builder default to `AUTO`; explicit textual formats validate registered extension aliases before any source file is read, while `TEXT` intentionally accepts any strictly valid UTF-8 file name. Directory sources apply the same contract recursively, reject symlink source paths, and report unsupported AUTO candidates instead of decoding binary files as text.

### Changed
- **BREAKING:** Declarative and server RAG now ingest local files through one public `FileIngestionService`. Single files and directories share discovery, selector and ignore rules, bounded content detection, structural extraction, stable chunk IDs, flattened provenance, and source hashing. The server keeps its incremental vector/BM25 adapter. Per-file read failures log and skip: previously committed chunks for that path stay, readable siblings still commit, and stale files are reconciled against the discovered snapshot — not aborted as an all-or-nothing `IOException`. Declarative `ingest()` still returns no chunks when the scan is incomplete. Filesystem ignore files use the portable `*`, `**`, and `?` selector grammar; unsupported syntax and admission-limit overflow fail the walk instead of silently weakening exclusions. Common credential files and default skip-directories are excluded. The server no longer maintains a second extension list, walker, reader, or chunking path.

- File-backed `AUTO` sources now inspect a bounded content prefix before using an extension hint, so extensionless JSON and mislabeled textual documents are classified by content while binary candidates remain excluded. Explicit textual declarations retain strict alias validation and now fail with a typed `KnowledgeSourceFormatException` when content contradicts the declaration; XML validation disables DTD and external-entity access.

- Action-level retrieval routes through the unified `RetrievalEngine` instead of a second, parallel pipeline. Scoped cancellation and deadline context are preserved across direct, tool-loop, streaming, event and MCP dispatch, and action evidence is fenced as untrusted context on every path. Public usage is unchanged; the duplicate implementation and its divergence from the engine's failure taxonomy are gone.

### Removed

- **BREAKING:** `@com.tnsai.annotations.Param` is deleted. It never did anything. Nothing in the framework read `Param.class`, no annotation nested a `Param[]` member, and `@ToolSpec` — the usage its own Javadoc documented it against — had already been removed. `ActionDiscovery.discoverParameters` builds every `ParamSpec` from `Parameter.getName()` under the `-parameters` compiler flag, ignoring annotations entirely, so an action's LLM-visible parameter name has always been the Java parameter name.

  This made it worse than dead weight: a mismatch between the annotation and the signature silently resolved to the signature. The tutorial for `@WebService` carried `@Param(name = "q") String query` — readers were told the model would see `q` when it always saw `query`.

### Fixed
- `@Contract` on a method without `@ActionSpec` now fails action discovery with the method FQN instead of being skipped as a helper, so the clause cannot silently never run.
- `api-compat-check.sh` no longer reports success when japicmp fails. `$?` after `if ! mvn` was the negation (0), so the release gate printed "build passed" on a `BUILD FAILURE`.
- `LocalFileSourceLoader` now uses `ContentExtractorRegistry.discover()` instead of a hard-coded bundled extractor. A consumer `ContentExtractor` wins the formats it claims; `StructuredTextContentExtractor` is a framework default that covers the rest.

- japicmp no longer compares the build to itself. The quality-profile baseline is the pinned last Central release, fetched from `repo1.maven.org` into `target/japicmp-baseline/` so `mvn install` cannot shadow it, and `api-compat-check.sh` fails when old and new jars have the same digest.
- `InMemoryKnowledgeBase.search` no longer serializes concurrent queries on the mutation monitor. Mutations take a write lock; search, embedding search, `getDocument` and `size` take a read lock, so readers overlap while incremental DF stays consistent.

- The bundled `KnowledgeToolsGraphStoreProvider` no longer makes a consumer `GraphStoreProvider` ambiguous. It is a framework default: when any other adapter also claims the Role, the default is dropped and the consumer wins. Zero-config GRAPH still works when the built-in is the only claimant.

- Workspace ingest no longer rebuilds every TF-IDF vector and BM25 `avgdl` on each chunk. `InMemoryKnowledgeBase` keeps incremental document frequencies and applies IDF at query time; `BM25Stream` maintains a running token total. `FileIndexer` adds a file's chunks in one `addDocuments` call.

- `LLMRoleExecutor` now reads `_rag_stale_fallback` and `_rag_context_truncated`. Stale `USE_CACHE` documents are still spliced but labelled as expired, so they cannot be read as fresh grounding. A `contextWindow` that drops every matched document fails with `VALIDATION` instead of answering as if the corpus were empty.

- Bundled-server `AgentFactory` no longer silently substitutes the default LLM when `AgentAdd` names a provider other than `ollama`. Unknown providers now fail with `AGENT_ADD_FAILED` (matching `TnsServerMain`), and a successful `AgentState` reports the live client rather than the requested name.

- `@Retrieval.metadataFilters` is now applied by the memory-backed strategies. `VectorRAGStrategy`, `KeywordRAGStrategy` and `HybridRAGStrategy` converted results with a score threshold and never consulted the filters at all, so a declared filter was accepted and silently ignored. Filtering now runs before the `maxResults` cut, and the candidate pool widens when filters are present so a filtered query still returns up to `maxResults` documents instead of fewer.

- Coalesced retrieval no longer lets one caller's cancellation or deadline affect the others sharing the same in-flight work. Waiter cancellation is event-driven with identity-safe callbacks, orphaned loaders are retired, and detached-flight admission and shutdown drain are bounded.
- HYBRID retrieval now normalizes Reciprocal Rank Fusion scores to `0.0..1.0` over the non-empty keyword/vector streams before applying `@Retrieval.minScore`. The default `minScore = 0.5` therefore retains a relevant result instead of rejecting every raw RRF score; an empty stream is excluded from the normalization ceiling and agreement across both streams still ranks above an equivalent single-stream hit.

### Migration

#### Knowledge source include/exclude

`KnowledgeSourceConfig` is now a nine-component public record. Record patterns must use: `name`, `type`, `format`, `path`, `connection`, `query`, `enabled`, `include`, `exclude`. Existing seven- and six-argument constructor calls remain valid and select empty selectors, as do annotations and builders that omit them. Callers using `from(...)` or `builder()` are unaffected.

#### File-source document formats

`KnowledgeSourceConfig` is now a seven-component public record. Record patterns must use the new component order: `name`, `type`, `format`, `path`, `connection`, `query`, `enabled`. Existing six-argument constructor calls remain valid and select `AUTO`, as do annotations and builders that omit `format`.

AUTO now admits every registered bundled textual alias, including Markdown, JSON, XML, YAML, CSV/TSV, HTML, JSONL, and source-code extensions. Reindex a FILE source if it previously contained one of those files: the expanded corpus changes its document fingerprint and therefore its retrieval output. Sources containing only the previously recognized text files do not require reindexing. Full format support and strictness documentation remains follow-up work.

#### HYBRID score thresholds

Explicit HYBRID `minScore` values now use normalized RRF relevance rather than the raw `sum(1 / (k + rank))` scale (about `0.016` per rank-one stream with the default `k = 60`). Keep `0.0` to preserve every candidate; use the ordinary `0.0..1.0` range for a monotonic relevance floor. Direct callers of `ReciprocalRankFusion.fuse()` continue to receive raw standard RRF scores.

#### Removed `@Param`

Delete every `@Param` usage; parameter names and types continue to come from the method signature, so nothing else changes. `-parameters` is already set by the parent POM. Mechanically:

```sh
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=io.github.tansuasici:tnsai-rewrite:0.14.0 \
  -Drewrite.activeRecipes=io.github.tansuasici.rewrite.UpgradeTnsAI_0_14_0
```

`@ToolParam` and `@LLMParam` are different, live annotations and are untouched.

### Stats

130 annotation usages removed across 15 files, plus 16 in the documentation tutorials. 12,062 tests green.

## [0.13.0] - 2026-08-01

The release that makes declarative RAG mean what it says. Most `@Retrieval` members were accepted and then silently dropped; they are now enforced, which is why this release carries 21 breaking changes rather than a handful — the declaration did nothing before and does something now. `@KnowledgeSource` was narrowed to ingestion in the same pass, so the two annotations no longer declare the same knobs with no rule for which wins. Seventeen further fixes cover agent memory and retrieval delivery. The new `tnsai-rewrite` module ships `UpgradeTnsAI_0_13_0` so the mechanical half of the migration runs with `mvn rewrite:run` instead of by hand — the same upgrade mechanism Spring Boot and Quarkus provide — and `tnsai-quality` finishes its package consolidation.

### Added
- `tnsai-rewrite`: **`UpgradeTnsAI_0_13_0` recipe** — migrates consumer code across this release's 61 type moves with `mvn rewrite:run`, so the three package consolidations below cost an import review rather than a manual sweep: the agent-group contracts `com.tnsai.coordination.groups.*` → `com.tnsai.agents.groups.*`, the quality-owned `com.tnsai.{observability,security,validation.parallel}.*` → `com.tnsai.quality.*`, and the duplicate `evaluation.evaluators.agentic.*` → the canonical `evaluators.agent.*` set. `UpgradeTnsAI_0_12_0` is untouched and both can be activated together, oldest first. The recipe lists **types, not packages**, deliberately: none of the three trees moved wholesale. `com.tnsai.security.audit` is split across two modules — `FileAuditStore` and `InMemoryAuditStore` moved while `AuditEvent`, `AuditQuery` and `AuditStore` stayed — and `com.tnsai.security`, `com.tnsai.observability` and `com.tnsai.coordination.groups` each kept members too. A `ChangePackage` over any of those prefixes would rewrite references to types that never moved, turning a compile error into a silently wrong import; tests pin each survivor. Two parts of this release stay manual: the deleted `com.tnsai.security.sandbox` pair has no replacement to rewrite to, and the `@Retrieval` members that stopped being inert change behaviour without changing any import — auto-inserting an opt-out like `cache = false` would silently pin consumers to the old behaviour, so each CHANGELOG entry names the opt-out and leaves the choice with the reader.
- `tnsai-core` / `tnsai-intelligence`: completed RAG annotation-builder parity. Action-level retrieval now has immutable `KnowledgeSourceConfig` / `RetrievalConfig` builder APIs, while chat-level retrieval supports `@ChatKnowledge` over a named `@KnowledgeSource`. Both paths preserve the existing `RetrievalSpi` invocation and `KnowledgeBase` contracts. A builder declaration and the equivalent annotation converge on one `RetrievalConfig` before validation, provider resolution, or cache lookup, so the two spellings retrieve identically and share one `RetrievalResultCache` entry.
- `@Retrieval.topK` is validated before retrieval runs, alongside the existing reranking, caching, failure-policy, expansion, deduplication, and context-assembly checks. A non-positive `topK` retrieves nothing while the action still reports success — the outcome the failure policy exists to prevent — so it is now a configuration error on both paths.
- `tnsai-rewrite`: OpenRewrite recipe module. `UpgradeTnsAI_0_12_0` migrates the 0.12.0 breaking renames (`com.tnsai.identity.AgentSpec` record → `AgentDescriptor`; `@com.tnsai.roles.annotations.RoleIdentity` annotation → `@RoleDeclaration`), run via the `rewrite-maven-plugin`. See `tnsai-rewrite/README.md` for the invocation.
- `tnsai-llm`: **task-aware embeddings** — new `EmbeddingTask` enum and `EmbeddingProvider.embed(text, task)` / `embedBatch(texts, task)` default overloads. `OllamaEmbeddingProvider` prepends the nomic task-instruction prefix (`search_query:` / `search_document:` / `clustering:` / `classification:`); non-nomic models pass through unprefixed so a prefix is never applied to a model not trained on it.
- `tnsai-intelligence`: **query-aware reranking** — new `Reranker` SPI (`supports(model)` / `rerank(Request)`, discovered via `ServiceLoader`) wired into the `@Retrieval(rerank, rerankerModel, topN)` path by `DefaultRetrievalSpi`. The runtime validates the provider's output (no documents outside the candidate set, no nulls) and applies `topN` after it returns; a provider that drops every candidate is logged at WARN so it is distinguishable from "retrieval matched nothing". `Reranker.noOp()` supplies the null-object identity provider. Implementations must be thread-safe — one instance is shared by every agent thread for the JVM's lifetime. TnsAI deliberately ships no cross-encoder: provider modules own model inference, credentials, and transport.
- `tnsai-intelligence`: **retrieval result cache** — `@Retrieval(cache, cacheTTL)` now memoises fully processed retrieval results in a bounded per-Role cache (256 entries, access-ordered LRU, per-entry TTL). Concurrent misses for one key coalesce onto a single load, so a cold key cannot stampede the corpus; documents are defensively copied on the way in and out, so a caller mutating its result cannot poison the entry. Cache keys hold a SHA-256 digest of the whitespace-normalised query rather than the query text, and no retrieved content reaches keys or telemetry. Results are cached **post-rerank**, which is sound because the key covers every input that changes the document list — including `rerank`, `rerankerModel` and `topN` alongside the Role, sources, strategy, `topK` and `minScore`.
- `tnsai-core`: **retrieval provenance in the action context** — new `RetrievalSpi.RETRIEVED_STALE_FALLBACK_KEY` (`_rag_stale_fallback`, Boolean), written by the same path that writes `_rag_context`: `false` for fresh grounding, `true` when `@Retrieval.onFailure = USE_CACHE` served an expired cache entry, absent when no context was injected at all. Without it a stale fallback is indistinguishable from a fresh retrieval — both leave a populated `_rag_context` and a positive `_rag_document_count`, which `LLMRoleExecutor`'s splice guard reads as "fully grounded" — so executors, interceptors, and evaluators had no way to surface or refuse degraded grounding. `ActionExecutor` scrubs the key from a caller-reused context alongside the other three reserved keys.
- `tnsai-intelligence`: **model-backed query expansion** — new `QueryExpander` SPI (`supports(mode, model)` / `expand(Request)`, discovered via `ServiceLoader`) and `MultiQueryRAGStrategy`, which retrieves every unique variation through the same source-scoped base strategy and fuses the results with reciprocal-rank fusion. Fusion controls *ordering only*: each returned document keeps the strongest original strategy score, so semantic, keyword, and hybrid score contracts are not replaced by an unrelated fusion scale, and the fusion score plus the zero-based indexes of the queries that found the document are attached as metadata (`retrieval.fusionScore`, `retrieval.queryIndexes`). The original query is always retained as index `0` and never counts against the variation budget; provider output is normalised, deduplicated case-insensitively, and truncated to `expandedQueries` as a hard bound, so a provider cannot widen the prompt beyond what the annotation declares. Expansion runs *inside* the result cache's loader, so a cache hit costs no expansion round-trips. Implementations must be thread-safe — one instance is shared by every agent thread for the JVM's lifetime. As with reranking, TnsAI deliberately ships no expander: provider modules own model inference, credentials, and transport.
- `tnsai-core`: **context completeness in the action context** — new `RetrievalSpi.RETRIEVED_CONTEXT_TRUNCATED_KEY` (`_rag_context_truncated`, Boolean), written by the same path that writes `_rag_context`: `false` when every retrieved document reached the context in full, `true` when the `contextWindow` forced one to be shortened or dropped, absent when no context was injected at all. Assembly walks the score-ordered list and stops at the first document that will not fit, so a single top-ranked document larger than the whole window yields an empty context and `_rag_document_count = 0` — which `LLMRoleExecutor`'s splice guard reads as "retrieval matched nothing" while the model answers ungrounded. The new key separates the two: count `0` with the flag `false` is an empty corpus, count `0` with the flag `true` is a `contextWindow` too small to hold anything, and that case additionally logs at WARN. `ActionExecutor` scrubs the key from a caller-reused context alongside the other three reserved keys.
- `tnsai-intelligence`: **graph retrieval SPI** — new `com.tnsai.intelligence.rag.graph` package with the `GraphStoreProvider` SPI (`name()` / `open(Request)`, discovered via `ServiceLoader`), the vendor-neutral `GraphStore` data contract (`findSeeds` / `neighbors`, plus the `Node`, `Edge`, `Seed` and `Neighbor` records), and `GraphCapabilityException`. Adapters supply seed and adjacency data only; the framework owns traversal depth, cycle handling, path scoring, ranking, source fencing, and provenance, so two adapters over the same graph rank identically. A provider returns `Optional.empty()` for Roles it is not configured for, which is how one classpath hosts several graph backends. Implementations must be thread-safe — one instance is shared by every agent thread for the JVM's lifetime. As with reranking and query expansion, TnsAI deliberately ships no graph backend: provider modules own the database, credentials, and transport.
- `tnsai-intelligence`: **hierarchical retrieval** — new `com.tnsai.intelligence.rag.hierarchy` package with the `HierarchyMetadata` key vocabulary (`hierarchyId` / `hierarchyParentId` / `hierarchySource` declared by loaders, and `hierarchyHitId` / `hierarchyDepth` / `hierarchyPath` / `hierarchyFlat` / `hierarchyTruncated` / `hierarchyStopReason` emitted as provenance), the `HierarchyIndex` document index with its `validate()` configuration gate, and `HierarchyException`. The `SourceLoader` SPI gains a `metadata(source, document)` **default** method — the hook a loader implements to declare parent relationships — so a loader that does not override it simply describes a flat hierarchy. (A loader must still be updated for the `SourceLoader` parameter change below; the `metadata` hook itself is what stays optional.) `HierarchicalRAGStrategy` uses semantic hits only as selectors and then expands parents deterministically: bounded to three edges by default, decaying relevance by 0.85 per level, collapsing shared ancestors onto their best path through a stable tie-break, and never crossing the `@Retrieval.sources` fence — so two deployments over the same documents rank identically.
- `tnsai-intelligence`: **temporal retrieval** — new `com.tnsai.intelligence.rag.temporal` package with the `TemporalMetadata` key vocabulary (`temporalTimestamp` declared by loaders and `temporalSource` assigned by the binding, plus `temporalContentScore` / `temporalDecay` / `temporalScoreFactor` / `temporalAgeSeconds` / `temporalDated` / `temporalStatus` / `temporalFutureSkewSeconds` emitted as provenance), the `TemporalIndex` document index with its `validate()` configuration gate, and `TemporalException`. `TemporalRAGStrategy` uses semantic hits only as selectors and then re-ranks them deterministically: exponential decay over a 30-day half-life bounded by a 0.8 relevance floor, undated documents held at that floor, future timestamps within five minutes clamped to the retrieval instant and marked in provenance, a stable content-score-then-ID tie-break, and no crossing of the `@Retrieval.sources` fence — so two deployments over the same documents rank identically. The `Clock` is injectable, so the ranking is reproducible in tests.
- `tnsai-core`: **unified retrieval engine contract** — new public types in `com.tnsai.rag` giving chat and action retrieval one shared shape: `RetrievalEngine` (a final, agent-scoped `AutoCloseable` wrapper that enforces the invariants and delegates to a `Retriever` backend), the `RetrievalEngineProvider` SPI discovered via `ServiceLoader`, `RetrievalRequest` with its builder, `RetrievalScope`, `RetrievalResult`, `RetrievalEvidence` (with nested `Provenance`), `RetrievalDiagnostics`, and `RetrievalException`. The runtime implementing the contract ships in this same release (below); no existing declaration changes behaviour, because chat and action entry points keep using their own pipeline until later items in this release migrate them. Three boundaries are stated on the contract itself rather than left for each implementation to guess:
  - **Windowing is the caller's.** The engine neither validates nor applies `@Retrieval.contextWindow`, because it returns `RetrievalEvidence` rather than assembled prompt text and so has nothing to bound; evidence volume is governed by `topK`, `topN`, and `minScore`. Whichever entry point renders evidence into a prompt enforces the window and reports truncation through its own channel. Left unsaid, `contextWindow` would be a hard configuration error on the annotation path and a silently ignored field on the engine path.
  - **Failure classification is fixed, not per-caller.** `RetrievalException.Reason` now carries `surfacesAboveFailurePolicy()`: `CONFIGURATION`, `PROVIDER_UNAVAILABLE`, and `ENGINE_CLOSED` are declaration or lifecycle faults that must reach the caller, while `TIMEOUT` and `EXECUTION` are transport faults `@Retrieval.onFailure` may absorb. Because declaration and transport failures share one exception class, callers branch on the predicate rather than on the type. Cancellation stays outside the taxonomy — it is caller intent, propagates unconditionally, and is never absorbed.
  - **Cache keys must cover scope and filters.** `RetrievalScope` gives absence exactly one encoding (a present-but-blank identity is rejected, not normalized to empty), so a digest cannot confuse "no tenant" with "the empty tenant". Filter keys and values are arbitrary non-blank text and may contain any separator, so a key length-prefixes each part instead of joining on a delimiter; and because `metadataFilters` is an unordered immutable map whose iteration order varies between JVM runs, entries are sorted before digesting.
- `tnsai-intelligence`: **unified retrieval runtime** — `DefaultRetrievalEngineProvider` implements the `RetrievalEngineProvider` SPI and is registered through `META-INF/services`, so `RetrievalEngineProvider.discover()` now returns a working engine instead of nothing. Every `create(sources)` ingests a fresh canonical source set and hands back an engine owning its own indexes and result cache; no mutable retrieval state is shared between owners, which is what lets the engine path key its cache by a constant owner component. The runtime drives the existing expansion → fusion → deduplication → rerank composition behind the core contract and returns `RetrievalEvidence` with full provenance rather than assembled prompt text. Three of the contract's boundaries are enforced here rather than merely restated:
  - Every failure is classified into a `RetrievalException.Reason` **first** and only then routed on `surfacesAboveFailurePolicy()`, so a declaration fault cannot reach `@Retrieval.onFailure` — `CONTINUE` would report success on an ungrounded answer, and `USE_CACHE` would answer from stale evidence while the declaration stays unhonourable. Branching on the exception type instead would route both classes the same way, since both are `RetrievalException`. Classification happens at the phase that can tell them apart: strategy *selection* raises declaration faults — an unknown source name, a malformed hierarchy or temporal index, an unresolvable graph provider — while the very same exception types raised later, traversing an index that already validated (a cycle reached at depth, a document dated beyond `maxFutureSkew`), are clock- and request-dependent, stay transport-class, and do reach the policy. Cancellation is checked before the predicate and propagates unconditionally.
  - `contextWindow` is neither validated nor applied, matching the package contract: the engine renders no prompt text, so it has no window to enforce.
  - Cache keys cover the request's complete `RetrievalScope` and metadata filter set, sorted and length-prefixed before digesting, so neither JVM-to-JVM iteration order nor a separator inside filter text can move an entry between owners. `onFailure=RETRY_SIMPLE` caps its retry at `topN` whenever the primary pipeline was reranked: the retry deliberately runs without reranking, and reranking is the only place `topN` is otherwise applied, so an uncapped retry would return *more* evidence than the healthy path at the moment relevance ordering is at its worst. Chat and action entry points still run their own `DefaultRetrievalSpi` pipeline — migrating them onto this engine is later work in this release, for action and for chat.

### Changed
- **BREAKING:** `@KnowledgeSource` now declares only what it can deliver: ingestion. Eleven elements are gone — `provider`, `index`, `topK`, `minSimilarity`, `embeddingModel`, `dimensions`, `namespace`, `filter`, `cache`, `cacheTTL`, `priority` — leaving `name`, `type`, `path`, `connection`, `query` and `enabled`. `KnowledgeSourceConfig` (added this release, never published) narrows with it, from 17 record components and 15 builder setters to 6 and 4. `UpgradeTnsAI_0_13_0` strips the removed attributes from your declarations. **Removing them preserves behaviour rather than changing it.** Not one was ever read: `git grep` finds no reader for any of the eleven, on either the annotation or the config. They read as configuration and were discarded, so `@KnowledgeSource(topK = 20, minSimilarity = 0.85)` compiled, looked deliberate, and did nothing. The split is structural, not a cleanup. A `SourceLoader` runs **once**, when a Role's binding is built, and returns documents the framework then indexes. A per-request retrieval limit has no moment at which such a loader could apply it, so `topK`, `minSimilarity`, `cache`, `cacheTTL` and `priority` could never have worked here — and `@Retrieval` already owns every one of them, per action, where the request exists. The remaining six described a remote index (`provider`, `index`, `dimensions`, `embeddingModel`, `namespace`, `filter`), which is the case below.
- **BREAKING:** `@Retrieval` and `@KnowledgeSource` no longer declare the same knobs. `topK`, `cache`, `cacheTTL` and the score threshold existed on both with no rule for which won — and the thresholds even disagreed on their default, `@Retrieval.minScore` 0.5 against `@KnowledgeSource.minSimilarity` 0.7. `@Retrieval` is now the single authority for all four. Set them there; the values are the ones that were already in effect, so no retrieval changes behaviour.
- **BREAKING:** `@KnowledgeSource.type` defaults to `FILE` instead of `VECTOR_DB`. The old default named a type the framework ships no loader for, so omitting `type` failed every declaration — loudly since 0.13.0's earlier hardening, silently before it. The default is now the one type core actually serves, which turns that failure into a working file corpus.
- **BREAKING:** `RoleRagBinding.evictForTesting(Class)` is renamed to `invalidate(Class)` and is now a supported operation. It shipped `public` in 0.12.0 with javadoc reading *"Production code should never call this"*, which left applications with **no way to refresh a Role's corpus at all**: a binding — its loaded documents, its indexes, and its retrieval-result cache — is built once per Role class per JVM and kept for the process lifetime. Nothing re-reads the knowledge sources on its own, so a deployment whose corpus changed on disk kept answering from the corpus as it stood at first dispatch. Call `invalidate(roleClass)` after the corpus behind a Role's `@KnowledgeSource`s changes; the next dispatch re-ingests, and retrieval already in flight finishes against the snapshot it started on. `UpgradeTnsAI_0_13_0` migrates the call. Worth being explicit about what this does *not* mean: `cacheTTL` never governed corpus freshness. It bounds how long a result is reused, but an expired entry is recomputed against the same immutable index and returns the same documents — so `cache = false` was never a workaround for a changed corpus, only a way to pay for the same answer more often.
- **BREAKING:** `SourceLoader` now consumes the canonical `KnowledgeSourceConfig` runtime model instead of reflection-backed `@KnowledgeSource` annotation instances. Annotation and builder declarations are normalized before loader invocation, preventing two divergent loader configuration paths. Both `load(source)` and the optional `metadata(source, document)` hook change their parameter type, so an out-of-tree loader must be recompiled against `KnowledgeSourceConfig`.
- **BREAKING:** normalization now rejects annotation values that could not be honoured, at the point the declaration is read rather than wherever it happened to fail later: a non-positive `@Retrieval.topK`, a `minScore` outside `0.0..1.0` or non-finite, a blank name in `@Retrieval.sources`, a blank `@KnowledgeSource.name`, and a non-positive `topK`, `dimensions`, or (with caching on) `cacheTTL` on `@KnowledgeSource`. These were previously accepted and silently retrieved nothing.
- `RoleRagBinding`'s per-Role cache is keyed by Role class **and** the builder source template, so two agents sharing a Role class but declaring different sources no longer share one ingested corpus. Annotation-only bindings are unaffected and still shared by Role class. Sources are also merged by name: a builder source replaces an annotation source of the same name rather than being ingested alongside it.
- **BREAKING:** consolidated `tnsai-quality` under `com.tnsai.quality.*`, ending the half-finished migration that left `com.tnsai.{observability,security,validation}` running in parallel with — and split across modules against — `com.tnsai.quality.*`. The quality-owned classes moved: `com.tnsai.observability.*` → `com.tnsai.quality.observability.*` (top-level + `health`/`theme`), `com.tnsai.security.*` → `com.tnsai.quality.security.*` (incl. `llm`/`audit`/`validation`/`moderation`), `com.tnsai.validation.*` → `com.tnsai.quality.validation.*`. Core-owned contracts (`com.tnsai.security.{SecurityException, PermissionProvider,SecurityEnforcerHandle,audit,approval}`, `com.tnsai.observability.{errors,events}`) are unchanged.
- **BREAKING:** fixed the `com.tnsai.coordination.groups` split package — the 16 agent-group *contracts* (`AgentGroup`, `AgentGroupFactory`, `GroupRegistry`, `MembershipManager`, …) shipped from `tnsai-core` while their implementations ship from `tnsai-coordination`, so the same package came from two modules (blocks JPMS, muddies layering). The contracts moved to the core-owned `com.tnsai.agents.groups` (joining `AgentGroupManager`); `com.tnsai.coordination.groups` is now coordination-only (impls + topology subpackages). A new ArchUnit guard (`GroupsPackageArchitectureTest`) keeps the contracts from leaking back.
- **BREAKING:** `@Retrieval.rerank` is enforced instead of ignored. Until now `rerank = true` was accepted and silently dropped — retrieval injected `topK` documents in strategy order and the annotation was decorative. It now resolves a `Reranker` provider, and because the model identifier is annotation configuration rather than request data, a blank `rerankerModel`, a non-positive `topN`, or an identifier no installed provider serves is a **configuration error that fails the annotated action at dispatch time** — `onFailure = CONTINUE` (the default) no longer applies to it. That policy still covers runtime failures, including a provider that throws while ranking. Routing configuration errors through `CONTINUE` set `_rag_document_count = 0` and left `_rag_context` unset, which `LLMRoleExecutor`'s splice guard reads as "retrieval was skipped": the LLM answered ungrounded while the action reported success. Migration: TnsAI ships no cross-encoder, so every `@Retrieval(rerank = true)` needs a `Reranker` provider module on the classpath serving its `rerankerModel`, or `rerank = false` — including annotations copied from the `@Retrieval` javadoc example, whose `"provider:model-id"` is a placeholder.
- **BREAKING:** `@Retrieval.cache` is enforced instead of ignored. Until now `cache` and `cacheTTL` were accepted and silently dropped — every dispatch re-ran retrieval. Because `cache` defaults to `true` and `cacheTTL` to 300, **every existing `@Retrieval` now serves results up to five minutes stale without any annotation change**; set `cache = false` to keep the previous per-dispatch behaviour. Following the `rerank` precedent above, a non-positive `cacheTTL` is annotation configuration that cannot be honoured, so it is a **configuration error that fails the annotated action at dispatch time** rather than something `onFailure = CONTINUE` can swallow — validated before the binding is resolved and before any retrieval or cache lookup runs. Reranker provider resolution likewise stays ahead of the cache lookup, so a warm entry can never postpone a misconfigured `rerankerModel` to whenever it expires. Cached results live for the JVM's lifetime, bounded by the entry cap and TTL; dropping a Role's binding clears its cache.
- **BREAKING:** `@Retrieval.onFailure = USE_CACHE` and `RETRY_SIMPLE` are implemented instead of degrading to `CONTINUE`. Until now both enum constants were accepted and quietly handled as `CONTINUE` — a retrieval failure logged a warning, set `_rag_document_count = 0` and let the action succeed. **Existing annotations already declaring either value change behaviour with no compile error**, including cases that now fail an action that previously succeeded; set `onFailure = CONTINUE` explicitly to keep the old behaviour. Specifically: `USE_CACHE` falls back to the *expired* result-cache entry for the exact same key — every document is tagged `tnsai.retrieval.provenance=stale-cache`, and the dispatch is flagged in the action context under the new `RetrievalSpi.RETRIEVED_STALE_FALLBACK_KEY` (`_rag_stale_fallback`) so consumers can tell stale grounding from fresh — and **rethrows the original failure when there is no such entry** rather than continuing ungrounded. Because the cache key covers every input that changes the document list, a fallback can never serve results produced under different retrieval settings, and only an expired entry is eligible (a live one would have been served as a hit, with no failure to fall back from). Following the `rerank`/`cacheTTL` precedent above, `onFailure = USE_CACHE` with `cache = false` is a contradiction the annotation cannot honour and is now a **configuration error that fails the annotated action at dispatch time**. `RETRY_SIMPLE` makes exactly one further attempt, against `Strategy.KEYWORD` with a punctuation-normalised query and no reranking, and **throws the retry's failure (with the primary attached as a suppressed exception) when that attempt also fails**; the retry result is injected but never cached, since it was produced under a different strategy than its key describes. Under `rerank = true` that retry is still capped at `topN` even though no reranker runs — `topN` is otherwise enforced only inside the reranking stage, so an uncapped fallback would inject up to `topK` documents, *more* context than the healthy reranked path delivers and at the moment relevance ordering is at its worst. Under `rerank = false` the cap does not apply: `topN` is unvalidated there and no other path honours it, so capping would let a `topN = 0` annotation silently reduce the retry to no grounding at all. Neither policy can swallow cancellation: an interrupted or cancelled dispatch propagates ahead of the policy switch and never spends a retry. The three policies that swallow the failure rather than rethrowing it — `CONTINUE`, `USE_CACHE` serving a hit, and `RETRY_SIMPLE` succeeding — no longer interpolate the failure message into their warning, a retrieval transport error can carry the endpoint or the query itself, and log the full cause and stack at `DEBUG` instead. The diagnostic gap is widest on the latter two: both return a populated `_rag_context` and a positive `_rag_document_count` and let the action succeed, so the warning is the only evidence retrieval failed at all. To keep stale-if-error possible, `RetrievalResultCache` now retains expired entries until a successful refresh, an explicit clear, or LRU eviction, instead of purging them on read.
- `@Retrieval.maxStaleness` bounds how long past expiry a cached result may still serve `onFailure = USE_CACHE`, defaulting to **3600 seconds**. `cacheTTL` bounds how long a result is *fresh*; nothing bounded how long an expired one could stand in for a failed retrieval, so retention was limited only by a successful refresh, an explicit clear, or eviction — a provider that stayed down kept the model answering from ever-older grounding, while `_rag_document_count > 0` and a populated `_rag_context` read as fully grounded. The two windows are measured in sequence, not from the same instant: an entry is fresh for `cacheTTL`, eligible as a stale fallback for a further `maxStaleness`, then no longer eligible — past the ceiling the original failure is rethrown, so the policy degrades to nothing rather than to something arbitrarily old. This mirrors HTTP `stale-if-error` (RFC 5861), whose max-age argument likewise counts from the end of the freshness lifetime. `maxStaleness = -1` opts out and restores an unbounded fallback; any other non-positive value is rejected when `USE_CACHE` is selected, on the same terms as `cacheTTL`. The value is deliberately **not** part of the cache key — it is a read policy, not part of what was stored, so changing it must not orphan existing entries. The default was chosen while `USE_CACHE` had no users, because adding the element later is not breaking but choosing its default later is: a finite default would silently change behaviour for adopters, and an unbounded one would permanently make the bound opt-in.
- **BREAKING:** `@Retrieval.queryExpansion` / `expandedQueries` are enforced instead of ignored, and `Strategy.MULTI_QUERY` performs multi-query retrieval instead of silently degrading to `SEMANTIC`. Until now `queryExpansion` and `expandedQueries` were read in exactly one place — building the result-cache key — and produced no retrieval behaviour at all, while `strategy = MULTI_QUERY` fell back to `SEMANTIC` with one warning per Role. **Existing annotations already declaring either value change behaviour with no compile error**: a Role that set `queryExpansion = HYDE` and got plain semantic retrieval now issues provider calls and retrieves against the generated variations. Set `queryExpansion = NONE` (and a strategy other than `MULTI_QUERY`) to keep the old behaviour. Following the `rerank`/`cacheTTL`/ `onFailure` precedent above, configuration that cannot be honoured is a **configuration error that fails the annotated action at dispatch time** rather than a degraded retrieval: `expandedQueries <= 0` with expansion active is rejected before any retrieval runs, and an unresolvable or ambiguous `queryExpansionModel` is rejected **outside** the `onFailure` guard — no policy, including `USE_CACHE` and `RETRY_SIMPLE`, may soften a misconfigured expander into ungrounded or stale-cached output. A provider that *raises* is a transport concern and stays under `onFailure` as before; the `RETRY_SIMPLE` retry deliberately runs unexpanded, so a failing expander is not called a second time, and stays capped at `topN` under `rerank = true`. New annotation member `queryExpansionModel` (default `""`) names the provider model and is required — and covered by the result-cache key — whenever expansion is active, so results retrieved under different expansion settings can never collide. Direct callers of the public `RoleRagBinding.strategyFor(Strategy)` see the same break: `MULTI_QUERY` now throws `IllegalArgumentException` instead of returning the SEMANTIC fallback, because expansion needs annotation-bound configuration the method has no access to — composing it is `DefaultRetrievalSpi`'s job. `GRAPH`, `HIERARCHICAL`, and `TEMPORAL` were still in the Phase-2 fallback group at this point; later items in this release emptied it.
- **BREAKING:** `@Retrieval.deduplicate` / `dedupeThreshold` are enforced instead of ignored. Until now both were read in exactly one place — building the result-cache key — and produced no retrieval behaviour at all. Because `deduplicate` defaults to `true` and `dedupeThreshold` to `0.9`, **every existing `@Retrieval` now injects fewer documents than before, with no compile error**: near-duplicate passages that previously reached the model as separate documents are collapsed into one. Set `deduplicate = false` to keep the previous behaviour. Similarity is measured with the binding's own embedding function — the one its vector index was built with, so documents compare under the measure that retrieved them — falling back to token-set Jaccard for content that embeds to a zero vector. Candidates at or above the threshold are grouped, the highest-ranked member survives as the representative and keeps its retrieval score, and the merge is recorded on it as `retrieval.deduplicatedDocumentCount`, `retrieval.deduplicatedSources` and `retrieval.deduplicatedEntryIds`, which a `Reranker` provider sees on the candidates it is handed. Only the representative's `source` reaches the prompt, so a merged document is attributed to its highest-ranked origin. `dedupeThreshold = 1.0` means "collapse only exact duplicates" and now actually does: cosine similarity is snapped to its endpoints, which floating-point rounding otherwise left unreachable for some texts and reachable for others. Deduplication runs **after** query expansion and reciprocal-rank fusion and **before** reranking. Fusion collapses only exact repeats of one entry across variations, so the near-duplicates `dedupeThreshold` exists for are precisely the ones that survive it, and deduplicating each variation first would instead change the ranks fusion scores. It runs **inside** the result cache's loader, so a hit serves an already-deduplicated entry and pays for no second similarity pass; `deduplicate` and `dedupeThreshold` were already covered by the cache key, and the threshold is now normalised out of that key while `deduplicate = false`, so a setting that cannot change the document list no longer splits the cache. `onFailure = RETRY_SIMPLE` keeps deduplication even though it drops reranking — a degraded retry must not widen the prompt with repetition — and its `topN` cap applies afterwards, so the cap counts distinct documents on both paths. Following the `rerank`/`cacheTTL`/`onFailure`/ `queryExpansion` precedent above, a non-finite `dedupeThreshold` or one outside the inclusive `0.0`–`1.0` range is, with `deduplicate = true`, a **configuration error that fails the annotated action at dispatch time**, rejected before the Role binding is resolved rather than surfacing as whatever the binding trips over first. With `deduplicate = false` the value is inert and deliberately left unvalidated, so an annotation carrying a leftover threshold on a feature it has switched off still dispatches.
- **BREAKING:** `@Retrieval.contextWindow` and `includeSpec` are enforced instead of ignored. Until now both were accepted and silently dropped — retrieval concatenated every document in full, so `contextWindow` bounded nothing and `includeSpec = false` still rendered `${source}` and `${score}` into the prompt. Neither had a single call site in main sources; they were inert annotation surface. **Every existing `@Retrieval` changes what reaches the model with no annotation change and no compile error**: `contextWindow` defaults to 4000 tokens, so a Role whose retrieved documents exceed that budget now has its context truncated, and any Role declaring `includeSpec = false` stops emitting source and score values it previously leaked. There is no opt-out flag — `contextWindow` is a hard bound by construction — so a Role that relied on the old unbounded behaviour must raise `contextWindow` to cover its corpus; the new `_rag_context_truncated` key (above) makes it observable when it does not. The rendered context is measured with the canonical content-aware `TokenEstimator`, including `contextFormat` overhead, and content is cut only at Unicode code-point boundaries, so truncation can never emit half a surrogate pair. `contextFormat` is now parsed rather than string-replaced: `${content}`, `${source}` and `${score}` are the only supported placeholders, document content is substituted as data and can no longer introduce placeholders of its own, and an unsupported or unterminated placeholder is — following the `rerank`/`cacheTTL`/`onFailure`/`queryExpansion`/`dedupeThreshold` precedent above — a **configuration error that fails the annotated action at dispatch time**, as is a non-positive `contextWindow`. Assembly runs in the injection path, *after* the result-cache lookup rather than inside its loader: the cache stores documents, not rendered text, so `contextWindow`, `includeSpec` and `contextFormat` cannot change what is retrieved and are deliberately absent from the cache key — two actions differing only in those settings share one entry and each still renders its own context.
- **BREAKING:** `@Retrieval(strategy = GRAPH)` performs graph retrieval instead of silently falling back to `SEMANTIC`. Until now `GRAPH` logged one warning per Role and returned the vector strategy, so **every existing `@Retrieval(strategy = GRAPH)` changes behaviour with no compile error**: it either retrieves through a graph adapter or stops dispatching, where before it always returned vector results. Retrieval starts from provider-scored seed nodes, expands outgoing edges best-first by path score, terminates on cyclic graphs, and emits each node once through its highest-scoring path — so a node reachable by several routes is grounded on the strongest one rather than the first one found; scores decay by edge weight and depth, and `maxDepth` (2), `maxVisitedNodes` (1024), `minScore`, `topK` and the `sources` fence all bound the walk. The fence applies to traversal as well as to seeds, so a scoped query cannot reach a node outside the selected sources by walking through one. Every result carries `graphNodeId`, `graphDepth`, `graphPath`, `graphEdgePath` and `graphProvider` metadata. Ordering is deterministic for a given store: ties break on a stable path key rather than on provider iteration order.

  **A missing or ambiguous provider is a configuration error, not a retrieval failure.** Which adapter serves a Role is fixed by the annotation and the installed classpath, never by request data — the same reasoning that governs `rerankerModel` and `queryExpansionModel`. So with **no `GraphStoreProvider` on the classpath** — or none that claims the Role, or more than one that does — the annotated action fails at dispatch with `GraphCapabilityException`, raised *above* the `@Retrieval.onFailure` guard. It does **not** fall back to vector retrieval, and no failure policy can soften it: `onFailure = CONTINUE` cannot turn an absent adapter into a silently ungrounded action, and `USE_CACHE` cannot answer it from a stale entry. Only failures a provider raises *while traversing* are runtime concerns and stay under `onFailure` as before. Direct callers of the public `RoleRagBinding.strategyFor(Strategy)` see the same break: `GRAPH` now resolves a provider or throws, joining `MULTI_QUERY` outside the Phase-2 fallback group; `HIERARCHICAL` and `TEMPORAL` were still in it at this point, and left it in later items in this release.
- **BREAKING:** `@Retrieval(strategy = HIERARCHICAL)` performs hierarchical retrieval instead of silently falling back to `SEMANTIC`. **Existing annotations change behaviour with no compile error**: an action that previously received flat vector hits now receives those hits *plus* their expanded parent context, ordered by decayed relevance, capped by `topK`, and carrying hierarchy provenance in each document's metadata. When nothing on the classpath supplies hierarchy metadata — the built-in FILE loader does not override `SourceLoader.metadata` — retrieval **does not throw and does not fall back**: the documents form a flat hierarchy and are returned exactly as before, now explicitly marked `hierarchyFlat=true` instead of being indistinguishable from vector retrieval. This is the deliberate difference from `GRAPH`, which has no meaningful degraded form and so requires a provider. A *malformed* hierarchy — a duplicate ID, a dangling parent, or a cycle — is by contrast a configuration fault: it is decided by the Role's knowledge sources and installed loaders, never by request data. It is therefore validated at strategy-selection time, above the failure-policy guard, and no policy can soften it — `onFailure = CONTINUE` cannot turn it into a silently ungrounded action and `USE_CACHE` cannot answer it from a stale entry. Only failures raised *while traversing* a valid hierarchy remain runtime concerns under `onFailure`. Direct callers of the public `RoleRagBinding.strategyFor(Strategy)` see the same break: `HIERARCHICAL` now returns a hierarchical strategy or throws, joining `MULTI_QUERY` and `GRAPH` outside the Phase-2 fallback group, which left `TEMPORAL` as its only member until a later item in this release removed it.
- **BREAKING:** `@Retrieval(strategy = TEMPORAL)` performs temporal retrieval instead of silently falling back to `SEMANTIC`. **Existing annotations change behaviour with no compile error**: an action that previously received plain vector hits — with one WARN per Role announcing the fallback — now receives those hits re-ranked by bounded exponential time decay, capped by `topK`, and carrying temporal provenance in each document's metadata (content score, decay, multiplier, age, status, source group). The default half-life is 30 days and freshness controls only 20% of the multiplier, so at least 80% of the content score is retained and a fresh but irrelevant document cannot displace a clearly relevant older one on age alone. Loaders declare timestamps through the optional `SourceLoader.metadata` hook as `TemporalMetadata.TIMESTAMP`, an ISO-8601 instant; documents that declare none are retained at the conservative relevance floor and marked `temporalDated=false`, so a corpus with no timestamps at all is returned as before rather than throwing. This is the same deliberate difference from `GRAPH` that `HIERARCHICAL` makes: temporal retrieval has a meaningful degraded form, so it does not require a capability. *Malformed* temporal metadata — a duplicate entry ID, a blank timestamp, or one `Instant.parse` rejects — is by contrast a configuration fault: it is decided by the Role's knowledge sources and installed loaders, never by request data. `TemporalIndex.validate()` is therefore called at strategy-selection time, above the failure-policy guard, and no policy can soften it — `onFailure = CONTINUE` cannot turn it into a silently ungrounded action and `USE_CACHE` cannot answer it from a stale entry. Only *clock-relative* faults stay under `onFailure`: a timestamp beyond the tolerated five-minute future skew depends on the retrieval instant rather than on configuration, and the same index turns valid as wall-clock time advances. Direct callers of the public `RoleRagBinding.strategyFor(Strategy)` see the same break: `TEMPORAL` now returns a temporal strategy or throws. It was the last member of the Phase-2 fallback group, so that group — and the per-Role fallback warning that announced it — is gone.
- `tnsai-core`: `@Retrieval.cacheTTL` javadoc now records that `Strategy.TEMPORAL` ranks against the wall clock, which is not configuration and so cannot join the result-cache key. A cached temporal ranking is pinned for the entry's TTL. At the default 300 s against the 30-day half-life the freshness multiplier moves by well under a hundredth of a percent and cannot reorder results; the note tells deployments that need faster turnover to shorten the TTL.
- `tnsai-evaluation`: the `Evaluator` SPI registry now registers the canonical `evaluators.agent` set (TNS-373) instead of the legacy `evaluators.agentic` duplicates, and additionally registers the previously-dead `step_efficiency` evaluator. A `ServiceLoader` uniqueness test now guards against duplicate evaluator names (benchmark aggregation keys on the name, so duplicates silently collide).
- `tnsai-evaluation`: extracted `AbstractJudgeEvaluator` — a template-method base for single-call LLM-as-judge evaluators (build prompt → judge once → parse verdict, with a shared `precheck` hook and the common `"LLM judge error: …"` handling). The 12 single-call evaluators (rag `Faithfulness`/`ContextualRecall`/`AnswerRelevancy`, safety `Hallucination`/`Bias`/`Toxicity`, multiturn `KnowledgeRetention`/`TurnRelevancy`/ `ConversationCompleteness`, agent `PlanAdherence`/`TaskCompletion`/`ToolCorrectness`) now extend it, dropping the duplicated judge field/ctor/`name()`/try-catch boilerplate; behavior preserved. The two multi-step judges (`GEvalEvaluator`, `ContextualPrecisionEvaluator`, which call the judge more than once) deliberately keep implementing `Evaluator` directly.

### Removed
- **BREAKING:** removed the `KnowledgeType` values `VECTOR_DB`, `WEB_SEARCH` and `CACHE`. These are not sources a loader can ingest: a vector database and a search API are queried *per request*, while `SourceLoader.load` runs once at binding time and returns a fixed document set. No implementation could have satisfied them at this seam, which is why none was ever written — the only references anywhere in the tree were tests using the two spare constants as registration keys. Retrieval against a remote index belongs to `com.tnsai.rag.RetrievalEngineProvider`, which is invoked per retrieval; that is the contract a Qdrant or pgvector module should implement. `FILE`, `URL`, `DATABASE` and `MEMORY` remain — they fit the ingest shape — and of those only `FILE` has a bundled loader, the rest being SPI extension points an optional module registers against. **No automated migration**: the values were removed with nothing to rewrite them to. A declaration using one must move to a `RetrievalEngineProvider` or to an ingest type.
- **BREAKING:** deleted the orphaned `com.tnsai.security.sandbox` package (`SandboxedExecutor`/`SandboxSpec`) — unreferenced dead code whose own javadoc admitted its `networkAccess`/`fileAccess` flags were *not enforced* (a sandbox that does not sandbox). Use container/process isolation for untrusted-code execution.
- **BREAKING:** deleted the duplicate `com.tnsai.evaluation.evaluators.agentic` package (`PlanAdherenceEvaluator`/`TaskCompletionEvaluator`/`ToolCorrectnessEvaluator`) — superseded by the canonical `evaluators.agent` set with the same evaluator names.
- `tnsai-core`: **`AgentBuilder` BDI seeding surface removed** (TNS-541). **BREAKING:** deleted the fluent `belief`/`beliefs`/`desire`/`desires`/`intention`/ `intentions`/`capability`/`capabilities` methods (plus their backing fields and package-private getters) from `AgentBuilder`. The surface was dead — values were collected at `build()` but never read: `ConfigurableAgent` never consumed them, the getters had zero callers (verified: no production/test usage), and beliefs/ desires/intentions/capabilities were already dropped from the LLM prompt in 2.20.0. The BDI **model** classes (`Belief`/`Desire`/`Intention`/`Capability`) are untouched — they stay live via `CognitiveEngine`/`ContextSnapshot`, where BDI state is populated at runtime. `AgentBuilder` plan handling is unaffected (still read by `ConfigurableAgent`). Migration: none — these methods had no runtime effect; delete any stray `.belief(...)`/ `.desire(...)`/`.intention(...)`/`.capability(...)` calls. [#444](https://github.com/TnsAI-Framework/TnsAI/pull/444)

### Fixed
- `dry-deploy-validate.sh` now validates `tnsai-payments` artifacts — the module is published but had been missing from the staging-validation module list.
- `tnsai-llm`: semantic cache no longer embeds `nomic-embed-text` without a task prefix. Un-prefixed input collapsed every vector into a narrow cosine band (unrelated text scoring 0.6–0.75, ranking inversions), making semantic neighbour selection noise. `InMemorySemanticCache` now embeds with the symmetric `clustering:` task on both store and lookup.
- `tnsai-payments`: `X402PaymentBroker.settle` no longer double-charges under concurrency. The idempotency check was a non-atomic get-then-put with the whole network settlement in between, so concurrent same-key calls could all pass the check and each charge. Settlement now reserves the idempotency key atomically (`putIfAbsent` of a settlement promise) before charging; concurrent callers await the winner and return its transaction as an idempotent replay.
- `tnsai-core`: retrieval context is now delivered to LLM prompts — resolved RAG results were assembled and then dropped before the call, so `@Retrieval` had no runtime effect.
- `tnsai-intelligence`: nested RAG documents are preserved instead of being flattened away during ingestion.
- `tnsai-intelligence`: retrieval sources are scoped to the declaring agent rather than shared globally across agents.
- `tnsai-intelligence`: loaderless RAG bindings are rejected at init instead of failing silently at query time.
- `tnsai-intelligence`: RAG fallback warnings are deduplicated — one warning per cause instead of one per retrieval call.
- `tnsai-core`: `@MemorySpec` capacity is enforced, and the `RELEVANCE`/`SUMMARIZE` prune strategies are implemented rather than declared-but-inert.
- `tnsai-core`: `FileMemoryStore` persistence hardened — atomic writes and corruption-safe loads.
- `tnsai-intelligence`: `FileSessionStore` durability and robustness hardened.
- `tnsai-core`: `@Memory(shared=true)` actually shares state between agents — it previously produced an isolated store per agent.
- `tnsai-core`: the `hasMemorySpec` gate covers every `@MemorySpec` member, so specs that set only a non-default member are no longer treated as absent.
- `tnsai-core`: `@KnowledgeSource.KnowledgeType.FILE` no longer claims to read PDF and DOCX. The bundled `LocalFileSourceLoader` is text-only — `.txt`, `.md`, `.markdown`, `.json`, `.yaml`, `.yml`, `.csv` — and always has been, so a source pointing at a PDF has been contributing nothing: filtered out silently inside a directory, or reported as `failed to read file` when named directly, which reads like an I/O problem rather than an unsupported format. Documentation fix only; a dedicated loader for those formats remains follow-up work.
- `tnsai-core` / `tnsai-intelligence`: corrected the claim, in three places, that a `@KnowledgeSource` whose type has no installed loader is a hard configuration error. `RoleRagBinding` fails only when *every* enabled source is unloadable; one unloadable source among loadable ones is warned and skipped, and the Role binds on a partial corpus. The behaviour is deliberate — it is what lets you install optional loaders one at a time — but "never a silently empty index" was not true of it.
- `tnsai-intelligence`: the no-loader warning now says which type is missing, how to register one, and what is currently registered, instead of stating that other types were "tracked for future phases" — nothing tracked them, and the constants that sentence described were removed in this release.
- `tnsai-intelligence`: removed seven javadoc promises that deferred behaviour to a "Phase 2" closed as complete on 2026-07-31. Each described a real gap and served as its only record, so each now has an issue instead: the query parameter cannot be named, ingestion does not chunk, and the embedding function is hard-wired, which makes declarative `SEMANTIC` retrieval lexical. `PhaseDeferralGuardTest` fails the build if such a promise reappears on the RAG surface.

### Security
- `tnsai-mcp`: `SSETransport` no longer sends credentialed requests to a server-chosen host. An `endpoint` SSE event was stored verbatim and used as the POST target for every subsequent message, and those requests carry the configured headers — so a compromised or malicious MCP server could redirect the stream to `https://evil.example.com/...` and receive the `Authorization: Bearer` token along with every tool argument. Endpoint events are now resolved against the SSE endpoint (as the MCP HTTP+SSE transport specifies, which also fixes spec-compliant servers advertising `/messages?sessionId=...` — those previously failed every send) and accepted only on an origin the caller configured: the SSE endpoint's, or the message endpoint's when one was configured on a different host. The server may pick the path and the session, but it cannot introduce a new origin. A rejected event leaves the endpoint in use untouched rather than tearing down the stream; an off-origin one is also reported to the transport's error consumer, so an attempted redirect is visible to the application instead of being a silent no-op. The transport's `HttpClient` now pins `followRedirects(NEVER)` explicitly — it was already the JDK default, but the guard depends on it, since a followed cross-host redirect carries the `Authorization` header with it.

### Migration
- Run `mvn rewrite:run` with `io.github.tansuasici.rewrite.UpgradeTnsAI_0_13_0` (module `tnsai-rewrite`) for the mechanical half: 61 type moves, 11 `@KnowledgeSource` attribute removals, and the `RoleRagBinding.evictForTesting` → `invalidate` rename. Activate the recipe for the version you are moving **to**.
- **Read your `@Retrieval` declarations first.** The recipe cannot help here: members that were accepted and silently discarded are now enforced, so code that compiles unchanged starts behaving differently. In particular `cache` defaults to `true` (300s TTL) — call `RoleRagBinding.invalidate(Class)` after a corpus change, or set `cache = false` to keep 0.12.0 behaviour.
- `rerank`, `queryExpansion` and `strategy = GRAPH` resolve through SPI seams with **no bundled provider**. Enabling them without an add-on module now throws at dispatch instead of being ignored.
- The removed `KnowledgeType` values (`VECTOR_DB`, `WEB_SEARCH`, `CACHE`) have no replacement to rewrite to — a remote index is queried per request, which the ingest-time `SourceLoader` seam cannot express. Move those behind `com.tnsai.rag.RetrievalEngineProvider`.
- `SourceLoader` implementers: `load(...)` and the optional `metadata(...)` hook take `com.tnsai.rag.KnowledgeSourceConfig` instead of the `@KnowledgeSource` annotation. `KnowledgeSourceConfig`'s six components are name-for-name and type-for-type identical to the narrowed annotation's six members, so a body that reads `name` / `type` / `path` / `connection` / `query` / `enabled` compiles unchanged and only the parameter type needs editing. A loader that read one of the 11 members removed above — `topK`, `provider`, `embeddingModel` and the rest — has that separate break to resolve too; those are retrieval knobs, and `SourceLoader` is an ingestion seam. Not covered by the recipe.

## [0.12.0] - 2026-06-04

First release since 0.11.0 — bundles the previously-unreleased 0.11.1 work (per-action guardrails, `@AuditLog` wiring, pure-orphan annotation removals) with a round of dogfood fixes. Includes breaking annotation/type renames; see Migration.

### Added
- `tnsai-llm`: **`OllamaEmbeddingProvider`** — keyless `EmbeddingProvider` over Ollama `/api/embed` (`nomic-embed-text` default; `OLLAMA_BASE_URL` / `OLLAMA_API_KEY`; batch input) via the shared `HttpClientFactory`.
- `tnsai-core`: **`AuthorityScope.permanent()`** — no-expiry authority scope (nullable `validFor`); `expiresAt()` → `Instant.MAX`, `isExpired()` → `false`. Removes the `Duration.ofDays(3650)` workaround for long-lived beans.
- `tnsai-core`: **Per-action guardrails — `ActionConfig.withInputGuardrail(...)` / `withOutputGuardrail(...)`** (TNS-643, follow-up to TNS-561) — a builder-added action (`RoleBuilder.addAction(ActionConfig...)`) can now carry its own input/output guardrail, the programmatic equivalent of a method-level `@InputGuardrail`/`@OutputGuardrail`. The config rides on `ActionMetadata` (mirroring how `ContractSpec`/TNS-637 is carried, since builder actions have no reflective `Method`), and the enforcers resolve it at **method-level precedence**: method annotation &gt; **per-action config** &gt; role builder config &gt; class annotation. Reflective (`@ActionSpec`) actions are unaffected (they keep reading their method annotation). Defaults to none, so existing builder actions behave identically. The agent-level guardrail tier remains in TNS-643 (blocked on agent-context wiring; see issue).
- `tnsai-core`: **`OutputGuardrailConfig` + `RoleBuilder.outputGuardrail(...)`** (TNS-561) — the output-side mirror of `InputGuardrailConfig` (below). New `com.tnsai.guardrails.OutputGuardrailConfig` record mirrors the **runtime-enforced** subset of `@OutputGuardrail` (`maxChars`/`minChars`/`onFailure`/`fallback`/`logFailures`) with `from(@OutputGuardrail)`, `defaults()`/`NONE`, and a fluent builder; the annotation's not-yet-wired scaffold fields are deliberately excluded (note `maxRetries` is inert because Phase-1 `RETRY` degrades to `REJECT`). `OutputGuardrailEnforcer` now resolves and enforces this record, with the annotation path adapting through `from(...)`. Resolution precedence: method `@OutputGuardrail` &gt; `RoleBuilder.outputGuardrail(...)` (via `Role.getOutputGuardrailConfig()`) &gt; class `@OutputGuardrail`. Existing annotation behaviour is unchanged. Both guardrail config records now also reject a contradictory bound pair (`maxChars`/`maxLength` &lt; `minChars`/`minLength` when both are non-zero) at construction — such a config makes every value unsatisfiable, so it fails fast.
- `tnsai-core`: **`InputGuardrailConfig` + `RoleBuilder.inputGuardrail(...)`** (TNS-561) — the programmatic counterpart of a class-level `@InputGuardrail`, so a builder-built role (which has no annotation to read) can opt into input guardrails. New `com.tnsai.guardrails.InputGuardrailConfig` record mirrors the **runtime-enforced** subset (`maxLength`/`minLength`/`blockPatterns`/`allowPatterns`/`onFailure`/ `errorMessage`/`logFailures`) with `from(@InputGuardrail)`, `defaults()`/`NONE`, and a fluent builder; the annotation's not-yet-wired scaffold fields are deliberately excluded to avoid inert surface. `InputGuardrailEnforcer` now resolves and enforces this record, with the annotation path adapting through `from(...)`. Resolution precedence: method `@InputGuardrail` &gt; `RoleBuilder.inputGuardrail(...)` (via `Role.getInputGuardrailConfig()`) &gt; class `@InputGuardrail`. Existing annotation behaviour is unchanged (the prior `enforce(@InputGuardrail, …)` entry point is preserved as a thin adapter).
- `tnsai-core`: **`@AgentSpec.toolCallFilter` declarative tool-call filter** (TNS-611) — the top-level annotation counterpart of `AgentBuilder.toolCallFilter(ToolCallFilter)`. The supplied `Class<? extends ToolCallFilter>` is instantiated via its public no-arg constructor at agent init (AGENT-V009 on failure, same as `@AgentSpec.roles`) and wired into the orchestrator before the first tool call. New public `com.tnsai.agents.execution.AllowAllToolFilter` doubles as the annotation default and the "not set" sentinel — the extractor surfaces it as `null` so the historical no-filter behaviour is preserved and the AGENT-V006 approval gate keeps warning about confirmation-required tools without an explicit filter. Precedence: `AgentBuilder.toolCallFilter(...)` / `Agent.setToolCallFilter(...)` wins over the annotation (`applyInitializationResult` only adopts the resolved filter when no pending filter was set).
- `tnsai-core`: **`@AgentSpec.maxContextTokens` declarative context budget** (TNS-609) — the top-level annotation counterpart of `AgentBuilder.maxContextTokens(int)`. Setting it (`> 0`) prunes the agent's conversation history to the budget before each LLM call (the same pruning the builder shortcut and `@MemorySpec.maxContextTokens` already drive). `AgentInitializer` resolves it with precedence: template &gt; `@AgentSpec.maxContextTokens` (top-level shortcut) &gt; `@MemorySpec.maxContextTokens` (nested). `0` (the default) keeps the previous behaviour, so existing agents are unaffected.
- `tnsai-mcp`: **MCP tool annotations map onto TnsAI safety hints** (TNS-641, follow-up to TNS-556) — `McpToolBridge.toDynamicToolMethod(...)` (the in-process MCP bridge, the documented `McpToolBridge.stdio(...).toTnsAITools()` path) now reads the MCP 2025-03-26 tool annotations: `destructiveHint=true → requiresConfirmation=true` (so a destructive MCP tool registered without a `ToolCallFilter` raises AGENT-V006 instead of dispatching unattended) and `idempotentHint=true → idempotent=true` (was hardcoded `false`). Both default to `false` when the annotation is absent, so tools with no annotations behave exactly as before. `readOnlyHint`/`openWorldHint` are not mapped (their counterpart `sideEffect` is not yet modelled — see TNS-556). The `tnsai-server` WebSocket path (`McpProxyTool`) is unchanged: its wire record `WsProtocol.McpToolDef` carries no annotations field, so propagating them there needs a protocol extension + client support (deferred).
- `tnsai-core`: **dynamic tools can opt into the AGENT-V006 approval gate** (TNS-556) — `DynamicToolMethod` (the runtime tool form fronting MCP servers) gained `requiresConfirmation` + `keywords`, mirroring `@Tool(requiresConfirmation=…, keywords=…)` on the annotated path. `AgentBuilder`'s confirmation scan now inspects registered dynamic tools too, so a confirmation-gated MCP/dynamic tool with no `ToolCallFilter` wired raises AGENT-V006 — previously only `@Tool` POJOs were scanned and dynamic tools silently escaped the check. Non-breaking: the pre-existing 5-arg shape is preserved as `DynamicToolMethod.of(...)` and a delegating 5-arg constructor (both default the new fields to the safe "no claim" values); a fluent `DynamicToolMethod.builder(name)` sets them. (`sideEffect`/`idempotencyHint` from `@Tool` are intentionally not modelled yet — no runtime consumer reads them off a `ToolMethod`, so they'd be no-op surface.)
- `tnsai-core`: **programmatic role resilience** (TNS-567) — `RoleBuilder.resilience(ResilienceConfig)` lets a builder-built role carry a retry/timeout policy without subclassing, the counterpart of class-level `@Resilience`. `ActionExecutor` applies it when no `@Resilience` annotation is present. Scoped to the runtime-enforced subset (retry + timeout); circuit-breaker / rate-limit / bulkhead await TNS-565 Phase 2. [#430](https://github.com/TnsAI-Framework/TnsAI/pull/430)
- `tnsai-core`: **programmatic `MemoryConfig`** (TNS-546) — `AgentBuilder.memoryConfig(...)` closes the `@MemorySpec` parity gap (8 fields, only `maxContextTokens` had a builder shortcut before). `MemoryConfig` record mirrors `@MemorySpec` with `from()`/`defaults()`/ `builder()`; `MemoryStoreFactory.create(MemoryConfig)` is the canonical factory the annotation path now delegates through. [#429](https://github.com/TnsAI-Framework/TnsAI/pull/429)
- `tnsai-core`: **programmatic `ContractSpec`** (TNS-637) — the builder-path form of `@Contract`. `ActionConfig.withContract(ContractSpec.builder()…build())` attaches Design-by-Contract gates (pre/post/invariants) to a `RoleBuilder.addAction(...)` action, enforced identically to the annotation. `ContractValidator` now operates on `ContractSpec` for both paths (the annotation adapts via `ContractSpec.from`). [#428](https://github.com/TnsAI-Framework/TnsAI/pull/428)
- `tnsai-core`: **build-time `@Contract` expression validation** (TNS-637, AGENT-V013). `AgentBuilder.build()` now parses every JEXL clause of each action's `@Contract` (`preconditions`/`postconditions`/`invariants`) and reports a malformed expression as a suppressible warning, instead of failing on first invocation. Suppress with `.relaxValidation("AGENT-V013")`. [#427](https://github.com/TnsAI-Framework/TnsAI/pull/427)

### Changed
- `tnsai-core`: **`com.tnsai.identity.AgentSpec` record renamed to `AgentDescriptor`**. **BREAKING:** resolves the simple-name collision with the `@com.tnsai.annotations.AgentSpec` annotation (which keeps its name, per the `@*Spec` = annotations convention). All referrers updated.
- `tnsai-core`: **`@com.tnsai.roles.annotations.RoleIdentity` renamed to `@RoleDeclaration`**. **BREAKING:** resolves the collision with the `com.tnsai.models.role.RoleIdentity` class (unchanged).
- `tnsai-core`: **role export reads `@RoleSpec.llm()` (`@LLMSpec`) instead of `@LLM`** (TNS-642, follow-up to TNS-568). **BREAKING:** the `RoleSpecExtractor.LLMSpec` nested record is renamed to `RoleSpecExtractor.LLMExportSpec` (it collided on simple name with the `@LLMSpec` annotation), and `RoleSpecExtractor.hasLLMAnnotation(...)` is renamed to `hasLLMConfig(...)`. The role-export subsystem (`ExportedRole`, `YamlRoleExporter`, `JsonRoleExporter`) now sources LLM config from the nested `@LLMSpec` a role actually declares — so roles using `@RoleSpec(llm=@LLMSpec(...))` now export their LLM config (previously the exporter only read the unused TYPE-level `@LLM`). `@LLMSpec.endpoint` populates the export record's `baseUrl`. [#435](https://github.com/TnsAI-Framework/TnsAI/pull/435)
- `tnsai-llm`: **canonical `providerId()` for error-mapper resolution** (TNS-624). The `ProviderErrorMapper` SPI lookup keyed off the lower-cased `executeRequest` display name, which drifts from the mapper's clean token (`"Together.ai"` → `together.ai` never matched `together`), so mappers loaded but never resolved for ~half the providers. `AbstractLLMClient.providerId()` now supplies a single canonical id per provider as the lookup key (display label kept for logs); all 29 executeRequest-based clients override it. Foundation for the missing-mapper fix. No public-API change. [#423](https://github.com/TnsAI-Framework/TnsAI/pull/423)
- `tnsai-llm`: **OpenRouter / Mistral / HuggingFace folded onto `AbstractOpenAICompatibleClient`** (TNS-636, follow-up to TNS-621), net −629 LOC. Each kept only its real divergence: OpenRouter's ranking / Claude-beta headers move to `addProviderHeaders()`, HuggingFace keeps a `parseChatResponse` override for usage tokens, Mistral has zero overrides. OpenAI, ZhipuAI and MiniMax stay bespoke (`JsonCapableLLMClient` + per-call `response_format`). No public-API change. [#425](https://github.com/TnsAI-Framework/TnsAI/pull/425)

### Removed
- `tnsai-core`: **`@SystemPrompt`, `@ChannelSpec`, `@Pipeline`, `@PipelineStep` removed** (TNS-569/573/577). **BREAKING:** four more pure-orphan annotations with no runtime consumer (verified: zero reflection readers, zero production/test usage). Their canonical counterparts are untouched: the `SystemPromptBuilder`, the channel `Channel` interface, and `PipelineBuilder` (`tnsai-coordination`) — these were always the wired surfaces; the annotations only mirrored their names. `@Pipeline`/`@PipelineStep` referenced only each other (Javadoc). Migration: none — delete any stray applications. [#439](https://github.com/TnsAI-Framework/TnsAI/pull/439)
- `tnsai-core`: **more pure-orphan annotations removed** (TNS-566/576/578/589). **BREAKING:** deleted `@RateLimited` (566 — superseded conceptually by `@Resilience`, but never wired), the channel-hook markers `@OnConnect`/`@OnDisconnect`/`@OnMessage` (576), the FSM family `@FSMState`/`@FSMTransition`/`@FSMStates`/`@FSMTransitions` (578 — defined but no FSM engine reads them), and `@RequiresPairing` (589). Each was source-verified as a pure orphan (no reflection reader, no production/test usage); the FSM annotations referenced only each other (`@Repeatable` containers). Stale Javadoc references in kept files were cleaned (`ChannelSpec`, `resilience/package-info`). Migration: none — these had no runtime consumer; delete any stray applications. [#438](https://github.com/TnsAI-Framework/TnsAI/pull/438)
- `tnsai-core`: **9 pure-orphan annotations removed** (TNS-590, Section 13 cleanup). **BREAKING:** deleted `@ContextCompaction`, `@SlashCommand`, `@WorkspaceSpec`, `@Property`, `@ConfigProperty`, `@Trigger`, `@Delegate`, `@Sanitize`, `@ContentFilter` from `com.tnsai.annotations`. Each had **no runtime consumer, no reflection reader, and zero production usage** (verified by source-trace) — they were unwired scaffolding that only added surface area and "is this wired?" confusion. Migration: none needed — removing a no-op annotation cannot change runtime behaviour; delete any stray applications. (`@NormType` is intentionally retained — it is the value enum for the `@Norm`/`@Norms` deontic-logic wiring tracked under TNS-591.) [#437](https://github.com/TnsAI-Framework/TnsAI/pull/437)
- `tnsai-core`: **`@LLM` annotation removed** (TNS-642, follow-up to TNS-568). **BREAKING:** the TYPE-level `com.tnsai.annotations.LLM` is gone — it was a parallel, export-only LLM-config surface with zero production usage that never instantiated an `LLMClient` and shadowed the live, integrated `@LLMSpec` (nested in `@RoleSpec`/`@AgentSpec`, which all four integration examples use). Migrate any `@LLM(provider=…, model=…)` on a role to `@RoleSpec(llm=@LLMSpec(provider=Provider.…, model=…))`. Resolves the `LLMSpec` simple-name collision and unblocks TNS-570 (`@LLMSpec` field wiring) + TNS-571 (programmatic `LLMConfig`). [#435](https://github.com/TnsAI-Framework/TnsAI/pull/435)

### Fixed
- `tnsai-llm`: **dangling `EmbeddingProvider` javadoc** — `EmbeddingProvider`, `CachedLLMClient`, and `SemanticCache` referenced a non-existent `OpenAIEmbeddingProvider`; examples now use the real `OllamaEmbeddingProvider`.
- `tnsai-quality`: **`@AuditLog` is now wired into `AuditLogger`** (TNS-579) — the annotation was declared on `tnsai-core` but never read, so marking an action `@AuditLog(action = "...")` produced no audit trail. `SecurityEnforcer.audit(...)` (already invoked per action via the `SecurityEnforcerHandle` SPI) now also reads the method's `@AuditLog` and emits a declarative named-action entry through the new `AuditLogger.auditAction(...)`. It is **independent of `@Security`** (an action with only `@AuditLog` is audited) and **complementary** (an action with both emits both records). `includeArgs`/`includeResult` gate whether args/result are logged, and both honour `@Security` masking — `includeArgs` routes through the same `maskForLogging` (so `@Security(maskFields = …)` fields are masked) and the result is masked when `@Security(sensitive = true)`, matching the level-audit so neither path leaks. No `tnsai-core` change (the annotation already existed; the wiring lives entirely in `tnsai-quality`).
- `tnsai-core`: **`@LLMSpec.topP` is now honored by `@RoleSpec`-driven role LLM init** (TNS-570, partial). `Role.initializeLLMFromAnnotation()` — the live path that builds a role's `LLMClient` from `@RoleSpec(llm=@LLMSpec(...))` — passed `null` for the `topP` argument of `LLMClientProvider.create(...)` even though the whole client layer accepts it, so a role's configured nucleus-sampling value was silently dropped. It now passes `@LLMSpec.topP()` (the model-default `1.0f` maps to `null`, matching the `LLMClientFactory` convention). The remaining `@LLMSpec` fields (`frequencyPenalty`/`presencePenalty`/`timeoutMs`/`endpoint`/`apiKeyEnv`) are **not** yet honored — they need a client-layer config change (the LLM clients' constructors take only `model`/`temperature`/`topP`/`maxTokens`[/`baseUrl`/`apiKey`]); tracked under TNS-570's corrected scope.
- `tnsai-llm`: **`BedrockClient.streamChat()` now works** (TNS-626) — it was an `UnsupportedOperationException("Streaming not yet implemented")` placeholder in production. Implemented against the Anthropic Messages event stream via a lazily-built `BedrockRuntimeAsyncClient` (the sync client has no event-stream API); `content_block_delta` events are parsed to text and returned as a `Stream<String>`. Buffered for now (gathered before the stream is consumed); true per-token delivery is a follow-up. Claude-3-only, same as `chat()`. [#421](https://github.com/TnsAI-Framework/TnsAI/pull/421)
- `tnsai-llm`: **typed errors for the remaining 18 providers** (TNS-622). Only 13 of the ~31 wired providers shipped a `ProviderErrorMapper`; the other 18 (Cerebras, DashScope, Databricks, DeepInfra, DeepSeek, Fireworks.ai, Hunyuan, llama.cpp, LM Studio, NVIDIA NIM, Perplexity, Replicate, Together.ai, Vertex AI, vLLM, Watsonx, xAI Grok, Yi) fell back to an untyped `LLMException` (`UNKNOWN`, no `ProviderDetails`), so `OnHttpStatus` / `OnErrorType` fallback rules never matched and the chain couldn't fail over on rate-limit/5xx for them. Each now has a mapper (15 share a new `AbstractOpenAICompatibleErrorMapper`; Vertex AI shares `AbstractGoogleErrorMapper` with Gemini; Watsonx/Replicate parse their own envelopes), resolved by the canonical `providerId()` from #423. [#424](https://github.com/TnsAI-Framework/TnsAI/pull/424)
- `tnsai-llm`: **`AbstractOpenAICompatibleClient` no longer double-wraps typed errors** (TNS-636). `chat()`/`streamChat()` re-wrapped every exception into a generic `LLMException` (`UNKNOWN`, no `ProviderDetails`), discarding the typed exception a `ProviderErrorMapper` had produced — latent since TNS-621, it meant the 16 migrated OpenAI-compatible clients silently lost the typed errors #424 added. Typed `LLMException`s now propagate unchanged. [#425](https://github.com/TnsAI-Framework/TnsAI/pull/425)
- `tnsai-llm`: **OpenAI-compatible clients now report real usage tokens** (TNS-639). `AbstractOpenAICompatibleClient.parseChatResponse` didn't read the `usage` block, so the ~18 clients on the base returned empty token counts and `CostAwareLLMClient` fell back to a character-count estimate. It now parses `prompt_tokens`/`completion_tokens`; cost tracking uses the provider's actual counts. `HuggingFaceClient`'s bespoke parse override (its only one) is removed. [#426](https://github.com/TnsAI-Framework/TnsAI/pull/426)

### Migration
- Replace `com.tnsai.identity.AgentSpec` → `com.tnsai.identity.AgentDescriptor` (the `@AgentSpec` annotation is unaffected).
- Replace `@com.tnsai.roles.annotations.RoleIdentity` → `@RoleDeclaration` (the `com.tnsai.models.role.RoleIdentity` class is unaffected).
- The 0.11.1-era orphan-annotation and `@LLM` removals (see Removed) are also breaking; migrate per those notes.

## [0.11.0] - 2026-05-27

Additive release. Closes a batch of annotation ↔ programmatic parity gaps (`@AgentSpec`/`@RoleSpec` builder methods), implements the `@Contract` Design-by-Contract safety primitive (JEXL pre/postconditions + `old(expr)`), consolidates 16 OpenAI-compatible LLM clients onto a shared base (~-3,300 LOC), and adds the `tnsai diagnose` bug-report reproducer CLI. No breaking changes — purely additive on the public API surface.

### Added
- `tnsai-core`: **`@AgentSpec` annotation-parity on `AgentBuilder`** (TNS-534). Six `@AgentSpec` metadata fields had a runtime consumer but no builder counterpart, so programmatic agents silently fell back to defaults. Builder now exposes `description`, `version`, `autoStart`, `idleTimeoutMs`, `did(DIDConfig)`, `groupMembership(GroupMemberSpec)`, with precedence **builder explicit > annotation > default** applied in `AgentInitializer` via new `InitializerContext` override hooks. [#411](https://github.com/TnsAI-Framework/TnsAI/pull/411)
- `tnsai-core`: **`DIDConfig`** (`com.tnsai.identity`) — programmatic counterpart of `@DIDSpec` (`from(annotation)`, `of(method, domain, agentId)`, `toDid(fallbackId)`), so the builder and annotation paths derive identical DIDs. [#411](https://github.com/TnsAI-Framework/TnsAI/pull/411)
- `tnsai-core`: **`GroupMemberSpec.of(String...)`** convenience factory for builder-side group membership. [#411](https://github.com/TnsAI-Framework/TnsAI/pull/411)
- `tnsai-core`: **`RoleBuilder.addAction(ActionConfig)`** — programmatic role actions (TNS-551). Builder-built roles can now register dispatchable actions; previously only `@ActionSpec`-annotated methods on a `Role` subclass worked, so `ConfigurableRole` was capability-less. New `ActionConfig` + `ActionHandler` (`com.tnsai.metadata`); `ActionExecutor` dispatches the handler lambda and `Role.discoverActions` merges them with annotation-discovered actions (duplicate names error). Scoped to `LOCAL` actions. [#414](https://github.com/TnsAI-Framework/TnsAI/pull/414)
- `tnsai-core`: **`@AgentSpec.roles`** (TNS-536) — declare an agent's role classes (`Class<? extends Role>[]`) in the annotation, the counterpart of `AgentBuilder.role(...)`. Instantiated via public no-arg constructor at init; precedence is **programmatic > annotation**. A missing no-arg ctor fails with an actionable `AGENT-V009` message. [#415](https://github.com/TnsAI-Framework/TnsAI/pull/415)
- `tnsai-tools`: **`tnsai diagnose` CLI** (TNS-525) — `com.tnsai.tools.diagnostics` prints a paste-ready environment report for bug reports: `tnsai_version` (lockstep), `jdk` (version/vendor/GC/max heap), `os`, and `providers_configured` (known LLM provider env vars as `set`/`missing`, never the value). Flags `--json` (default), `--issue-template` (GitHub markdown block), `--minimal`, `--no-redact` (secret redaction via the framework `PatternRedactor` is on by default). Adds `.github/ISSUE_TEMPLATE/bug.md` + README "run this first" section. Scoped to the static environment; runtime state (MCP reachability, checkpoint store, OTel traces, log lines) needs a live agent and is a follow-up. [#416](https://github.com/TnsAI-Framework/TnsAI/pull/416)
- `tnsai-core`: **`@Contract` Design-by-Contract enforcement** (TNS-552) — the scaffold annotation (preconditions/postconditions/invariants, zero readers since 2.18.0) is now enforced at action dispatch via a JEXL evaluator. Preconditions reject hallucinated input before the method runs with an LLM-friendly `"precondition violated: <expr>"`; postconditions bind `result` and resolve `old(expr)` to the pre-execution value; invariants run before and after. Honors `validate`/`strict`/`message`. New `com.tnsai.actions.contracts` (`ContractEvaluator`, `ContractValidator`, `ContractViolationException`); adds `commons-jexl3`. Additive to the existing `ActionContract` / `@ActionSpec.precondition` / `@State.invariants` paths. Programmatic `ContractSpec` + build-time syntax validation are follow-ups. [#418](https://github.com/TnsAI-Framework/TnsAI/pull/418)

### Changed
- `tnsai-core`: removed the dead, unused `AgentSpecExtractor.DIDInfo` record (0 callers, verified across the full reactor) — its role is now served by `DIDConfig`, and `AgentSpecExtractor.generateDID` is DRYed through `DIDConfig.toDid`. Internal cleanup; no consumer impact. [#411](https://github.com/TnsAI-Framework/TnsAI/pull/411)
- `tnsai-llm`: **collapsed 16 duplicated OpenAI-compatible provider clients** into a new `AbstractOpenAICompatibleClient` (TNS-621). Each carried a byte-identical ~225-line copy of the chat-completions mapping (`buildChatRequest` / `parseChatResponse` / `extractStreamContent` / `chat` / `streamChat`); a fix had to be applied 16× by hand. Now they shrink to a constructor + base-URL/env-var. Migrated: DeepSeek, Groq, Together.ai, Fireworks.ai, DeepInfra, Cerebras, Databricks, NVIDIA NIM, Perplexity, DashScope, Hunyuan, xAI Grok, Yi, LM Studio, vLLM, llama.cpp. Clients with a divergent wire format (OpenAI, OpenRouter, Mistral, HuggingFace, ZhipuAI, MiniMax) stay on `AbstractLLMClient` — follow-up. **No public-API change**; net ~-3,290 LOC; 1644 tests green. [#417](https://github.com/TnsAI-Framework/TnsAI/pull/417)

## [0.10.5] - 2026-05-18

Same-day follow-up to 0.10.4 — ships the TNS-449 x402 micropayments stack (`tnsai-payments` umbrella + HttpInterceptor primitive + end-to-end `X402PaymentBroker` + EnvKeystoreWallet + mandate enforcement + liability records) and a batch of dependency bumps. Purely additive on the public API front; `tnsai-payments` is a new optional module that consumers opt into. No migration required.

### Added
- `tnsai-mcp`: **`HttpInterceptor` primitive for `HttpTransport`** (TNS-449 P1). Composable interceptor chain on the framework's HTTP transport — auth, rate limit, retry, tracing, and (the immediate driver) x402 payment-aware request mutation. 11 tests pin ordering / short-circuit / chain composition; no new dependency, no API break on `HttpTransport` callers. [#374](https://github.com/TnsAI-Framework/TnsAI/pull/374)
- `tnsai-payments`: **new umbrella module + `PaymentBroker` SPI skeleton** (TNS-449 P2). New optional module — `tnsai-payments` carries the `PaymentBroker` SPI (`quote`, `settle`, `verify`) plus the `x402` protocol-specific package skeleton. Module-graph dep: `tnsai-payments → tnsai-core`. Consumers opt in by adding it to their classpath; core ships with the no-op broker via [#305](https://github.com/TnsAI-Framework/TnsAI/pull/305) (0.10.0). [#375](https://github.com/TnsAI-Framework/TnsAI/pull/375)
- `tnsai-payments`: **`X402PaymentBroker` end-to-end x402 settlement** (TNS-449 P3). Implements the `PaymentBroker` SPI against the x402 HTTP-402 payments protocol over Base USDC. `PaymentRequirement` parses the 402-body wire format; `TransferAuthorization` builds EIP-3009 typed data with inline EIP-712 encoding (skips web3j's heavier `StructuredDataEncoder` for tighter hot-path); `Web3jWallet` wraps secp256k1 `ECKeyPair.sign` for 65-byte `r||s||v` signatures; `X402PaymentBroker.quote()` probes `service.metadata["x402.resource"]`, parses the 402, picks the compatible `PaymentRequirement`, mints a `Quote`; `settle()` builds typed data, signs, replays the request with `X-PAYMENT` (base64-JSON header), returns a sealed `Settlement` variant. Idempotency: `Quote.idempotencyKey` maps deterministically (Keccak-256) to the EIP-3009 nonce, so retried settles return `Settlement.AlreadySettled` rather than double-charging. New dep: `org.web3j:core-crypto` (slice, ~1.5 MB — full web3j was 5 MB). 39 net-new tests, module coverage **87.3%**. Stub `HttpServer` facilitator in-process; no live testnet calls in CI. [#389](https://github.com/TnsAI-Framework/TnsAI/pull/389) (rebased successor to [#377](https://github.com/TnsAI-Framework/TnsAI/pull/377))
- `tnsai-payments`: **`EnvKeystoreWallet` + mandate enforcement + liability records** (TNS-449 P4, partial). `EnvKeystoreWallet.fromEnv(prefix, network)` reads `<PREFIX>_PRIVATE_KEY` from env for a zero-config local wallet (encrypted JSON keystore decryption deferred — needs the full `web3j-core` artifact, kept opt-in for now). `X402Config` gains `liabilitySink` + `authorityScope` optional builder fields. `X402PaymentBroker.settle()` runs mandate enforcement before signing: sums prior `x402.settle` records from the configured `LiabilitySink`, rejects when projected spend > `AuthorityScope.spendCeilingUSD`. Conservative posture — ceiling-set with no sink configured = block. Each terminal Settlement emits one liability record (Settled=MEDIUM, Rejected=HIGH, Expired=LOW); idempotency replays skip emission. 21 net-new tests (9 EnvKeystoreWallet + 12 X402PaymentBroker mandate/liability), module coverage **88.1%**. **Held for separate PR with explicit approval:** `AgentBuilder.paymentBroker(PaymentBroker)` Protected Change. [#390](https://github.com/TnsAI-Framework/TnsAI/pull/390) (rebased successor to [#378](https://github.com/TnsAI-Framework/TnsAI/pull/378))

### Changed
- **Dependency bumps** ([#388](https://github.com/TnsAI-Framework/TnsAI/pull/388) batch + [#333](https://github.com/TnsAI-Framework/TnsAI/pull/333)):
  - `aws.sdk` 2.44.4 → 2.44.7 (patch)
  - `jsoup` 1.22.1 → 1.22.2 (patch)
  - `javalin` 7.1.0 → 7.2.2 (minor)
  - `slf4j` 2.0.17 → 2.0.18 (patch)
  - `junit` 5.14.3 → **6.0.3** (major; we run JDK 21 so compatible)
  - `telegram-bot-api` 8.3.0 → **9.6.0** (major)
  - `angus-mail` 2.0.3 → 2.0.5 (patch)
  - `greenmail-junit5` 2.1.5 → 2.1.8 (patch)
  - `opentelemetry-semconv` 1.41.0 → 1.41.1 (patch)

  No code edits required — pure pom property changes. The two major bumps (`junit` 5 → 6, `telegram-bot-api` 8 → 9) ride the same `mvn verify` reactor; if either surfaces a test regression, the specific bump is reverted in a follow-up patch release.

### Stats
- 12 PRs landed in the release window: TNS-449 stack ([#374](https://github.com/TnsAI-Framework/TnsAI/pull/374), [#375](https://github.com/TnsAI-Framework/TnsAI/pull/375), [#389](https://github.com/TnsAI-Framework/TnsAI/pull/389), [#390](https://github.com/TnsAI-Framework/TnsAI/pull/390)) plus the dependency batch ([#333](https://github.com/TnsAI-Framework/TnsAI/pull/333), [#388](https://github.com/TnsAI-Framework/TnsAI/pull/388) — which closed 8 dependabot PRs).
- New optional module: `tnsai-payments` (~4250 LOC across the four-PR stack, +693 of that in EnvKeystoreWallet + mandate work).
- Module count: 11 → 12 (`tnsai-payments` joins the active set).

## [0.10.4] - 2026-05-18

Channel-stack expansion (Slack/Discord/WhatsApp adapters bring shipped count to six), eighteen-provider LLM catalogue expansion completing the TNS-322 umbrella, `WatsonxClient` IAM-refresh hardening, a multi-agent WS tool-approval routing fix, and a `tnsai-server` Docker image with a multi-arch publish workflow. Monorepo README link cleanup. No public API breakage, no migration required.

### Fixed
- `tnsai-server`: **WS tool-approval routing in multi-agent sessions** (TNS-308). `WsHandler.handleToolApprove` walked the session's `approvalFilters` map but called `filter.handleApproval(toolCallId, decision)` on the first entry and returned regardless of match. In single-agent sessions there is only one filter so the bug never surfaced; in multi-agent sessions (more than one `WsToolApprovalFilter` per session), an approval whose `toolCallId` belonged to a non-first filter was silently dropped and the pending future on the actual owning filter timed out unanswered — surfacing to the user as "I approved but nothing happened." `WsToolApprovalFilter.handleApproval` now returns `boolean` (`true` iff it owned the id and consumed it); `WsHandler` iterates every filter in the session until one returns `true`, then breaks. Added a package-private `registerPendingForTesting` seam so the routing contract is unit-testable without a live WS broadcast. Nine new tests cover the boolean return on match/miss/repeated-id, the multi-agent routing pattern itself, and a `cancelAll` regression guard.

### Added
- `tnsai-channels`: **`WhatsAppChannel` adapter (Cloud API)** (TNS-352). Sixth channel adapter and the **first one that embeds an HTTP server** in `tnsai-channels` — WhatsApp Cloud API delivers inbound events via webhook only (no WebSocket or polling alternative), so the adapter binds a JDK `com.sun.net.httpserver.HttpServer` to a configurable port + path. Inbound: Meta GETs the path with `hub.challenge` at subscription time → we echo iff `hub.verify_token` matches; Meta POSTs JSON events signed with `X-Hub-Signature-256: sha256=<hex>` computed via HMAC-SHA256 of the body under the app secret → we verify in constant time before parsing. Outbound: `POST /{phone_number_id}/messages` to the Graph API with the access token as a Bearer header. Sender-id doubles as conversation-id (WhatsApp is 1:1, no channels). v1 deliberately scopes out media (image/document/audio/voice) because UnifiedResponse's attachment shape doesn't yet model Graph's media-id-then-download flow, and scopes out template messages + 24-hour-window enforcement because templates are a separate API surface with their own approval flow. Loopback bind by default; production runs behind a reverse proxy for TLS. Env: `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_VERIFY_TOKEN`, `WHATSAPP_APP_SECRET` (all required) + `WHATSAPP_GRAPH_BASE_URL`, `WHATSAPP_WEBHOOK_PORT`, `WHATSAPP_WEBHOOK_PATH`, `WHATSAPP_WEBHOOK_BIND_ADDRESS` (all optional). **Architectural note:** the embedded HttpServer is per-adapter — each webhook-based channel binds its own port. If a future second webhook adapter lands (e.g. a Slack Events API variant), the right move is to extract a shared `WebhookReceiver` with path-based routing; YAGNI for now.

### Changed
- `tnsai-llm`: **`WatsonxClient` IAM token refresh hardening** (TNS-501, follow-up to TNS-340 / PR #363). The IAM-token cache had the refresh logic from day one but the safety margin was only 60 seconds — acceptable for steady-state but tight under load. Bumped to **300 seconds** so refresh fires at ~minute 55 of a 60-minute token lifetime, well ahead of expiry, matching the acceptance spec. Concurrent chat calls are guaranteed to collapse to a single exchange via the existing {@code synchronized} guard on {@code getIamToken()}. Token strings are never logged — only the expiry timestamp and the IAM URL appear in debug output. Added an {@code iamTokenUrl()} override seam so tests can route the exchange through {@link mockwebserver3.MockWebServer} without reflection on the cache fields, plus a {@code expireCachedIamTokenForTesting()} hook for deterministic expiry-driven refresh tests. Three new tests: expired-cache-forces-refresh, eight-thread concurrent-refresh collapses to one exchange, and a log-leak assertion that scans every TRACE-level log line for the token string. The refactor only touches {@code WatsonxClient.java} + its test; behaviour is strictly additive (no API changes).

### Added
- `tnsai-channels`: **`DiscordChannel` adapter (Gateway WebSocket)** (TNS-351). Fifth channel after Telegram, CLI, Email, and Slack — and the second WebSocket adapter. Inbound: opens a Discord Gateway v10 connection via `GET /gateway/bot` → WSS, then handles the full handshake (`HELLO` → schedule heartbeats → `IDENTIFY` with token + intents → `READY` captures the bot user_id → `MESSAGE_CREATE` becomes `UnifiedMessage`). Heartbeats run on a single-threaded `ScheduledExecutorService` with the interval Discord supplies in `HELLO`, carrying the last observed `s` sequence number on each beat. Outbound: `POST /channels/{channel_id}/messages` with the `Bot <token>` header and a `message_reference.message_id` when the response targets a specific reply. Bot-echo filtering: messages authored by other bots (or our own bot once `READY` lands) are silently dropped. v1 scopes out **slash commands** (needs `POST /applications/{id}/commands` registration + `INTERACTION_CREATE` handling), **embeds** (UnifiedResponse doesn't carry an embed shape today), and **streaming via message edits** (PATCH per-token is gated by Discord's edit ratelimit) — all named in the issue but deferred to a follow-up so v1 stays focused on the text-DM-and-mention path. Env: `DISCORD_BOT_TOKEN` (required), `DISCORD_APPLICATION_ID` (optional, slash-command forward-compat), `DISCORD_REST_BASE_URL` (optional override), `DISCORD_INTENTS` (optional integer bitfield override; default covers DM + guild messages + the privileged `MESSAGE_CONTENT` intent).
- `tnsai-channels`: **`SlackChannel` adapter (Socket Mode)** (TNS-350). Fourth channel after Telegram, CLI, and Email — and the first one that speaks a real-time WebSocket protocol. Inbound: opens a Socket Mode WebSocket via `POST /apps.connections.open` with the `xapp-...` app token, then routes `events_api` envelopes (and `app_mention` events) into `UnifiedMessage` after acking with the matching `envelope_id`. Outbound: `POST /chat.postMessage` with the `xoxb-...` bot token; thread continuity preserved by carrying the inbound `thread_ts` forward into the response body. Bot-echo and bot-id-tagged messages are filtered to prevent feedback loops; Slack message subtypes (edits, joins, channel renames) are skipped so the agent only sees user-authored text. `SlackChannelConfig` reads `SLACK_BOT_TOKEN` (required) + `SLACK_APP_TOKEN` (required) + `SLACK_WEB_API_BASE_URL` (optional override for mirrors / proxies) from env or system properties, with an explicit programmatic constructor for consumer workspace YAML. **Note on deviation from TNS-350 spec:** the issue called for the HTTP Events API webhook with `SLACK_SIGNING_SECRET`; v1 chose Socket Mode instead because the framework has no embedded HTTP server in `tnsai-channels` and every existing adapter pulls from its platform — Socket Mode keeps that invariant and lets a consumer run behind NAT without exposing a public URL. A signing-secret-verifying `SlackWebhookChannel` sibling can be added later if a use case needs it.
- `tnsai-llm`: **`WatsonxClient` provider** (TNS-340). Twenty-fifth LLM provider — talks to IBM watsonx.ai (enterprise LLM platform) via its `/ml/v1/text/chat` endpoint. The response is OpenAI-shaped but the request body uses IBM-specific fields (`model_id` instead of `model`, plus a required `project_id`), so the request construction diverges from the cookie-cutter OpenAI-shape providers. **Auth via IBM Cloud IAM exchange:** unlike the bearer-token cloud providers, watsonx requires an IAM access token obtained by exchanging an IBM Cloud API key. The client does the exchange lazily on first call against `https://iam.cloud.ibm.com/identity/token`, caches the resulting token until its `expires_in` window closes (minus a 60-second safety margin), and refreshes transparently. Catalog at time of writing: `ibm/granite-3-8b-instruct`, `meta-llama/llama-3-3-70b-instruct`, `mistralai/mistral-large`. Region default `us-south`; override `WATSONX_BASE_URL` for `eu-de` / `jp-tok` / etc. Required env: `WATSONX_API_KEY`, `WATSONX_PROJECT_ID`. Registered in `LLMClientFactory` under `watsonx` and `ibm` aliases.
- `tnsai-llm`: **`DeepInfraClient` provider** (TNS-329). DeepInfra's cost-leader open-model hosting — typically the cheapest hosted Llama-70B option. OpenAI-compatible chat-completions API at `https://api.deepinfra.com/v1/openai` (note the `/v1/openai` suffix — DeepInfra's native API lives at `/v1/inference`; we target the OpenAI shim so the wire format matches every other provider). Catalog includes Llama 3.x, Mixtral 8x7B, Qwen 2.5 72B, DeepSeek V3. Override base URL via `DEEPINFRA_BASE_URL`. Registered in `LLMClientFactory` under `deepinfra` and `deep-infra` aliases. API key via `DEEPINFRA_API_KEY`.
- `tnsai-llm`: **`FireworksAIClient` provider** (TNS-328). Fireworks.ai's open-model hosting + FireFunction (function-calling-tuned) — Llama 3.x, Mixtral, Qwen 2.5, DeepSeek V3, plus `firefunction-v2`. OpenAI-compatible chat-completions API at `https://api.fireworks.ai/inference/v1` (override via `FIREWORKS_BASE_URL` for mirrors / proxies). Same wire format as every other OpenAI-shape provider — structurally identical client. Registered in `LLMClientFactory` under `fireworks`, `fireworksai`, and `fireworks-ai` aliases. API key via `FIREWORKS_API_KEY`.
- `tnsai-llm`: **`TogetherAIClient` provider** (TNS-326). Together.ai's open-model hosting — Llama 3.x (8B / 70B / 405B Turbo), Mixtral 8x7B, Mistral 7B, Qwen 2.5 72B, DeepSeek V3, Nemotron-tuned Llama, and the rest of the serverless catalogue. OpenAI-compatible chat-completions API at `https://api.together.xyz/v1` (override via `TOGETHER_BASE_URL` for mirrors / proxies). Same wire format as `GroqClient` / `NvidiaNIMClient` / `XAIGrokClient` / `CerebrasClient` — structurally identical client. Registered in `LLMClientFactory` under `together`, `togetherai`, and `together-ai` aliases. API key via `TOGETHER_API_KEY`.
- `tnsai-llm`: **`CerebrasClient` provider** (TNS-327). Cerebras's WSE-3 hosted inference — top-of-industry token throughput (reportedly 1800+ tok/s on Llama 70B). OpenAI-compatible chat-completions API at `https://api.cerebras.ai/v1` (override via `CEREBRAS_BASE_URL` for mirrors / proxies). Catalog at time of writing: `llama-3.3-70b`, `llama3.1-8b`, `qwen-3-32b`. Same wire format as `GroqClient` / `NvidiaNIMClient` / `XAIGrokClient` — structurally identical client. Registered in `LLMClientFactory` under `cerebras`. API key via `CEREBRAS_API_KEY`. Headline use case: latency-sensitive agent inner loops (tool-call coordination, REPL chat, realtime UI agents).
- `tnsai-llm`: **`VertexAIClient` provider** (TNS-325). Twenty-seventh LLM provider — **completes the TNS-322 LLM provider expansion umbrella** (18 new providers shipped across two sessions). Talks to Google Cloud Vertex AI (enterprise Gemini) via its native `generateContent` endpoint at `https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{PROJECT}/locations/{LOCATION}/publishers/google/models/{MODEL}:generateContent`. Wire format is the Gemini shape (`contents[].parts[]`, `systemInstruction`, `generationConfig`) with OpenAI-style `assistant` history roles mapped to Gemini's `model` role. **Auth (v1 scope):** takes a pre-fetched OAuth access token via `VERTEX_AI_API_KEY` (e.g. `gcloud auth print-access-token` in dev, sidecar-managed in prod). Application Default Credentials (ADC) — service-account JWT signing, GCE metadata-server probe — is a deliberate follow-up since it pulls in `google-auth-library-java` or hand-rolled RS256 crypto. Required env: `VERTEX_AI_API_KEY`, `VERTEX_AI_PROJECT_ID`. Optional: `VERTEX_AI_LOCATION` (default `us-central1`; host derived from it), `VERTEX_AI_BASE_URL` (full override). Registered in `LLMClientFactory` under `vertexai`, `vertex-ai`, and `vertex` aliases.
- `tnsai-llm`: **`ReplicateClient` provider** (TNS-331). Twenty-sixth LLM provider — talks to Replicate (community model marketplace) via its native predict/poll HTTP API at `https://api.replicate.com/v1`. Unlike every other provider in this module, Replicate is **not OpenAI-shaped** — the request takes an arbitrary `input` object and the response is delivered through a submit-then-poll lifecycle. **v1 scope:** sync chat only, targeting the common chat-model case (Llama, DeepSeek, Mixtral). Model identifier `owner/name` auto-resolves the latest version; `owner/name:version` pins. System prompt + history + user message concatenated into a single `{"prompt": "..."}` input (de facto convention for chat models on Replicate). Output handles both single-string and string-array shapes. Polling uses exponential backoff (500ms → 30s, 10-minute overall timeout). **Documented limitations:** no streaming (`streamChat` emits the final answer as a single chunk), no tool calling. Env: `REPLICATE_API_KEY` (Replicate's docs call it `REPLICATE_API_TOKEN`; this module standardises on `_API_KEY`). Registered in `LLMClientFactory` under `replicate` alias.
- `tnsai-llm`: **`DeepSeekClient` provider** (TNS-324). Twenty-fourth LLM provider — talks to DeepSeek (Chinese frontier lab) via its OpenAI-compatible chat-completions endpoint at `https://api.deepseek.com/v1`. Catalog at time of writing: `deepseek-chat` (V3, ~$0.14/M input / $0.28/M output — very cost-efficient), `deepseek-reasoner` (R1, reasoning model). **Known v1 limitation:** when the model is `deepseek-reasoner`, responses include a `reasoning_content` field carrying R1's visible chain-of-thought trace alongside the standard `content` text. The shared `ChatResponse`/`ChatChunk` SPI doesn't yet carry a reasoning channel, so the trace is dropped — R1 still answers correctly, callers just don't see the trace. Plumbing reasoning content through is a follow-up SPI extension. Registered in `LLMClientFactory` under `deepseek` alias. API key via `DEEPSEEK_API_KEY`.
- `tnsai-llm`: **`LMStudioClient` provider** (TNS-333). Sixteenth LLM provider — talks to a locally-running LM Studio desktop server via its OpenAI-compatible chat-completions endpoint at `http://localhost:1234/v1` (override via `LMSTUDIO_BASE_URL` for LAN rigs or proxies). Like `OllamaClient`, the API key is optional: when unset the `Authorization` header is omitted entirely, matching LM Studio's ungated-by-default local server; when set (e.g. for an LM-Studio instance behind a reverse-proxy with basic auth) the key is sent as `Bearer <key>`. Model name comes from the loaded model in the LM Studio UI — discover via `GET /v1/models`. Streaming, tool calls, and topP follow the same wire format as the other OpenAI-shape providers. Registered in `LLMClientFactory` under `lmstudio` and `lm-studio` aliases.
- `tnsai-llm`: **`PerplexityClient` provider** (TNS-330). Twenty-third LLM provider — talks to Perplexity's search-augmented Sonar family via its OpenAI-compatible chat-completions endpoint at `https://api.perplexity.ai`. Sonar models fetch web results inline and ground answers in real-time sources. Catalog at time of writing: `sonar` (fast / cheap), `sonar-pro` (higher quality), `sonar-reasoning` (chain-of-thought over search), `sonar-deep-research` (multi-hop). **Known v1 limitation:** Perplexity responses include a `citations[]` array alongside the standard OpenAI text content; the shared `ChatResponse` SPI doesn't yet carry citation metadata, so citations are dropped — text-only answer is returned. Surfacing citations is a follow-up `ChatResponse` extension. Registered in `LLMClientFactory` under `perplexity` and `pplx` aliases. API key via `PERPLEXITY_API_KEY`.
- `tnsai-llm`: **`DatabricksClient` provider** (TNS-339). Twenty-second LLM provider — talks to Databricks Mosaic AI Model Serving via its OpenAI-compatible Foundation Model API at the customer's workspace-scoped URL. Unlike the multi-tenant cloud providers, Databricks endpoints live per-customer at `https://{workspace}.cloud.databricks.com/serving-endpoints`, so there is no sensible default — every caller must supply `DATABRICKS_BASE_URL` (or the constructor `baseUrl` argument). Built-in catalog at time of writing: `databricks-meta-llama-3-3-70b-instruct`, `databricks-meta-llama-3-1-405b-instruct`, `databricks-dbrx-instruct`, `databricks-mixtral-8x7b-instruct`; customers can also point this client at their own fine-tuned endpoints by passing the endpoint name as `model`. Auth via `DATABRICKS_API_KEY` (PAT or service- principal OAuth token, both passed as opaque bearer); native service-principal OAuth flows can land as a follow-up. Registered in `LLMClientFactory` under `databricks` and `mosaic` aliases.
- `tnsai-llm`: **`QwenCloudClient` provider** (TNS-335). Twenty-first LLM provider — talks to Alibaba's DashScope service (the hosted API for the Qwen family) via its OpenAI-compatible chat-completions endpoint. Two regional defaults: international at `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` (the client's default) and China at `https://dashscope.aliyuncs.com/compatible-mode/v1` (select via `DASHSCOPE_BASE_URL` env var or constructor `baseUrl` argument). Catalog at time of writing: `qwen-max` (frontier), `qwen-plus` (balanced), `qwen-turbo` (low-latency), `qwen2.5-coder-32b-instruct` (coding), `qwen-vl-max` (multimodal). Streaming, tool calls, and topP all follow the standard OpenAI-shape wire format. Registered in `LLMClientFactory` under `qwen`, `dashscope`, and `alibaba` aliases. API key via `DASHSCOPE_API_KEY` (single-token "DashScope" label in `requireApiKey` to satisfy the env-var pairing test).
- `tnsai-llm`: **`LlamaCppServerClient` provider** (TNS-334). Twentieth LLM provider — talks to `llama-server` (the HTTP server binary from the llama.cpp project) via its OpenAI-compatible chat-completions endpoint at `http://localhost:8080/v1` (override via `LLAMACPP_BASE_URL`). Mirrors `OllamaClient` / `LMStudioClient` / `VLLMClient`: API key is optional — `Authorization: Bearer <key>` is sent only when `LLAMACPP_API_KEY` (or constructor `apiKey`) is set. Model name comes from the GGUF file that llama-server's `--model` flag loaded. llama.cpp's small footprint makes it the lightest possible LLM server — runs on Raspberry Pi 5-class ARM hardware and Apple Silicon laptops equally well; pairs naturally with edge-class sandbox profiles where Ollama and vLLM are too heavy. Registered in `LLMClientFactory` under `llamacpp`, `llama-cpp`, and `llama.cpp` aliases.
- `tnsai-llm`: **`VLLMClient` provider** (TNS-332). Nineteenth LLM provider — talks to a self-hosted vLLM inference server (typically invoked as `python -m vllm.entrypoints.openai.api_server`) via its OpenAI-compatible chat-completions endpoint at `http://localhost:8000/v1` (override via `VLLM_BASE_URL` for remote inference rigs). Mirrors `OllamaClient` / `LMStudioClient`: API key is optional — `Authorization: Bearer <key>` is sent only when `VLLM_API_KEY` (or constructor `apiKey`) is set, supporting vLLM instances started with `--api-key` or behind auth proxies. Model name comes from vLLM's `--model` flag; discover via `GET /v1/models`. Streaming, tool calls, and topP follow the standard OpenAI-shape wire format. Registered in `LLMClientFactory` under `vllm` alias.
- `tnsai-llm`: **`TencentHunyuanClient` provider** (TNS-336). Eighteenth LLM provider — talks to Tencent's Hunyuan family via its OpenAI-compatible chat-completions endpoint at `https://api.hunyuan.cloud.tencent.com/v1`. Catalog at time of writing: `hunyuan-pro` (frontier), `hunyuan-standard` (balanced), `hunyuan-lite` (cheap / fast), `hunyuan-vision` (multimodal). Streaming, tool calls, and topP all follow the same wire format as the other OpenAI-shape providers. Registered in `LLMClientFactory` under `hunyuan`, `tencent`, and `tencent-hunyuan` aliases. API key via `HUNYUAN_API_KEY`. Tencent Cloud SigV3 signing on the parallel native API path is deliberately not implemented — the bearer-token OpenAI-compatible surface keeps the wire format identical to every other provider in this module.
- `tnsai-llm`: **`YiClient` provider** (TNS-337). Seventeenth LLM provider — talks to 01.AI (Kai-Fu Lee's lab) via its OpenAI-compatible chat-completions API at `https://api.lingyiwanwu.com/v1`. Catalog at time of writing: `yi-large` (frontier), `yi-large-turbo` (cheaper / faster variant), `yi-lightning` (low-latency small-batch), `yi-vision` (multimodal). Streaming, tool calls, and topP all follow the same wire format as the other OpenAI-shape providers — structurally identical client. Registered in `LLMClientFactory` under `yi`, `01ai`, and `01.ai` aliases. API key via `YI_API_KEY`.
- `tnsai-llm`: **`XAIGrokClient` provider** (TNS-323). Fifteenth LLM provider — talks to xAI's Grok models via the OpenAI-compatible chat-completions API at `https://api.x.ai/v1` (override via `XAI_BASE_URL` for mirrors / proxies). Catalog at time of writing: `grok-3`, `grok-3-mini`, `grok-2-vision`, plus the gated `grok-beta` pre-release tier. Streaming, tool calls, and topP all follow the same wire format as `GroqClient` / `NvidiaNIMClient` — structurally identical client. Registered in `LLMClientFactory` under `xai`, `grok`, and `xai-grok` aliases. API key via `XAI_API_KEY`.
- `tnsai-channels`: **`EmailChannel` adapter (IMAP poll + SMTP send)** (TNS-354). Third channel after Telegram + CLI, the first async one. IMAP polling on a configurable interval (default 60s) marks UNSEEN messages SEEN as they're processed; thread continuity tracked through `Message-ID` / `In-Reply-To` / `References` headers — replies in the same thread land on the same `conversationId`. Outbound SMTP replies set `In-Reply-To` and `Re:`-prefix the subject. `EmailChannelConfig` is env-var-driven (`EMAIL_IMAP_HOST`, `EMAIL_IMAP_USER`, `EMAIL_IMAP_PASSWORD`, `EMAIL_SMTP_HOST`, …) with explicit programmatic override for a consumer's per-workspace YAML. Sender allowlist is mandatory — empty allowlist drops everything, since email is the most spammable channel. Jakarta Mail deps declared `<optional>true</optional>` to match the Telegram pattern; consumers opt in by adding them to their classpath. GreenMail-backed integration tests cover allowlist, thread continuity, attachment parsing, and outbound headers.
- `tnsai-llm`: **`NvidiaNIMClient` provider** (TNS-338). Fourteenth LLM provider — talks to NVIDIA NIM (NVIDIA Inference Microservices) via the OpenAI-compatible chat-completions API on both deployment shapes: the hosted catalog at `https://integrate.api.nvidia.com/v1` (default) and any self-hosted NIM container via the `baseUrl` constructor parameter or `NVIDIA_BASE_URL` env var. Cloud catalog covers Llama 3.1 (8B / 70B / 405B), Mixtral 8x7B, Mistral 7B, and NVIDIA's own Nemotron-4 340B + Llama-3.1-Nemotron-70B tunes. Streaming, tool calls, and topP follow the same wire format as `GroqClient` / `OpenRouterClient`. Registered in `LLMClientFactory` under `nvidia`, `nvidia-nim`, and `nim` aliases. API key via `NVIDIA_API_KEY`.

### Added (ops)
- `tnsai-server`: **Docker image + multi-arch publish workflow** (TNS-520). Multi-stage `Dockerfile` (Maven 3.9 + Eclipse Temurin 21 → distroless `gcr.io/distroless/java21-debian12:nonroot`) ships a self-contained `tnsai-server` JAR runnable with `docker run`. The JVM is PID 1 so `SIGTERM` reaches the existing `Runtime.addShutdownHook()` drain path; image defaults to `TNSAI_HOST=0.0.0.0` + `TNSAI_ALLOW_PUBLIC=true` but deliberately leaves `TNSAI_TOKEN` unset — operators must supply a token before exposing to anything other than a private network. New `/healthz` + `/readyz` route aliases (Kubernetes-conventional) added *additively* alongside the existing `/health/live` + `/health/ready` endpoints, sharing the same handler lambdas. `maven-shade-plugin` lives in an opt-in `docker` profile so `mvn install` for downstream consumers stays fast and Maven Central isn't polluted with a `-shaded` classifier. New `.github/workflows/docker-publish.yml` smoke-tests on every `v*` tag (boots the image, polls `/healthz` for 30s, verifies SIGTERM-driven graceful shutdown) then publishes multi-arch (`linux/amd64` + `linux/arm64`) to Docker Hub. Skipped for forks. Requires `DOCKERHUB_USERNAME` + `DOCKERHUB_TOKEN` repo secrets. [#385](https://github.com/TnsAI-Framework/TnsAI/pull/385)

### Docs
- **Monorepo consolidation link cleanup** (TNS-513). Module READMEs still pointed at the deprecated split-repo URLs (`TnsAI.Core`, `TnsAI.LLM`, …) retired in the April 2026 consolidation — those repos no longer exist, so every reference rendered as a 404 on GitHub. Forty link refs + bare-prose mentions across nine module READMEs rewritten to monorepo paths (`tnsai-core`, `tree/main/tnsai-X`). Refs to external repos outside this framework were preserved. [#386](https://github.com/TnsAI-Framework/TnsAI/pull/386)
- **`handoff/` vs context-snapshot disambiguation** (TNS-117). Coordination module docs clarify the distinction between explicit agent handoffs (`HandoffStrategy`) and implicit context snapshots taken on session continuation — no behaviour change. [#370](https://github.com/TnsAI-Framework/TnsAI/pull/370)

### Internal
- `tnsai-core`: **`PaymentBroker` SPI record test coverage** (TNS-518). Three new test files in `com.tnsai.payment` (`SettlementTest`, `QuoteTest`, `ServiceTest`) pin every validation invariant on the shared SPI records — sealed-variant exhaustiveness on `Settlement`, null-checks + blank-string rejection + `priceUSD ≥ 0` + `expiresAt > issuedAt` on `Quote`, defensive-copy isolation on `Service.metadata`. 31 tests, all green. No production code touched. [#376](https://github.com/TnsAI-Framework/TnsAI/pull/376)

### Stats
- 9 PRs landed in the release window (#366, #367, #368, #369, #370, #371, #376, #385, #386). The bulk LLM-provider expansion that finishes the TNS-322 umbrella was already in `Unreleased` carried from the 0.10.3 window.

## [0.10.3] - 2026-05-14

Same-week follow-up to 0.10.2 — extends `ProjectTools` with two new `@Tool` methods aligned to the agents.md spec so agents can route on structured project context (sections, intro) instead of an opaque blob, and can bootstrap a fresh `AGENTS.md` from a project's build system. Purely additive, no public API breakage, no migration required.

### Added
- `tnsai-tools`: **`ProjectTools.agentsmdParse` + `agentsmdGenerate`** (TNS-399 axis 1). Two new `@Tool` methods on the `PROJECT_TOOLS` toolkit. `agentsmd_parse` returns a structured `AgentsMdContent` record (`intro` + ordered `sections: [{level, title, body}]`) parsed from `AGENTS.md`, with case-variant + `CLAUDE.md` + `README.md` fallback — letting agents route on individual sections (e.g. pull just "Setup") rather than treating the document as an opaque blob. `agentsmd_generate` produces a draft `AGENTS.md` by detecting the build system from `pom.xml` / `package.json` / `pyproject.toml` / `Cargo.toml` / `go.mod` and filling in language-appropriate setup + test commands — returns the markdown string, the caller decides whether to write it. [#343](https://github.com/TnsAI-Framework/TnsAI/pull/343)

### Stats
- 1 PR landed in the release window for new public surface (#343).
- 4 supporting doc PRs (#339, #340, #341, #342) refreshed module CLAUDE.md / README / per-module AGENTS.md without touching public API.

## [0.10.2] - 2026-05-13

Same-week follow-up to 0.10.1 — adds a streaming-capable channel adapter mixin (the structural fix the TNS-438 REPL polish workaround had been waiting on) plus a CI workflow defensive fix. Purely additive, no public API breakage, no migration required.

### Added
- `tnsai-channels`: **`StreamingChannelAdapter` mixin interface + `UnifiedChunk` record** (TNS-440). Lets adapters opt into per-token delivery without changing the existing `ChannelAdapter` contract. `UnifiedChunk` carries `conversationId`, `delta`, `done` flag, free-form `metadata` (tool-call markers etc.), and `timestamp`. The mixin extends `ChannelAdapter` so any `StreamingChannelAdapter` is also a regular adapter — gateway code can `instanceof`-check and dispatch chunks via `sendChunk(...)` as they arrive, then still call `send(UnifiedResponse)` once with the assembled reply for non-streaming downstreams (logging, audit). Adapters that don't implement the mixin keep working unchanged. [#335](https://github.com/TnsAI-Framework/TnsAI/pull/335)
- `tnsai-channels`: **`CliChannel` now implements `StreamingChannelAdapter`** with `capabilities().streaming() = true`. REPL mode emits the `assistant: ` prefix once on the first chunk then concatenates deltas inline; JSON mode emits one `{"type":"chunk","content":"...","done":...}` record per chunk. The post-stream `send(UnifiedResponse)` is suppressed (the reply was already rendered chunk-by-chunk); the `suppressNextSend` state resets after one consumption so subsequent standalone `send()` calls render normally. [#335](https://github.com/TnsAI-Framework/TnsAI/pull/335)

### Fixed
- **CI**: artifact upload steps in `.github/workflows/build.yml` now use `continue-on-error: true` and 3-day retention (down from 7). Previously, a GitHub Actions free-tier storage quota hit would mark the whole `Build & Test` job red even though `mvn verify` had passed. Soft-fail makes the build status reflect code health, not artifact store availability. [#336](https://github.com/TnsAI-Framework/TnsAI/pull/336)
- **Release tooling**: `make release` CHANGELOG extract now uses literal-substring match instead of regex, so versions with `.` (every version) extract correctly (TNS-419). [#324](https://github.com/TnsAI-Framework/TnsAI/pull/324)

### Stats
- `tnsai-channels`: 128 → 146 tests (+18). 2 new public types (`StreamingChannelAdapter`, `UnifiedChunk`).
- 3 PRs (#324, #335, #336). 1 additive feature + 2 infra fixes.
- Reactor `mvn verify` 13/13 PASS.

## [0.10.1] - 2026-05-08

Same-day follow-up to 0.10.0 — purely additive observability + evaluation surface and a second channel adapter. No public API breakage, no migration required. Released under the `0.x` patch policy (`0.X.Y → 0.X.Y+1` for additive + bugfix + chore).

### Added
- `tnsai-channels`: **CLI channel adapter** (`com.tnsai.channels.cli`) — second `ChannelAdapter` after Telegram. Two modes: REPL (interactive `> ` prompt with `/exit` `/quit` `/clear` local slash commands intercepted, everything else flows to the gateway as a `UnifiedMessage`) and JSON (newline-delimited `{"text":"..."}` in / `{"type":"text","content":"..."}` out for scripting). SPI-discoverable via `META-INF/services`, mode selected from a config string (`"json"` case-insensitive → JSON, default REPL). Closes TNS-353 Phase 1+2. [#317](https://github.com/TnsAI-Framework/TnsAI/pull/317), [#318](https://github.com/TnsAI-Framework/TnsAI/pull/318)
- `tnsai-quality`: **OTLP-native LLMCallLog exporter** (`OtlpLLMCallExporter implements LLMCallPublisher`) — every captured `LLMCallLog` now flows to any OpenTelemetry collector (Langfuse, LangWatch, Phoenix, Honeycomb, Tempo, Loki) via the [GenAI semconv](https://opentelemetry.io/docs/specs/semconv/gen-ai/) wire shape. One `CLIENT` span per call (`chat <model>` / `chat_stream <model>`), three metrics (`gen_ai.client.token.usage` long counter partitioned by `gen_ai.token.type`, `gen_ai.client.cost.usd` + `gen_ai.client.operation.duration` histograms). Cardinality discipline: 7 ctx fields on the span, only `tenant + role` on the metric (others would explode the metric series). Retroactive timing — `setStartTimestamp(call.startedAt())` + `span.end(call.completedAt())` so dashboard duration matches captured elapsed even for post-hoc spans. Closes TNS-374. [#319](https://github.com/TnsAI-Framework/TnsAI/pull/319)
- `tnsai-quality`: **Sampling + Redacting decorators for `LLMCallPublisher`** — companion to the existing `Sampling*` / `Redacting*Publisher` pair on `AgentEventPublisher`. `SamplingLLMCallPublisher` reuses the `EventSamplingPolicy` SPI by mapping each `LLMCallLog` into a `SamplingInput` (eventKind `"llm.called"`, level `ERROR` when `isFailure()` else `INFO` so `ErrorAlwaysPolicy` passes failures regardless of nominal sample rate). `RedactingLLMCallPublisher` scrubs every leaky surface: `prompt.systemPrompt` / `prompt.messages` / `prompt.parameters` (`LLM_PROMPT` scope) and `response.content` / `response.toolCalls[].arguments` / `response.reasoningContent` / `error.errorMessage` / `providerExtensions` (`LLM_RESPONSE` scope). Tool names pass through (framework metadata). Composition: `new SamplingLLMCallPublisher(new RedactingLLMCallPublisher(otlp, redactor), policy)` — redaction inside, sampling outside, so dropped events skip the redactor cost. Closes TNS-417, completes TNS-374 acceptance #3. [#321](https://github.com/TnsAI-Framework/TnsAI/pull/321)
- `tnsai-evaluation`: **Agent-tier evaluators** (`com.tnsai.evaluation.evaluators.agent`) — four new metrics that score the *trace* rather than just the final response. `PlanAdherenceEvaluator` (LLM judge, "did you stick to the declared plan?"), `StepEfficiencyEvaluator` (deterministic, `expected / max(expected, actual)`, "did you reach the goal without burning extra tool calls?"), `ToolCorrectnessEvaluator` (LLM judge, "were the right tools picked?"), `TaskCompletionEvaluator` (LLM judge, "did the final response solve the task?"). Shared package-private `JudgeScoreParser` mirrors `GEvalEvaluator.extractScore` generalised to any `[min, max]` range; returns `-1` on no match so callers fail explicitly instead of guessing a middle value. Closes TNS-373. [#320](https://github.com/TnsAI-Framework/TnsAI/pull/320)

### Fixed
- `tnsai-quality`: typo in two test method names — `nullCallProapagates` → `nullCallPropagates`. Cosmetic, JUnit method-name agnostic. [#322](https://github.com/TnsAI-Framework/TnsAI/pull/322)
- `tnsai-channels`: stale `@since 0.9.4` on `CliChannel` → `@since 0.10.1`. Author wrote the tag pre-0.10.0 cut; next release after 0.10.0 is 0.10.1. [#318](https://github.com/TnsAI-Framework/TnsAI/pull/318)
- `tnsai-evaluation` + `tnsai-quality`: 8 stale `@since 0.10.2` Javadoc tags → `@since 0.10.1` (anticipated wrong version on TNS-373 and TNS-417). Cosmetic, no API change.

### Stats
- 6 PRs (#317 → #322), purely additive — no BREAKING items, no `Changed`, no `Removed`.
- Reactor `mvn verify` 13/13 PASS — quality 1357 → ~1357, channels 106 → 128, evaluation 277 → 322. Test count up from 10357 → 10458 (+101).
- Module dep graph delta: `tnsai-quality → tnsai-llm` (new, TNS-374) — one-way edge, no cycle (tnsai-llm only depends on tnsai-core).

PR: [release: 0.10.0 → 0.10.1](https://github.com/TnsAI-Framework/TnsAI/pull/323)

## [0.10.0] - 2026-05-08

The reliability + safety platform release. Five new capability layers land together — durable idempotency stores, checkpoint/resume primitives, agent identity + accountability + payment SPIs, on-demand modular knowledge (skills), and unified sandbox execution — alongside framework-wide cost governance (rate-limit + budget hooks at the LLM boundary) and server-side hardening (five-layer security). Two BREAKING changes drive the minor bump per the `0.x breaking → minor` policy: accountability wiring is now explicit (no silent no-op fallback in `AgentBuilder`), and `logback-classic` moves to test-scope only (consumers choose their own SLF4J binding). Process improvements ship in the same window: a nightly reactor build catches time-bomb tests + cross-module drift before consumers hit them, and the PR template enforces reactor verification when public surfaces change.

### Added
- `tnsai-quality`: idempotency keys with pluggable persistence — Redis (Lettuce) and Postgres (JDBC) `IdempotencyStore` implementations, MCP `idempotentHint` flag wired through tool-call routing. Closes TNS-224. [#301](https://github.com/TnsAI-Framework/TnsAI/pull/301)
- `tnsai-quality`: rate-limit + budget hooks at the LLM call boundary — token-bucket rate limiter, USD spend budget tracker, configurable per-agent / per-tenant. Closes TNS-210. [#302](https://github.com/TnsAI-Framework/TnsAI/pull/302)
- `tnsai-core`: checkpoint + resume + idempotent retry primitives — `CheckpointStore` SPI, in-memory default, automatic snapshot on agent state transitions, replay-safe retry. Closes TNS-299. [#303](https://github.com/TnsAI-Framework/TnsAI/pull/303)
- `tnsai-quality`: durable `CheckpointStore` implementations — Redis (Lettuce) for fast volatile checkpoints, S3 (AWS SDK v2) for cold long-term snapshots. Closes TNS-312. [#304](https://github.com/TnsAI-Framework/TnsAI/pull/304)
- `tnsai-core`: agent identity + accountability + payment SPIs — `AgentIdentity` (DID + cryptographic key), `AccountabilityLog` (signed event chain), `PaymentRail` (x402 / settlement abstraction). Closes TNS-298. [#305](https://github.com/TnsAI-Framework/TnsAI/pull/305)
- `tnsai-core`: on-demand modular knowledge layer — `@Skill` annotation, `SkillActivationEvent` (added to `TnsAIEvent` sealed hierarchy), lazy skill loading via SPI, runtime skill discovery. Closes TNS-289. [#307](https://github.com/TnsAI-Framework/TnsAI/pull/307)
- `tnsai-quality`: unified file/doc guardrails — sandbox execution + size limits + extension whitelist, applied uniformly to file-write and document-export tools. Closes TNS-342. [#308](https://github.com/TnsAI-Framework/TnsAI/pull/308)
- `tnsai-quality`: `Sandbox` SPI — isolated execution primitive (process / container / WASM strategies), pluggable resource limits. Closes TNS-296. [#309](https://github.com/TnsAI-Framework/TnsAI/pull/309)
- `tnsai-quality`: code review pipeline harness — pluggable, idempotent, deepsec pattern; routes proposed code changes through configurable checks before commit. Closes TNS-291. [#312](https://github.com/TnsAI-Framework/TnsAI/pull/312)
- `tnsai-server`: five-layer security hardening — auth, rate-limit, input validation, output redaction, audit log on every request. Closes TNS-302. [#313](https://github.com/TnsAI-Framework/TnsAI/pull/313)
- CI: nightly reactor build (`.github/workflows/nightly-reactor.yml`) — `mvn verify` on `main` daily at 06:00 UTC, surfaces time-bomb tests and cross-module compile drift before consumers hit them. [#314](https://github.com/TnsAI-Framework/TnsAI/pull/314)
- Process: `PULL_REQUEST_TEMPLATE.md` with mandatory reactor-build checkbox when Protected Changes / sealed-type permits are touched, plus a downstream-PR section linking consumer repos. [#314](https://github.com/TnsAI-Framework/TnsAI/pull/314)

### Changed
- **BREAKING:** `tnsai-core`: `AgentBuilder.accountability(...)` is now mandatory when `@Accountable` is on the agent — the silent no-op shim is removed. Builder fails fast with a configuration error if accountability isn't wired. Closes TNS-298 follow-up. [#306](https://github.com/TnsAI-Framework/TnsAI/pull/306)
- `tnsai-tools`: Python and JavaScript execution tools migrated from per-tool isolation to the shared `Sandbox` SPI — single hardening surface, consistent resource limits across languages. Closes TNS-343. [#311](https://github.com/TnsAI-Framework/TnsAI/pull/311)

### Removed
- **BREAKING:** `tnsai-core`: `logback-classic` moved from compile-scope to test-scope only. Consumers now pick their own SLF4J binding (logback / log4j2 / slf4j-simple) — no transitive logback pulled into application classpaths. Closes TNS-309. [#315](https://github.com/TnsAI-Framework/TnsAI/pull/315)
- `tnsai-core`: no-op accountability fallback shims (`NoOpAccountabilityLog`, `NoOpPaymentRail`) — replaced by explicit-wire failure mode. [#306](https://github.com/TnsAI-Framework/TnsAI/pull/306)

### Fixed
- `tnsai-intelligence`: `ContradictionDetector` time-bomb fixed — `Clock` is now injected so tests can pin time; the `FUTURE = NOW + 30d` constant no longer leaks wall-clock dependency into CI. Closes TNS-341 (CI broke 2026-05-07T12:00 UTC when the original constant expired). [#310](https://github.com/TnsAI-Framework/TnsAI/pull/310)

### Migration

**Accountability (TNS-298 follow-up):** if your agent declares `@Accountable`, wire the SPI explicitly in the builder:

```java
AgentBuilder.create()
    .accountability(new MyAccountabilityLog())  // required — no implicit fallback
    .build();
```

If you don't need accountability, drop the `@Accountable` annotation. Builder will fail at build time with a clear error message if the annotation is present but no log is wired.

**Logback (TNS-309):** add an SLF4J binding to your application's runtime classpath. Logback consumers add it explicitly:

```xml
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.5.13</version>
    <scope>runtime</scope>
</dependency>
```

Or pick `log4j2-slf4j2-impl` / `slf4j-simple` if your stack uses those. The framework no longer assumes a binding.

### Stats
- 15 PRs (#301 → #315), 5 new capability layers, +24449 / −629 LOC across 265 files in 13 modules.
- Reactor `mvn verify` 13/13 PASS — 0 failures, 0 errors.
- BREAKING change count: 2 (TNS-298 accountability strict wiring, TNS-309 logback test-scope).
- Process: nightly reactor + PR template land in this release; first nightly run scheduled 2026-05-09 06:00 UTC.

PR: [release: 0.9.3 → 0.10.0](https://github.com/TnsAI-Framework/TnsAI/pull/316)

## [0.9.3] - 2026-05-06

Closes the `@ToolExample` silent-drop chain across all LLM providers — examples now reach the model on Anthropic (`input_examples` field), OpenAI / Gemini / 8 OpenAI-passthrough providers (description fold with `EXAMPLES:` / `AVOID:` sections), Bedrock-Claude (via the extracted Anthropic converter), and Cohere (flat parameter shape + fold). System-prompt prose additionally renders examples under `## Available Actions` so the model sees them when reasoning about a role overall, not only at tool-call time. Constraint rendering format also unified across the three prompt builders: positives-first, header-once (`Must always:` / `Must never:` blocks rather than repeated `- Must always:` / `- Must never:` prefix per rule). Purely additive — no API breakage, no migration required.

### Added
- `tnsai-llm`: Bedrock (Claude 3 family) and Cohere now support `@ToolExample` end-to-end. Bedrock routes via the extracted `AnthropicToolConverter`; Cohere has its own `CohereToolConverter` (JSON-Schema → flat `parameter_definitions` shape, types translated `string→str` / `integer→int` / `number→float` / `boolean→bool` / `array→list` / `object→dict`). [#297](https://github.com/TnsAI-Framework/TnsAI/pull/297)
- `tnsai-core`: `@ToolExample` now renders in the system-prompt prose under each action's `## Available Actions` block. Positive examples appear under `Examples:`, negatives under `Avoid (anti-patterns):`. Wired through both `RolePromptBuilder` (in-process role) and `SystemPromptBuilder` (SCOP bridge). [#299](https://github.com/TnsAI-Framework/TnsAI/pull/299)
- `tnsai-core`: `ActionMetadata.getExamples()` accessor — returns the combined positive + negative example list in declaration order. [#299](https://github.com/TnsAI-Framework/TnsAI/pull/299)
- `tnsai-core`: `com.tnsai.prompt.format.PromptFormat` — shared formatter for prompt-building call sites. `renderConstraints` (`mustAlways` / `mustNever`) and `renderExamples` (`@ToolExample`). Used by `RolePromptBuilder`, `RoleSpecReader`, and `SystemPromptBuilder` (SCOP). [#298](https://github.com/TnsAI-Framework/TnsAI/pull/298), [#299](https://github.com/TnsAI-Framework/TnsAI/pull/299)

### Changed
- `tnsai-core` / `tnsai-integration`: constraint block rendering switched from per-bullet repeated prefix (`- Must always: <rule>` / `- Must never: <rule>`) to header-once (`Must always:` / `Must never:` block headers with indented bullets). Positives now render before negatives. User-observable in generated system prompts; the format change drops ~200 prompt tokens per typical 5-action × 4-rule role. The negatives-first ordering of 0.9.2 is gone — positives set the tone, negatives draw the boundary. [#298](https://github.com/TnsAI-Framework/TnsAI/pull/298)

### Fixed
- `tnsai-llm`: `@ToolExample` annotations on tool methods are no longer silently dropped on the Anthropic provider. Positives are mapped into the native `input_examples` field on each tool definition; negatives are folded into the tool description as an `AVOID:` section (Anthropic's tool API has no first-class anti-pattern field). [#291](https://github.com/TnsAI-Framework/TnsAI/pull/291)
- `tnsai-llm`: `@ToolExample` annotations no longer silently dropped on OpenAI and Gemini. Both providers receive examples folded into the function description as `EXAMPLES:` (positives) and `AVOID:` (negatives) sections — neither provider has a native examples API. [#292](https://github.com/TnsAI-Framework/TnsAI/pull/292)
- `tnsai-llm`: `@ToolExample` annotations no longer silently dropped on the 8 OpenAI-passthrough providers (Mistral, Groq, OpenRouter, Ollama, HuggingFace, Azure OpenAI, MiniMax, ZhipuAI — both chat and multimodal sites). Same description-fold strategy as OpenAI/Gemini. [#296](https://github.com/TnsAI-Framework/TnsAI/pull/296)

### Documentation
- `@ToolExample` Javadoc now cites the Anthropic source for the "72% → 90% accuracy on complex parameter handling" claim ([Introducing advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use)) instead of a bare assertion. Also fixes outdated `@since` tag (`2.14.0` template-artefact → `0.3.0`) and a broken `@see Action` cross-reference (now `@see ActionSpec`). [#295](https://github.com/TnsAI-Framework/TnsAI/pull/295)
- `tnsai-core/README.md`: annotation count refreshed `100+` → `98` (verified via `grep -rh "public @interface"`) on both prose and feature-table sites. [#293](https://github.com/TnsAI-Framework/TnsAI/pull/293)
- 8 module READMEs (core, llm, mcp, tools, intelligence, coordination, integration, quality): per-module `LICENSE` link now correctly resolves to the monorepo-root `LICENSE` file (`(LICENSE)` → `(../LICENSE)`) — submodules don't carry their own LICENSE files post-monorepo. [#294](https://github.com/TnsAI-Framework/TnsAI/pull/294)

### Stats
- 8 PRs, ~1900 lines added across `tnsai-llm` (Anthropic / Bedrock / Cohere / 8 passthrough converters), `tnsai-core` (PromptFormat helper), and Javadoc / README polish.
- 44+ new test cases — `ToolExampleConverterTest` (52 cases for Anthropic + OpenAI/Gemini fold), `AnthropicToolConverterTest` (8 cases), `CohereToolConverterTest` (9 cases), `PromptFormatTest` (16 cases — 10 constraints + 6 examples).
- All 13 modules build green (`mvn verify` reactor 13/13 PASS); no behavioural regressions.

## [0.9.2] - 2026-05-06

Removes `@ActionSpec.invariants[]` — the vestigial third bucket that consistently became a misuse magnet for behavioral rules belonging in `mustAlways` / `mustNever`. Three buckets where two had clear semantic homes turned the third into a dumping ground; rules wound up double-rendered in system prompts (token waste + LLM ambiguity over which list to honour). With this cut the framework's per-action constraint surface collapses to two intents — *do* (`mustAlways`) and *don't* (`mustNever`) — plus the orthogonal Hoare-triple pair (`precondition` / `postcondition`) and state-level `@State.invariants`. Net delta: 12 files, +32 / -233 lines. BREAKING — released as a **patch** under user pragma rather than the strict `0.x breaking → minor` rule (parent CLAUDE.md), given the removed field's low real-world usage and the trivial mechanical migration (drop the annotation parameter or move its strings into `mustAlways` / `mustNever`).

### Removed
- **BREAKING:** `@com.tnsai.annotations.ActionSpec.invariants()` — the `String[]` field is gone. Move strings to `mustAlways` (positive obligations) or `mustNever` (negative prohibitions).
- **BREAKING:** `ActionMetadata.invariants` field + `hasInvariants()` + `getInvariants()` accessors.
- **BREAKING:** `ContractConfig.invariants` field — record arity drops 6 → 5.
- **BREAKING:** `ActionExecutor` before/after method-level invariants check; only `checkPrecondition` / `checkPostcondition` / `checkStateInvariants` remain on the action lifecycle.
- **BREAKING:** `InvariantCheckerHandle.checkActionInvariants(Method)` SPI method.
- **BREAKING:** `InvariantChecker.checkActionInvariants(Method)` impl + references in `checkBeforeAction` / `checkAfterAction` / `collectViolations`.
- **BREAKING:** `RoleSpecReader.setInvariants(...)` / `getInvariants()` + private field.
- **BREAKING:** `RoleSpecExtractor.ResponsibilityInfo.invariants()` record component + extraction line.
- **BREAKING:** `SystemPromptBuilder.appendActionsSection` "Invariant: ..." render block (the 5 lines added in 0.9.0 / PR #284 — that addition surfaced the misuse magnet, this removal closes it).
- Tests covering removed paths.

### Kept (different concerns, valid use)
- `@State.invariants` — Gaia state predicates evaluated by `InvariantChecker.checkStateInvariants()` after any state change.
- `@ActionSpec.precondition` / `postcondition` — Hoare-triple Method contracts; still wired through `ActionExecutor`.
- `@ActionSpec.fulfills` / `effects` — planning subsystem coordinates.
- `@Contract.invariants` — different annotation, contract-by-design layer.

### Migration

| Concern | Use this field |
|---|---|
| Behavioral obligation ("agent must …") | `@ActionSpec.mustAlways` |
| Behavioral prohibition ("agent must never …") | `@ActionSpec.mustNever` |
| State predicate (field-level invariant) | `@State.invariants` |
| Method postcondition | `@ActionSpec.postcondition` |
| Method precondition | `@ActionSpec.precondition` |

Mechanical sweep: `grep -r "@ActionSpec(.*invariants\s*=" .` should return 0 hits after migration.

### Versioning note
Strict rule per parent `CLAUDE.md` (`0.x breaking → minor`) would have called for 0.10.0; user pragma chose 0.9.2 patch given the removed field's low real-world usage and the trivial migration. Future BREAKING removals will revert to the strict rule unless explicitly noted.

### Stats
12 files changed · +32 / -233 lines · `@ActionSpec.invariants` consumer references in framework: 0 (sweep verified).

PR: [#289](https://github.com/TnsAI-Framework/TnsAI/pull/289).

## [0.9.1] - 2026-05-06

Re-cut of 0.9.0 (which was rejected by Sonatype Central Portal as "component already exists" — earlier release.yml attempts had partially staged 0.9.0 artifacts that couldn't be cleared without manual portal UI work). Identical content to the originally-planned 0.9.0; version bumped to 0.9.1 as the cleanest forward path.

PR: [#release-recovery](https://github.com/TnsAI-Framework/TnsAI/commits/main).

## [0.9.0] - 2026-05-06 (UNRELEASED — superseded by 0.9.1)

Consolidates three overlapping abstractions named "Responsibility" into per-action constraints on `@ActionSpec`. Every safety constraint now lives on the action it applies to — the action method becomes the natural locus for traceability, audit, and rendering. Action-attributed constraints flow into the system prompt as bullets under each action; downstream exporters (JSON / YAML / Jason) emit them with action attribution. BREAKING — bumps minor because the `@Responsibility` / `@Responsibilities` annotations, the `Responsibility` model interface, `Role.getResponsibilities()`, and `RoleBuilder.responsibility(...)` / `mustNever(...)` / `mustAlways(...)` are removed. Also lands LLM call granular capture (issue #79 phase 1) — typed `LLMCallLog` events with USD cost + stream metrics + `EventContext` attribution — so consumers can route per-call observability into LangFuse / Helicone / Phoenix or custom cost trackers.

### Added
- **`@ActionSpec.mustNever()` / `mustAlways()`** — `String[]` arrays declaring per-action safety constraints. Rendered into the system prompt as `- Must never: ...` / `- Must always: ...` bullets under each action's signature, alongside its description and type marker. Closes [#283](https://github.com/TnsAI-Framework/TnsAI/issues/283).
- **`ActionMetadata.getMustNever()` / `getMustAlways()` + `hasMustNever()` / `hasMustAlways()`** — runtime accessors for the new constraints; consumed by `RolePromptBuilder`, `RoleSpecExtractor.extractResponsibilitiesFromActions`, and the SCOP `SystemPromptBuilder`.
- **`RoleSpecExtractor.ResponsibilityInfo.mustNever()` / `mustAlways()`** — extra fields on the auto-extracted record so exporters can render constraints with action attribution (`actionName: constraint`).
- **LLM call granular capture — Phase 1 (#79)** — typed `LLMCallLog` events emitted per LLM invocation, decoupled from the legacy raw-string `LLMObserver`. Wires the `LLMCallLog` record (already shipped as data-shape-only in 0.5.0) into a working publish path so consumers can route prompt + response + token usage + USD cost + stream metrics + `EventContext` into LangFuse / Helicone / Phoenix dashboards or custom cost trackers.
  - **`LLMCallPublisher`** (SPI) — single-method `publish(LLMCallLog)`; `NOOP` is the framework default.
  - **`Slf4jLLMCallPublisher`** — default opt-in publisher; one structured SLF4J line per call (no raw prompt/response text — that is intentionally behind redaction SPI #80).
  - **`CapturingLLMClient`** — decorator wrapping any `LLMClient`. Captures success and failure paths; for streaming, captures TTFT + chunk count. Multimodal `chat(List<ContentPart>)` and `streamChatWithSpec` route through the delegate without capture in this PR — separate hot-path refactor.
  - **`JsonLLMPricingRegistry`** — loads rate cards from classpath JSON. Ships `/pricing/2026-05.json` with rates for `openai/gpt-4o`, `openai/gpt-4o-mini`, `openai/o1-preview`, `anthropic/claude-sonnet-4`, `anthropic/claude-opus-4`, and an `ollama/*` wildcard (zero — local). Other 7 framework providers' rate cards land incrementally.
  - **`ToolSurfaceHasher`** — single canonical entry point that turns the framework's raw `List<Map<String,Object>>` tool shape into a `ToolSurface` with a stable SHA-256 hash. Sorted-key Jackson serialisation so identical tool sets across calls correlate (prompt-cache friendly).
  - **`(provider, model)` cost attribution via `LLMCallLog.context()`** — every captured event carries the active `EventContext` (tenant / agent / role / capability / session / group), so consumers downstream can attribute USD cost along any dimension already wired by issue #78.
- **18 new tests** covering CapturingLLMClient (success / error / streaming / cost / context paths) + JsonLLMPricingRegistry (default registry + wildcard + missing-resource).
- **LLM rate cards — Phase 2 (#79)** — `pricing/2026-05.json` expanded from 3 providers / 6 models to 7 providers / 13 models, so the `cost` field on `LLMCallLog` is populated for the major providers a TnsAI agent is likely to be wired against. Adds: `anthropic/claude-haiku-4-5`, `google/gemini-2.0-flash`, `google/gemini-2.0-pro`, `mistral/mistral-large`, `mistral/mistral-small`, `groq/llama-3.3-70b-versatile`, `groq/mixtral-8x7b-32768`, `cohere/command-r-plus`, `cohere/command-r`. Providers without prompt caching (`groq`, `cohere`) declare `cached_per_1k: null`. 10 new tests (1099 total in `tnsai-llm`).

### Removed
- **BREAKING:** `@com.tnsai.annotations.Responsibility` annotation (used inside `@RoleSpec.responsibilities`).
- **BREAKING:** `@com.tnsai.roles.annotations.Responsibilities` annotation + nested `Duty`, `SafetyConstraint`, `Severity` types.
- **BREAKING:** `com.tnsai.models.role.Responsibility` model interface + `CoreDuty` / `SafetyProperty` / `Responsibilities` (container) implementations.
- **BREAKING:** `com.tnsai.enums.role.SafetyType` enum (only consumed by deleted classes).
- **BREAKING:** `Role.getResponsibilities()` abstract template method + `Role.responsibilities()` accessor + `Role.getMustNeverConstraints()` / `getMustAlwaysConstraints()`.
- **BREAKING:** `RoleBuilder.responsibility(...)`, `responsibilities(...)`, `duty(...)`, `mustNever(...)`, `mustAlways(...)` — fluent API surface for role-level safety constraints. Use `@ActionSpec` annotations on a `Role` subclass instead.
- **BREAKING:** `@RoleSpec.responsibilities()` field.
- **BREAKING:** `RolePromptBuilder.generateResponsibilitiesSection(...)`, the second `buildMinimalRolePrompt(identity, responsibilities)` overload (only identity is needed now).
- **BREAKING:** `RoleSpecExtractor.extractResponsibilities(...)` / `hasResponsibilitiesAnnotation(...)`. Use `extractResponsibilitiesFromActions(...)` for the action-bound replacement.
- **BREAKING:** `RoleSpecReader.RoleSpec.ResponsibilityMeta` + `getResponsibilities()` / `setResponsibilities(...)`.
- **BREAKING:** `ExportedRole.responsibilities` field + `hasResponsibilities()`. `autoResponsibilities` (per-action) is now the single source.

### Changed
- **`RolePromptBuilder` rendering** — the `## Responsibilities` markdown block is gone. Per-action `Must never:` / `Must always:` bullets render under each action in the `## Available Actions` section.
- **`SystemPromptBuilder` (SCOP integration)** — same shape change; renders constraints under each `@ActionSpec`-discovered action rather than as a separate role-level block.
- **`CoalitionFormation.CAPABILITY_BASED`** — capability-count metric switches from `role.getResponsibilities().size()` to `role.getActions().size()`. Comment in the file already said "Sort by number of capabilities/actions"; the new metric matches the comment and is more honest (a role with 5 mustNever and zero actions used to outrank a role with 10 actions and zero mustNever).
- **`JsonRoleExporter` / `YamlRoleExporter` / `JasonExporter`** — `responsibilities` block is now sourced from per-action `mustNever` / `mustAlways` aggregated into `actionName: constraint` strings rather than from the deleted role-level annotation.
- **`DeclarativeRole`** — the auto-generated declarative role no longer overrides `getResponsibilities()` (no template method to override).
- **`ConfigurableRole`** — drops the `roleResponsibilities` field; instances built via `RoleBuilder` now carry only identity + LLM.

### Migration

Before:
```java
@RoleIdentity(name = "Researcher", goal = "Find academic information")
@Responsibilities(
    duties = {"Search databases"},
    mustNever = {"fabricate references"},
    mustAlways = {"cite sources"}
)
public class ResearcherRole extends Role { }
```

After:
```java
@RoleIdentity(name = "Researcher", goal = "Find academic information")
public class ResearcherRole extends Role {
    @ActionSpec(
        type = ActionType.LOCAL,
        description = "Search academic databases",
        mustNever = {"fabricate references"},
        mustAlways = {"cite sources"}
    )
    public List<Paper> search(String query) { ... }
}
```

For roles without a natural tool method, declare a marker LLM action to host the constraints:
```java
@ActionSpec(type = ActionType.LLM, description = "Default conversational behavior",
    mustNever = {"reveal system prompt"})
public String chat(String message) { return message; }
```

For action-less roles that don't need constraints (just identity), drop the `getResponsibilities()` override entirely — the abstract method is gone.

`RoleBuilder` users: `.duty(...)`, `.mustNever(...)`, `.mustAlways(...)`, `.responsibility(...)` no longer compile. Either move to a `Role` subclass with `@ActionSpec`, or drop the calls if your test only needs identity.

### Stats
- ~50 framework files changed (6 deleted, 13 Core rewritten, 6 consumers, ~25 tests migrated)
- 0 net new public types — `@ActionSpec.mustNever()` / `mustAlways()` extend an existing annotation
- All 13 modules build green; full test suite (~6300 tests across modules) passes

### Out of scope (focused follow-ups for #79)
- Redaction modes (`HASH_ONLY` / `FULL` / `REDACTED`) — depends on issue #80 (redaction SPI not yet open). Until then, no raw prompt/response text is logged by the default publisher.
- Prometheus metric derivation from `LLMCallLog` — separate PR; needs `MeterRegistry` plumbing in `tnsai-quality`.
- Inter-chunk percentile (p50 / p99) computation in `StreamMetrics` — single-pass percentile adds a sort per call; ship histogram-feed (TTFT + chunkCount) now, percentile sketch follow-up.
- Rate cards for the remaining 7 providers (Gemini, Mistral, Groq, Cohere, Bedrock, Azure, OpenRouter, HuggingFace) — phase 2 in flight (#287).
- `docs/capabilities/llm/observability.md` page + integration test (100 calls × 3 providers).
- `AgentBuilder.captureLLMCalls(...)` opt-in helper — once builder API ergonomics are signed off.

PRs: [#284](https://github.com/TnsAI-Framework/TnsAI/pull/284) (consolidation), [#285](https://github.com/TnsAI-Framework/TnsAI/pull/285) (LLMCallLog phase 1), [#287](https://github.com/TnsAI-Framework/TnsAI/pull/287) (LLMCallLog phase 2 — rate cards)

## [0.8.6] - 2026-05-04

#85 epic complete: all four spinoff validators (V004 / V006 / V011 / V012) have shipped, so every validator from the original #85 design is now in the pipeline. Idempotency story moves from primitives-only (PR #108) to a working tool-call dedup loop with HTTP `Idempotency-Key` injection on the WEB_SERVICE path. Release CI gains three independent gates from #203 (preflight, dry-deploy validation, downstream drift check) so half-bumped releases can no longer reach Maven Central. Backward-compatible, purely additive.

### Added
- **AGENT-V004 declarative capability check** — `@RoleSpec(requires = {LLMCapability.STREAMING, LLMCapability.VISION, ...})` field surfaces when a configured LLM doesn't support a capability the role declares. Severity `WARNING` (soft-launch). New `com.tnsai.enums.LLMCapability` enum (`STREAMING` / `STRUCTURED_OUTPUT` / `VISION`; `FUNCTION_CALLING` intentionally NOT here — already covered by V003 via tool-presence). Co-located with V003 in `LLMCapabilityValidator` (shared introspection prologue, independent emit). Closes [#238](https://github.com/TnsAI-Framework/TnsAI/issues/238). PR: [#246](https://github.com/TnsAI-Framework/TnsAI/pull/246).
- **AGENT-V011 MCP server reachability check** — opt-in (`withReachabilityChecks(true)`) reachability probe for every `@MCPTool(serverUrl = ...)` URL referenced by an agent's actions. Runs the actual MCP `initialize` handshake at build time. New `com.tnsai.spi.McpClientFactory` SPI in `tnsai-core` + `DefaultMcpClientFactory` adapter in `tnsai-mcp` (auto-discovered via `META-INF/services`). Validator graceful-degrades when the adapter is absent. Closes [#237](https://github.com/TnsAI-Framework/TnsAI/issues/237). PRs: [#244](https://github.com/TnsAI-Framework/TnsAI/pull/244), [#252](https://github.com/TnsAI-Framework/TnsAI/pull/252).
- **AGENT-V012 tenant scope validator (Phase 1 + 2)** — `AgentBuilder.tenantId(String)` declares per-tenant agent scope; `com.tnsai.spi.TenantAware` is a marker SPI consumers' `MemoryStore` implementations opt into to advertise tenant safety. `TenantScopeValidator` fires `WARNING` when `tenantId` is set but the wired store is not `TenantAware` (or is the build-time default `InMemoryStore`). Suppressible via `.relaxValidation("AGENT-V012")` (e.g. for one-process-per-tenant deployments). Closes [#235](https://github.com/TnsAI-Framework/TnsAI/issues/235). At this release, runtime tenant propagation, tool-side hooks, and audit-event tenant ids remained follow-up work; 0.14.0 later added runtime `TenantContext` propagation while tool and audit integration remain separate. PRs: [#248](https://github.com/TnsAI-Framework/TnsAI/pull/248), [#250](https://github.com/TnsAI-Framework/TnsAI/pull/250).
- **`@Idempotent` wired into `ToolMethodDispatcher`** — the primitives shipped in [PR #108](https://github.com/TnsAI-Framework/TnsAI/pull/108) (annotation, SPI, key derivation, in-memory store, exception) now have an active call site. New `IdempotencyResolver` orchestrator handles all four `KeyStrategy` values + all three `RetryBehavior` policies + failure caching opt-in + store unavailability. New `IdempotencyKeySupplier` opt-in interface for `KeyStrategy.EXPLICIT`. Tools without `@Idempotent` bypass the resolver entirely (zero overhead, per-`Method` cache for reflection-free dispatch). PR: [#251](https://github.com/TnsAI-Framework/TnsAI/pull/251).
- **`Idempotency-Key` HTTP header injection on `WEB_SERVICE` actions** — `WebServiceExecutor` injects the same key the resolver uses internally onto outgoing `POST` / `PUT` / `PATCH` / `DELETE` requests when the action is `@Idempotent`. Stripe / SendGrid / Twilio / GitHub upstream-side dedup activates alongside the framework's client-side cache. Safe methods (`GET` / `HEAD` / `OPTIONS`) get the cache but not the header. `IdempotencyResolver.deriveKey` promoted to `public static` so the header path and the cache path land on the same key (EXPLICIT suppliers must be deterministic — called twice per call). PR: [#253](https://github.com/TnsAI-Framework/TnsAI/pull/253).
- **Release CI hardening (3 of 7 #203 items)** — \`release.yml\` now runs three independent gates before the Maven Central deploy step:
  - **Preflight** ([#254](https://github.com/TnsAI-Framework/TnsAI/pull/254), #203 item 2) — same `scripts/release-preflight.sh` `make preflight VERSION=X.Y.Z` runs locally; verifies tag exists, root pom version matches tag, all 13 poms lockstep, CHANGELOG section header present, no duplicate tag at the same commit.
  - **Dry-deploy validation** ([#255](https://github.com/TnsAI-Framework/TnsAI/pull/255), #203 item 4) — `mvn deploy -P release` to a local file repo + per-module artifact validation (poms + jars + sources + javadocs + signatures) before the immutable Central deploy.
  - **Downstream drift check** ([#256](https://github.com/TnsAI-Framework/TnsAI/pull/256), #203 item 7) — clones the downstream consumer repos and grep-scans for stale-version markers; surfaces "shipped 0.8.6 but a consumer still says 0.8.5" before the release advances.

### Changed
- `tnsai-core/agent_docs/validation.md` — "Spinoffs" section narrowed to "Resolved spinoffs" (all four originally-spun-off validators now ship); shipped-validators table goes from 9 to 12 entries; `LLMCapabilityValidator` row clarified "(FC head)" / "(declared head)" to disambiguate V003 + V004 sharing one class. PRs: [#244](https://github.com/TnsAI-Framework/TnsAI/pull/244), [#246](https://github.com/TnsAI-Framework/TnsAI/pull/246), [#250](https://github.com/TnsAI-Framework/TnsAI/pull/250).
- `IdempotencyResolver.deriveKey` — promoted from `private` to `public static`. Necessary so the HTTP header injection path in `WebServiceExecutor` derives the same key the resolver's internal cache lookup uses; otherwise the header value and the cache lookup would diverge for `KeyStrategy.HASH_INPUT`. PR: [#253](https://github.com/TnsAI-Framework/TnsAI/pull/253).

### Fixed
- **Bare `<NNN` patterns escaped in 0.8.5 CHANGELOG entry** — `<100ms` and `<500ms` parsed as MDX tag-opens by `fumadocs-mdx` in the downstream changelog sync, breaking the Next.js build for the entire 0.8.5 downstream PR. Wrapped in backticks; consumer-side render now matches plain-Markdown intent. Future-proofing: this CHANGELOG entry follows the same backtick discipline for any threshold expressions. PR: [#249](https://github.com/TnsAI-Framework/TnsAI/pull/249).

### Stats
- 11 commits since `v0.8.5`
- All 12 `#85` design validators now shipped (was 9): `AGENT-V004`, `AGENT-V006`, `AGENT-V011`, `AGENT-V012` previously deferred, all in
- 1 new `tnsai-core` SPI (`McpClientFactory`)
- 1 new `tnsai-core` SPI marker (`TenantAware`)
- 1 new `tnsai-core` enum (`LLMCapability`, 3 values)
- 1 new `tnsai-mcp` adapter (`DefaultMcpClientFactory` via `ServiceLoader`)
- 4 new `AgentBuilder` setters (`tenantId`, `toolCallFilter` was 0.8.5 — adding `tenantId` here)
- 3 new release-pipeline gates (preflight + dry-deploy + drift)
- ~150 new tests across `IdempotencyResolverTest` (24) + `ToolMethodDispatcherIdempotencyTest` (14) + `WebServiceExecutorIdempotencyTest` (14) + `LLMCapabilityValidatorV004Test` (14) + `MCPServerReachabilityValidatorTest` + integration tests + `AgentBuilderTenantIdTest` (9) + `TenantScopeValidatorTest` (6) + `TenantScopeIntegrationTest` (7) + `DefaultMcpClientFactoryTest` (10)

## [0.8.5] - 2026-05-04

AGENT validator family one step closer to #85 acceptance: V006 (ToolApproval gate) ships alongside the `@Tool.requiresConfirmation` + `AgentBuilder.toolCallFilter` primitives it gates on. Phase 3 closeout for #85 ships the `Set<String>` relaxValidation overload, the `<100ms` SLA pinning perf test, and first-class validator docs. Also contains a transitive `slf4j-simple` leak from `tnsai-evaluation` that was hijacking consumer-side logback configuration. Backward-compatible, purely additive.

### Added
- **AGENT-V006 ToolApprovalValidator** — fires when at least one registered `@Tool` POJO has `requiresConfirmation = true` AND no `ToolCallFilter` has been wired through `AgentBuilder`. Suppressible via `.relaxValidation("AGENT-V006")`. Severity `WARNING` (soft-launch). Closes [#234](https://github.com/TnsAI-Framework/TnsAI/issues/234). PR: [#243](https://github.com/TnsAI-Framework/TnsAI/pull/243).
- **`@Tool.requiresConfirmation()`** — boolean annotation field for tool authors to declare a runtime safety gate. Independent of `@Tool.idempotent()` (retry hint) and `ToolRiskLevel` (gradient classification) — the existing `ToolRiskLevel` javadoc already pre-referenced this field as "the boolean gate". PR: [#243](https://github.com/TnsAI-Framework/TnsAI/pull/243).
- **`AgentBuilder.toolCallFilter(ToolCallFilter)`** — pre-build setter parallel to the existing post-build `Agent.setToolCallFilter()`. Wired via the same pending-pattern used for `KnowledgeBase` so `build()` stays lazy. Null filter clears the slot symmetrically with the post-build setter. PR: [#243](https://github.com/TnsAI-Framework/TnsAI/pull/243).
- **`AgentBuilder.relaxValidation(Set<String>)`** — bulk suppression overload for the common CI pattern of skipping multiple validation codes in one call. Eager null-entry rejection so typos surface at the configuration site, not silently passing every issue through. PR: [#241](https://github.com/TnsAI-Framework/TnsAI/pull/241).
- **`ValidationPipelinePerformanceTest`** — pins the `<100ms` SLA from #85 acceptance ("typical agent validates in `<100ms` (static checks only)"). Asserts `<500ms` with 5× margin for CI runner jitter; locally completes in 10–30ms. PR: [#241](https://github.com/TnsAI-Framework/TnsAI/pull/241).
- **`tnsai-core/agent_docs/validation.md`** — first-class consumer-facing docs covering all 9 shipped validators, the `Healthcheckable` SPI with implementation contract, opt-in reachability + per-probe timeout, suppression model (single + bulk), performance SLA, `AgentValidationException` shape, and the 3 deferred validators with their blocker issues. PR: [#241](https://github.com/TnsAI-Framework/TnsAI/pull/241).

### Fixed
- **`slf4j-simple` no longer leaks at compile scope** from `tnsai-evaluation` to consumers. The dep was declared without an explicit `<scope>`, defaulting to `compile` and racing the consumer's logback binding for the SLF4J "one binding per JVM" slot. Now `<scope>test</scope>` — test JVM still has a binding (277 evaluation tests stay green), the published artifact's transitive graph no longer carries it. Inline `<!-- ... -->` comment added explaining the regression context so a future copy-paste / cleanup doesn't silently undo the fix. Closes [#240](https://github.com/TnsAI-Framework/TnsAI/issues/240). PR: [#242](https://github.com/TnsAI-Framework/TnsAI/pull/242).

### Stats
- 3 commits since `v0.8.4`
- 1 new public annotation field (`@Tool.requiresConfirmation`)
- 1 new `AgentBuilder` setter (`toolCallFilter`)
- 1 new `AgentBuilder` overload (`relaxValidation(Set<String>)`)
- 1 new AGENT validator (V006); 9 of 12 #85 validators now shipped (was 8) — 3 spinoffs remaining: V004 (#238), V011 (#237), V012 (#235)
- 1 perf test pinning #85's `<100ms` SLA
- ~25 new tests across V006 + relaxValidation overload
- 1 published-artifact regression contained (slf4j-simple at compile scope)

## [0.8.4] - 2026-05-03

Multimodal toolkit completion + safety-gate plumbing + AGENT validator expansion. Backward-compatible, purely additive — no removals, no behaviour changes. Headlines: image generation (DALL-E 3 / FLUX / Stability), audio generation (ElevenLabs / Cartesia / Deepgram + Whisper alternatives), `ChannelScopedId` typed identity, prompt-injection scanning for project-context files, and two new build-time validators (AGENT-V003 / V005).

### Added
- **Image generation toolkit** — single function-shape POJO `ImageGenTools` with three `@Tool` methods (`dalle3_generate`, `flux_generate`, `stability_generate`) so the LLM can pick provider at call time by cost/latency/quality. Uniform `{provider, model, urls[, revised_prompt]}` envelope; Stability binary response decoded to `data:image/png;base64,…` URI for shape parity. New `BuiltInTool.IMAGE_GEN_TOOLS` enum entry. PR: [#221](https://github.com/TnsAI-Framework/TnsAI/pull/221) (closes [#93](https://github.com/TnsAI-Framework/TnsAI/issues/93) Phase 1).
- **Audio generation toolkits** — two POJOs (`TextToSpeechTools` + `SpeechToTextTools`) covering the canonical non-OpenAI alternatives to `MediaTools`. TTS: ElevenLabs Multilingual v2/Turbo/Flash, Cartesia Sonic-2 (latency leader), Deepgram Aura (cheapest). STT: Deepgram Nova-2 (sync), AssemblyAI Universal-2 (async with internal poll loop, 5-min hard cap), OpenAI Whisper large-v3 hosted on Replicate (FLUX-key reuse). New `BuiltInTool.TEXT_TO_SPEECH_TOOLS` + `SPEECH_TO_TEXT_TOOLS` entries. PR: [#222](https://github.com/TnsAI-Framework/TnsAI/pull/222) (closes [#93](https://github.com/TnsAI-Framework/TnsAI/issues/93) Phase 2).
- **`ChannelScopedId` value type** in `tnsai-channels` — typed `(channelId, senderId)` record replacing the ad-hoc `channelId + ":" + senderId` string concat. Compact constructor refuses a separator-bearing channelId; `parse()` splits on the first colon so a senderId with internal colons (Slack `T123:U456`) round-trips losslessly. `UnifiedMessage.scopedId()` convenience helper added (parallel to existing `sessionKey()` — sender-scope vs conversation-scope). PR: [#224](https://github.com/TnsAI-Framework/TnsAI/pull/224) (closes [#19](https://github.com/TnsAI-Framework/TnsAI/issues/19) Phase 1).
- **Prompt-injection scan for project-context files** — `PromptInjectionDetector.detectInProjectContext(content, source)` adds a context-only pattern set targeting attack vectors that don't make sense in regular chat: SSH-key dumps, `.env` reads, `~/.aws/credentials` reads, env-var dumps, `curl` POSTs of secret files, `display:none`/`visibility:hidden`/white-on-white HTML, zero-width-character payloads, HTML-comment overrides. New `ContextFileSource` enum (TNSAI_MD / CLAUDE_MD / AGENTS_MD / README_MD / OTHER) tags audit-log entries. New `InjectionType` enum values: `CREDENTIAL_EXFILTRATION` and `HIDDEN_INSTRUCTION`. PR: [#228](https://github.com/TnsAI-Framework/TnsAI/pull/228) (closes [#35](https://github.com/TnsAI-Framework/TnsAI/issues/35)).
- **AGENT validator family expansion** — `LLMCapabilityValidator` (AGENT-V003, function-calling capability check) and `Healthcheckable` SPI + reachability validation infra (AGENT-V005). PRs: [#233](https://github.com/TnsAI-Framework/TnsAI/pull/233), [#236](https://github.com/TnsAI-Framework/TnsAI/pull/236) (advances [#85](https://github.com/TnsAI-Framework/TnsAI/issues/85)).
- **Release-pipeline hardening Phase A** — `make preflight` (5-check gate: tag exists, root pom version, 13 module poms lockstep, CHANGELOG section, no duplicate tag), `make drift-check` (downstream stale-version scan), japicmp Maven plugin in the `quality` profile. PR: [#206](https://github.com/TnsAI-Framework/TnsAI/pull/206) (closes [#203](https://github.com/TnsAI-Framework/TnsAI/issues/203) Phase A).

### Changed
- **Surefire migrated to explicit Mockito `-javaagent`** — Java 24+ removes the silent agent-loading path, so the test runner now declares the agent jar by full path. Standardises across all 13 modules. PR: [#225](https://github.com/TnsAI-Framework/TnsAI/pull/225).
- **JaCoCo coverage gate added for `#9` protected classes** — five test-coverage waves landed (FormatAwareOutputParser, CompositeResilienceStrategy + ComposedResilienceStrategy, InMemoryMessageBroker + Message factories, ContextSnapshot + Builder, GroupTask record + Builder + lifecycle), and the gate ratchets coverage so it cannot regress. PRs: [#226](https://github.com/TnsAI-Framework/TnsAI/pull/226), [#227](https://github.com/TnsAI-Framework/TnsAI/pull/227), [#229](https://github.com/TnsAI-Framework/TnsAI/pull/229), [#230](https://github.com/TnsAI-Framework/TnsAI/pull/230), [#231](https://github.com/TnsAI-Framework/TnsAI/pull/231), [#232](https://github.com/TnsAI-Framework/TnsAI/pull/232) (advances [#9](https://github.com/TnsAI-Framework/TnsAI/issues/9)).
- **Dependency bumps** (Dependabot wave) — jackson group (3 updates), micrometer-registry-prometheus 1.3.1 → 1.16.5, mongodb-driver-sync 5.2.1 → 5.7.0, jakarta.mail-api 2.1.3 → 2.1.5, playwright 1.58.0 → 1.59.0, graalvm 25.0.2 → 25.0.3, jacoco-maven-plugin 0.8.12 → 0.8.14, spotbugs-maven-plugin 4.8.6.5 → 4.9.8.3, central-publishing-maven-plugin 0.7.0 → 0.10.0, maven-plugins group (6 updates), ci-actions (checkout 4→6, setup-java 4→5, upload-artifact 4→7, action-gh-release 2→3).

### Stats
- 29 commits since `v0.8.3`
- 5 new public types (`ChannelScopedId`, `ImageGenTools`, `TextToSpeechTools`, `SpeechToTextTools`, `ContextFileSource`)
- 9 new `@Tool` methods (3 image + 6 audio) + 2 new AGENT validators (V003, V005)
- 3 new `BuiltInTool` enum entries
- 2 new `InjectionType` enum values
- 4 issues closed via PR (#19, #35, #93, #203 Phase A) + #9 advanced through 5 phases + #85 advanced through phases 1/2a
- ~115 new tests across the new toolkits and detector

## [0.8.3] - 2026-05-02

Two bug fixes batched together — both surfaced during open-issue triage. Backward-compatible, additive: `AuthType.API_KEY` is a new enum constant the framework's javadoc has documented since the annotation landed but never actually defined.

### Fixed
- `StdioTransport.shouldHandleProcessExit` flaky test (#205): the startup race in `waitForProcessStart()` polled `process.isAlive()` in a 50 ms loop, treating "alive" as a proxy for "started". Wrong for fast-exiting processes — a one-shot like `echo` writes its line and exits between two polls, so `isAlive()` returns `false` even though the process started, ran, and produced output successfully (pipe data persists in the kernel buffer after the writer exits). Replaced the loop with `return process != null` — `ProcessBuilder.start()` is synchronous on POSIX/macOS, so by the time we hold a `Process` reference exec(2) has succeeded. Test also migrated from `Thread.sleep(500)` to a `CountDownLatch(1)` synchronisation point, removing the timing assumption. 5× sequential local runs all PASS (was: failing once every ~3-5 CI runs).

### Added
- `AuthType.API_KEY` (#175): the `@WebService` javadoc has documented this auth type since the annotation landed, but the enum only defined `NO_AUTH` / `BEARER` / `BASIC`. A consumer copy-pasting from the javadoc got a compile error with no clear hint that the documented value didn't exist. Added the enum constant + a matching switch arm in `WebServiceExecutor.addAuthHeaders` that reads from `@WebService.authTokenEnv` and uses `@WebService.apiKeyHeader` for the header name (defaulting to `X-API-Key` when empty). The orphan `apiKeyHeader` annotation field — previously reachable from no code path — is now wired in.

### Stats
4 source files, +49/-22 LOC across `tnsai-mcp` + `tnsai-core`. 1 test rewritten for determinism (`StdioTransportTest$RealProcessTests .shouldHandleProcessExit`). 13/13 modules build, 9 083 tests pass.

PRs: [#205](https://github.com/TnsAI-Framework/TnsAI/pull/205), [#175](https://github.com/TnsAI-Framework/TnsAI/pull/175)

## [0.8.2] - 2026-05-01

Fixes a prompt-rendering bug in `SystemPromptBuilder` (the SCOP-bridge prompt path) where `@Responsibility.invariants` was emitted as a single comma-joined line. Long natural-language rules with internal commas (e.g. *"…a real-sounding name, a real city, a real job, real numbers"*) collapsed into prose under that join, hurting the LLM's ability to extract individual rules. Now rendered as a bullet list, one rule per line — restoring per-rule saliency.

### Fixed
- `SystemPromptBuilder.buildFromAnnotations(...)` rendered `@Responsibility.invariants[]` as `Invariants: rule1, rule2, rule3` (single comma-joined line). Fix emits each rule on its own bullet:
  ```
  Invariants:
      - rule1
      - rule2
      - rule3
  ```
  `@State.invariants` is intentionally **unchanged** — those are short technical predicates (e.g. `count >= 0`) that read cleanly in the existing inline `[constraints: …]` tag and don't suffer the same collapse problem.

### Stats
1 source file (`SystemPromptBuilder.java`, +6/-1 LOC) + 1 test case (`SystemPromptBuilderPromptTemplatesTest`, +35 LOC). 13/13 modules build; 9 083 tests pass.

PR: [#205](https://github.com/TnsAI-Framework/TnsAI/pull/205)

## [0.8.1] - 2026-04-30

Adds a typed-input overload to `Agent.executeAction` so consumers can replace `Map.of("path", "…", "question", "…")` call sites with a Java record. The new overload reflects the record's components into a parameter map and forwards to the existing untyped dispatch path — existing `Map<String, Object>` callers are unchanged thanks to Java's overload resolution preferring the more specific `Map` parameter.

This matches the dominant 2024–2025 industry pattern (LangChain `args_schema`, Vercel AI SDK + Mastra `inputSchema: z.object(...)`, Spring AI `FunctionToolCallback.inputType(...)`, LangChain4j typed `@Tool` parameters, Embabel `@Action` records) — typed input + string action name. No new annotations, no new mental model: write a record, pass an instance.

### Added
- `com.tnsai.actions.ParamBeanMapper` — utility that reflects a Java `record`, a POJO with `getX`/`isX` accessors, or a class with public fields into a `Map<String, Object>`. Maps are passed through unchanged. Null values are preserved as map entries so the framework's parameter binding can decide how to treat them.
- `Agent.executeAction(String actionName, Object input)` — typed-input overload that delegates to `ParamBeanMapper.toMap(input)` then to the existing `executeAction(String, Map<String, Object>)`.
- `Agent.executeActionOnRole(String roleId, String actionName, Object input)` — symmetric typed-input overload for role-scoped dispatch.

### Stats
3 files added/modified in `tnsai-core` (~330 LOC), 11 new `ParamBeanMapperTest` cases covering records, POJOs, public-field beans, map pass-through, null handling, and unsupported bean shapes. 13/13 modules build, 9 082 tests pass.

PR: [#204](https://github.com/TnsAI-Framework/TnsAI/pull/204)

## [0.8.0] - 2026-04-29

Finishes the RFC #188 cleanup by deleting the residual `@LLMTool` / `@ToolBinding` cookbook layer. The annotations were a metadata surface for a tool-calling executor (`LLMToolsExecutor`) that was already removed in 0.6.0 — the configuration was being silently ignored at runtime. Per-action LLM overrides (`llmSystemPrompt`, `llmTemperature`) move directly onto `@ActionSpec`. Tool exposure is purely agent-level via `AgentBuilder.builtInTools(...)` / `.toolPojos(...)`; the agent's `ToolMethodDispatcher` is the single dispatch path for any `@Tool` methods the LLM emits. One annotation, one runtime path, no dead config carriers.

### Added
- `@ActionSpec.llmSystemPrompt() : String` — per-action system-prompt override (replaces `@LLMTool.systemPrompt()`)
- `@ActionSpec.llmTemperature() : float` — per-action temperature override (replaces `@LLMTool.temperature()`)
- `AgentBuilder.builtInTools(BuiltInTool... tools)` — compile-time-safe shortcut that reflectively instantiates each shipped POJO toolkit and registers it through the same pipeline as `.toolPojos(...)`
- `BuiltInToolInstantiationException` — surfaced when a `BuiltInTool` entry's backing class is missing from the classpath (typically because `tnsai-tools` is not a dependency)
- `BuiltInTool` enum: per-entry `getClassName()` accessor + `instantiate()` reflective constructor

### Changed
- `BuiltInTool` enum rewritten from 33 dangling entries (post-SPI delete in 0.5.7 they had no backing classes) to 59 POJO-aligned entries that map each shipped toolkit's FQCN. Each entry's Javadoc lists every `@Tool` method the toolkit exposes
- `BuiltInTool.AI_TOOLS` renamed → `VISION_TOOLS` (the backing `AiTools` POJO ships only `image_analyze`; the previous name was misleading)
- `LLMRoleExecutor` now reads `llmTemperature` / `llmSystemPrompt` from `@ActionSpec` directly (was `@LLMTool` nested annotation)
- `ActionExecutor` LLM-branch routing simplified — every `ActionType.LLM` action now goes through `LLMRoleExecutor` regardless of (former) `availableTools` content; tool calls are dispatched by the agent-level `ToolMethodDispatcher`

### Removed
- **BREAKING:** `@com.tnsai.annotations.LLMTool` annotation (every field: `tools`, `customTools`, `maxToolCalls`, `parallelToolCalls`, `systemPrompt`, `temperature`, `maxIterations`, `stopSequences`, `includeToolHistory`, `mcpServers`, `bindings`, `returnKey`)
- **BREAKING:** `@com.tnsai.annotations.ToolBinding` annotation + `@LLMTool.bindings()` field — the SCOP-side dispatcher that consumed them was deleted in 0.6.0; the `${param}` template + text-protocol pattern is superseded by typed `@Tool` method parameters that the LLM populates directly from its function-call arguments
- **BREAKING:** `@ActionSpec.llmTool() : LLMTool` annotation field
- **BREAKING:** `com.tnsai.metadata.LLMToolConfig` record (use the `@ActionSpec.llmSystemPrompt()` / `.llmTemperature()` accessors on `ActionMetadata` directly)
- **BREAKING:** `ActionMetadata.llmToolConfig()` accessor
- **BREAKING:** `ActionMetadata.getBuiltInTools()`, `getCustomToolNames()`, `getAvailableTools()`, `getMaxToolCalls()`, `isParallelToolCalls()`, `getMaxIterations()`, `getStopSequences()`, `isIncludeToolHistory()`, `getMcpServers()`, `hasMcpServers()`, `isRequireToolUse()` — all dead since 0.6.0
- **BREAKING:** Reference examples `tnsai-integration/.../scop/examples/DataAnalystRole`, `CsvLoaderRole`, and the `DataAnalystRoleAnnotationRoundTripTest` — they demonstrated the deleted cookbook against an executor that no longer existed
- `LLMToolConfigTest` test class
- 7 stale `LLMToolsExecutor` Javadoc references in `actions/executors/package-info`, `actions/package-info`, `TypedActionExecutor`, and assorted other source files (the class was deleted in 0.6.0 but the comments lingered)

### Fixed
- The Fumadocs static-export search on the docs site was hitting `/api/search` with no static index materialised; now wires `createFromSource(source).staticGET()` to a `static.json` route handler and passes `search={ options: { type: 'static', api: '/static.json' } }` to `RootProvider`. ~10 600 docs entries indexed at build time
- `tnsai-tools/README.md`, `tnsai-core/README.md`, `tnsai-tools/CLAUDE.md`, `tnsai-tools/CODEBASE_MAP.md`, the root `CLAUDE.md`, `tnsai-core/.../annotations/{ActionSpec,LLMTool,ToolBinding}.java` Javadoc, and the entire docs-site `capabilities/tools/` + `tutorials/`
  + Quick-Start surface — all rewritten against the post-RFC-#188 reality (no `Tool` interface, no `*Tool` classes, no `@LLMTool`, no `LLM_TOOL`/`LLM_ROLE` action types, accurate tool counts)
- `ActionSpec.java` Javadoc: action-types table + `@LLMTool`-using example swapped for the new `llmSystemPrompt` / `llmTemperature` shape

### Migration
The compile-time fix for any consumer using `@LLMTool`:

```java
// before (compile error in 0.8.0)
@ActionSpec(
    type = ActionType.LLM,
    description = "...",
    llmTool = @LLMTool(
        systemPrompt = "You are concise.",
        temperature = 0.2f,
        tools = { BuiltInTool.CSV_TOOLS }   // never dispatched anyway since 0.6.0
    )
)
public String summarise(String text) { ... }

// after
@ActionSpec(
    type = ActionType.LLM,
    description = "...",
    llmSystemPrompt = "You are concise.",
    llmTemperature = 0.2f
)
public String summarise(String text) { ... }
```

Tool exposure moves to the agent-build call site, where it has always been the only working path:

```java
Agent agent = AgentBuilder.create()
    .id("data-analyst")
    .llm(llmClient)
    .role(new MyRole())
    .builtInTools(BuiltInTool.CSV_TOOLS, BuiltInTool.PDF_TOOLS)   // or .toolPojos(new MyOwnTools())
    .build();
```

Anyone leaning on `@ToolBinding(tool = "csv_parser", inputTemplate = "${path}|||command")` ports to typed `@Tool` parameters on the receiving toolkit method — the LLM populates them directly from its function-call arguments, no text-protocol substitution layer.

### Stats
~870 lines net delete across `tnsai-core` + `tnsai-integration`. 13/13 modules build green, 9 071 tests pass.

PR: [#203](https://github.com/TnsAI-Framework/TnsAI/pull/203)

## [0.7.0] - 2026-04-29

Closes RFC #188 by retiring the legacy `Tool` interface entirely. `ToolMethod` is now a sealed interface with two variants: `StaticToolMethod` (POJO `@Tool` methods) and `DynamicToolMethod` (runtime-defined via `Handler` callback, e.g. MCP proxies). One registry, one dispatcher.

### Added
- `DynamicToolMethod` record — runtime-defined tool variant for proxies and plugin systems
- `StaticToolMethod` record — extracted reflection-dispatch logic from `ToolMethodDispatcher`
- `AgentBuilder.dynamicTool(DynamicToolMethod)` and `.dynamicTools(List<DynamicToolMethod>)`
- `AutoTeamBuilder.dynamicTool(...)` / `.dynamicTools(...)` mirror APIs
- `McpProxyTool.toDynamicToolMethod(...)` static factory — replaces `implements Tool`
- `ToolMethodDispatcher.lookup(name)` and `.registry()` accessors
- `TnsAIToolProvider.fromDynamic(DynamicToolMethod...)` and `.from(pojos, dynamicTools)` factories

### Changed
- `ToolMethod` is now a `sealed interface` (was a record); permits `StaticToolMethod`, `DynamicToolMethod`
- `McpToolBridge.toTnsAITools()` returns `List<DynamicToolMethod>` directly (no wrapper)
- `ActionExecutor` constructor now takes `ToolMethodDispatcher` (was `List<Tool>`)
- `ActionExecutor.executeExternalTool(String, Map<String, Object>)` (was `(String, String)`)
- `UnifiedContextAssembler.tools(List<ToolMethod>)` (was `List<Tool>`)

### Removed
- **BREAKING:** `com.tnsai.tools.Tool` interface
- **BREAKING:** `AgentBuilder.tool(Tool)`, `.tools(List<Tool>)`, `.getToolsList()`
- **BREAKING:** `ConfigurableAgent.getExternalTools()`
- **BREAKING:** `ToolSchemaGenerator.generateToolSchema(Tool)`
- **BREAKING:** `McpToolBridge.TnsAIToolWrapper` adapter class
- **BREAKING:** `TnsAIToolProvider.fromTools(Tool...)` factory + legacy dispatch branch
- **BREAKING:** `AutoTeamBuilder.tool(Tool)` / `.tools(List<Tool>)`
- `ToolMethodAdapter` bridge class (no consumers left after migration)
- `ToolFailureMode` annotation + `ToolFailureModeReader` helper
- Orphan `tnsai-integration/.../CsvLoaderRoleBindingTest` (referenced llmtools deleted in 0.6.0)

### Fixed
- `CancellationToken` concurrency race: concurrent `cancel()` + `onCancel()` could fire a callback twice. Now exactly-once via per-registration `AtomicBoolean` guard.

### Migration
Consumers using `AgentBuilder.toolPojos(...)` are unaffected — recommended path unchanged. Direct `Tool` implementers move to either:
- A POJO with `@Tool`-annotated methods, registered via `.toolPojos(new MyTool())`
- A `DynamicToolMethod` constructed via factory, registered via `.dynamicTool(myTool)`

`ChatRequest.tools` is still `List<Map<String, Object>>` (unchanged since 0.6.0).

### Stats
35 files, ~733 lines net delete. Combined with 0.6.0: ~14k lines removed from the legacy tool stack.

PR: [#202](https://github.com/TnsAI-Framework/TnsAI/pull/202)

## [0.6.0] - 2026-04-29

Continues the RFC #188 legacy-tool-stack delete that started in 0.5.7. `tnsai-core` shrinks to a leaner spine. `Tool` interface stays as a slim registration surface for one more release; full removal in 0.7.0.

### Changed
- **BREAKING:** `ChatRequest.tools` type changed `List<ToolDefinition>` → `List<Map<String, Object>>` (JSON-Schema fragments, Anthropic-style tool-use format)
- `Tool` interface slimmed (369L → 138L) — kept core contract + safety/policy hints

### Removed
- **BREAKING:** `ToolDefinition` record + builder + `fromMap` / `toMaps` helpers
- **BREAKING:** `ToolSchemaGenerator.generateToolDefinition*` methods (3 overloads)
- **BREAKING:** `Tool` interface metadata-discovery surface — 12 default methods removed: `getCategory`, `getUsageExamples`, `getMetadata`, `getSearchKeywords`, `getPriority`, `canHandle`, `getAllowedCallers`, `isParallelizable`, `getReturnFormat`, `getLatencyCategory`, `getShortDescription`, `executeAsync`
- **BREAKING:** `ToolMetadata`, `ToolCategory`, `ToolLatency` types
- Legacy `actions/llmtools/` subsystem
- `@ToolSpec` and `@ToolAction` annotations + reflective extractor
- Hooks/policy/validators ecosystem (`Pre/Post/Error/Register ToolUse` events, `ToolPolicy*`, `ToolApprovalValidator`, `LLMCapabilityValidator`)
- `ToolMetrics` (628L) + `ToolExecutionMetric`
- `ToolRegistry` (225L) + `ToolProvider` SPI
- `AgentBuilder.tool(String name)` lookup overload
- Dead `ToolCallProcessor` (264L, no consumers)

### Migration
Custom `ChatRequest` callers swap `ToolDefinition.of("name", "desc")` for `Map.<String, Object>of("name", "name", "description", "desc", "parameters", Map.of())`. Direct `Tool` implementers can drop the deleted-method overrides — they no longer compile but no consumer reads them.

### Stats
~10.4k lines removed from the runtime.

PRs: [#194](https://github.com/TnsAI-Framework/TnsAI/pull/194), [#195](https://github.com/TnsAI-Framework/TnsAI/pull/195), [#196](https://github.com/TnsAI-Framework/TnsAI/pull/196), [#197](https://github.com/TnsAI-Framework/TnsAI/pull/197), [#198](https://github.com/TnsAI-Framework/TnsAI/pull/198), [#199](https://github.com/TnsAI-Framework/TnsAI/pull/199), [#200](https://github.com/TnsAI-Framework/TnsAI/pull/200)

## [0.5.7] - 2026-04-29

RFC #188 Phase 2 + 3a + 3b: full migration of the tool catalog to the function-shape POJO pattern (LangChain / Spring AI / CrewAI / Mastra style). Replaces 130+ `extends AbstractTool` legacy implementations with 60 typed POJOs exposing ~190 `@Tool`-annotated methods across 28 categories.

### Added
- `ToolMethodRegistry` — reflection-based @Tool discovery, duplicate-name fail-fast
- `ToolMethodDispatcher` — Jackson type-aware coercion + `Method.invoke` dispatch
- `JsonSchemaGenerator` — derives JSON Schema fragments from `@Tool`/`@ToolParam` metadata
- `ToolMethodAdapter` — bridges function-shape `ToolMethod` to legacy `Tool` interface (deleted in 0.7.0)
- `AgentBuilder.toolPojos(Object...)` registration path
- `TnsAIToolProvider.fromPojos(Object...)` for MCP server integration
- 60 function-shape POJOs across 28 categories (file, search, database, communication, fintech, utility, etc.)
- 3 server-tool POJOs: `ServerFileTools`, `ServerShellTools`, `ServerGitTools`

### Removed
- 134 `*Tool.java` legacy implementations under `tnsai-tools`
- 32 `*ToolProvider.java` SPI factory classes
- 18 framework infrastructure files (`AbstractTool`, `AbstractCategoryToolProvider`, validation/health/manifest/enhancement helpers)
- 152 corresponding `*Test.java` files
- `tnsai-tools` SPI registration

### Migration
Consumers extending `AbstractTool` move to a POJO with `@Tool`-annotated methods. Pattern documented in `tnsai-core/CLAUDE.md`. Most consumers don't touch this — they use built-in tools through `tnsai-tools`, which is now backed by the new POJOs transparently.

### Stats
+27,160 / −85,000 lines (~58k net smaller). The biggest single cleanup in TnsAI history.

PRs: [#190](https://github.com/TnsAI-Framework/TnsAI/pull/190), [#191](https://github.com/TnsAI-Framework/TnsAI/pull/191), [#192](https://github.com/TnsAI-Framework/TnsAI/pull/192), [#193](https://github.com/TnsAI-Framework/TnsAI/pull/193)

## [0.5.6] - 2026-04-28

Patch release. Broadens the `FileToolProvider` optional-dep isolation introduced in 0.5.5 to also cover `JSONQueryTool` and `CSVParserTool`, which the first pass missed.

### Fixed
- `FileToolProvider` — `JSONQueryTool` (`com.jayway.jsonpath` optional dep) and `CSVParserTool` (`com.opencsv` optional dep) were still on the eager `toolSuppliers()` list. Same `LinkageError` failure mode as 0.5.5 (#184) — taking the whole provider down when a consumer pulled `tnsai-tools` without those transitives. Moved both to `reflectiveToolClassNames()` so missing optional deps are isolated per tool.

### Changed
- `FileToolProvider.toolSuppliers()` now lists ONLY 3 pure-JDK tools (`FileReadTool`, `FileWriteTool`, `XMLParserTool`)
- `FileToolProvider.reflectiveToolClassNames()` now lists 8 optional-dep tools (`JSONQueryTool`, `CSVParserTool`, `MarkItDownTool`, 5× `PDF*Tool`)
- `AbstractCategoryToolProviderIsolationTest` updated for new split (controls = pure-JDK FQNs, base assertion ≥3)

### Migration
None — pure bug fix. Behaviour-changing only when an optional dep is missing (failure now isolated as `ToolLoadFailure`, was complete provider crash).

PR: [#186](https://github.com/TnsAI-Framework/TnsAI/pull/186)

## [0.5.5] - 2026-04-28

Patch release. Fixes an all-or-nothing failure mode in `FileToolProvider` when a consumer pulls `tnsai-tools` without the optional Apache PDFBox or MarkItDown transitive dependencies.

### Fixed
- `FileToolProvider` — eager `XYZTool::new` method-references in `toolSuppliers()` resolved their `MethodHandle` at `List.of(...)` evaluation time, **outside** the per-tool try/catch. Missing PDFBox or MarkItDown transitive → `LinkageError` from list construction → entire provider unloaded → 8 unrelated File tools (JSON, CSV, file IO, XML) became unavailable.

### Added
- `AbstractCategoryToolProvider.reflectiveToolClassNames()` load path — `Class.forName(name)` defers linkage until inside the per-tool try/catch
- `AbstractCategoryToolProviderIsolationTest` (6 cases): pins the contract that missing FQN does not break a present sibling, and PDF failures are captured without propagating

### Changed
- `FileToolProvider`: 6 optional-dep tools (`MarkItDownTool`, 5× `PDF*Tool`) moved from `toolSuppliers()` to `reflectiveToolClassNames()`. `toolSuppliers()` keeps the 5 base-JDK + opencsv tools.

### Migration
None — pure bug fix. Behaviour-changing only when an optional dep is missing.

PR: [#184](https://github.com/TnsAI-Framework/TnsAI/pull/184)

## [0.5.4] - 2026-04-28

Minor-feature release. **Annotation-driven runtime resolution sprint** closing most of the umbrella tracked under [#169]. All additive (Phase 1 boundaries: no external storage / SPI deps); existing call sites unchanged for code that doesn't opt into the new annotations.

### Added
- `@LLMTool` runtime path surfaced via `LLMToolsExecutor` ([#167]) + `DataAnalystRole` reference + `BuiltInToolEnumAuditTest`
- `@WebService` runtime path surfaced via `WebServiceExecutor` ([#174]) + `WeatherRole` reference
- `com.tnsai.guardrails` package — `@InputGuardrail` / `@OutputGuardrail` enforcement with `minLength` / `maxLength` / `blockPatterns` / `allowPatterns` + `onFailure` ∈ `{REJECT, WARN, SANITIZE, REVIEW}` ([#176])
- Optional `RetrievalSpi` in `tnsai-core` + default impl in `tnsai-intelligence` (`RoleRagBinding`, `LocalFileSourceLoader`, `DefaultRetrievalSpi`) — wires `@KnowledgeSource` / `@Retrieval` end-to-end ([#178])
- `@ToolBinding` declarative tool-input mapping with `${param}` / `${role.name}` / `${action.name}` / `${env:VAR}` substitution ([#179])
- `com.tnsai.resilience` decorators — `@Traced` (MDC trace-id), `@Metered` (in-memory `ResilienceMetrics`), `@Fallback` (`forAction` binding + immediate-retry) ([#182])
- 6 reference roles in `tnsai-integration/scop/examples/`: `DataAnalystRole`, `WeatherRole`, `UserInputRole`, `ResearchRole`, `CsvLoaderRole`, `PaymentRole` — each with its own integration test exercising the live `ActionExecutor` pipeline
- `ActionParams.firstStringValueInDeclarationOrder` shared helper

### Changed
- `@ToolBinding` simplified to single-field `tool()` (was two mutually-exclusive `builtIn` + `custom` fields with `BuiltInTool.NONE` sentinel) — single source, identical syntax for built-in and custom tools ([#181] refactor of [#179])
- `FallbackResolver.tryRecover` and `RetryCallback.invoke()` narrowed `catch (Throwable)` → `catch (Exception)` (caught by `SourceHygieneTest.noBroadThrowableCatches` from #41)
- `LLMToolsExecutor` non-deterministic `parameters.values().iterator().next()` (HashMap iteration order is per-JVM) replaced with the new shared helper

### Stats
- Tests: tnsai-core 2992 → 3047 (+55), tnsai-integration 78 → 129 (+51)
- Why not 0.6.0: every gap closed was a runtime path that was documented but unenforced — no API removals, no behaviour changes for non-opt-in code

### Deferred to Phase 2+ (separate issues)
- `@Sanitize` / `@ContentFilter` standalone enforcement (#171 follow-up)
- `InputValidator` / `InputSanitizer` SPI for custom `Class[]` hooks
- `@MemorySpec` resolver (Persistence.REDIS / DATABASE / FILE)
- KnowledgeType source loaders (URL / VECTOR_DB / DATABASE / WEB_SEARCH)
- Embedding SPI replacing `HashEmbeddingFunction`
- `@RateLimited`, `@Resilience(circuitBreaker)`, `@Idempotent` keyed cache (need distributed-state SPI)
- OpenTelemetry SPI for `@Traced`; Micrometer/Prometheus sink for `@Metered`
- Build-time validation (fail-fast on `@ToolBinding` typos)
- `AuthType.API_KEY` enum value ([#175])

PRs: [#167](https://github.com/TnsAI-Framework/TnsAI/pull/167), [#174](https://github.com/TnsAI-Framework/TnsAI/pull/174), [#176](https://github.com/TnsAI-Framework/TnsAI/pull/176), [#178](https://github.com/TnsAI-Framework/TnsAI/pull/178), [#179](https://github.com/TnsAI-Framework/TnsAI/pull/179), [#181](https://github.com/TnsAI-Framework/TnsAI/pull/181), [#182](https://github.com/TnsAI-Framework/TnsAI/pull/182)

## [0.5.3] — 2026-04-27

Patch release. **4 PRs** (#156, #157, #158, #165) since `v0.5.2`. All additive — no public API removals, no behaviour regressions. Single theme: closing the `ProviderErrorMapper` SPI matrix at **13/13 shipping LLM providers** (issue #87 fully resolved).

### Added — Nine new `ProviderErrorMapper`s (closes #87)

`v0.5.2` shipped 4 mappers (OpenAI + Anthropic + Gemini + Ollama). This release adds the remaining 9 to complete coverage of every provider in `tnsai-llm`:

- **`MistralProviderErrorMapper`** (PR #156) — OpenAI-compatible envelope plus Mistral-specific code routing. Handles `requests_too_many` (alongside `rate_limit_exceeded`) → `MODEL_OVERLOADED` and Mistral's stricter `model_quota_exceeded` semantics. `MistralAIClient.chat` / `streamChat` refactored to `executeRequest("Mistral")`.

- **`BedrockProviderErrorMapper`** (PR #157) — first mapper that works against AWS SDK exceptions, not HTTP responses. `BedrockClient.mapAwsException` extracts the AWS error code, reconstructs an AWS-shape envelope, propagates `x-amzn-requestid` via headers, and feeds the SPI mapper's HTTP-style API. Same SPI contract handles both code paths so consumers see typed `LLMException` regardless of transport.

- **`GroqProviderErrorMapper`** (PR #158) — OpenAI-compatible at the envelope level (Groq mirrors OpenAI's API by design); maps Groq's low-latency-specific codes (`requests_too_many`, queue saturation variants) to `MODEL_OVERLOADED` so consumer fallback chains treat them as transient. Captures `groq-region` header for routing-issue triage.

- **`OpenRouterProviderErrorMapper`** (in PR #165) — aggregator envelope. Surfaces the upstream provider name via `metadata.provider_name` so consumers triaging an OpenRouter failure can see which downstream provider actually misbehaved.

- **`AzureOpenAIProviderErrorMapper`** (in PR #165) — OpenAI-compat body, Azure deployment-id model field, captures `apim-request-id` / `x-ms-region` headers for Azure-specific triage. Distinguishes Azure's `content_filter` (Azure's responsible AI gating) from OpenAI's lexical codes.

- **`CohereProviderErrorMapper`** (in PR #165) — Cohere's RAG-focused API. Maps the `command-r*` model family appropriately; handles Cohere's distinct streaming JSON-lines envelope.

- **`HuggingFaceProviderErrorMapper`** (in PR #165) — covers both Inference API and custom Inference Endpoints. Critical: routes the model-loading-503 case to `SERVER_ERROR` (retryable) so cold models don't kill consumer requests on first hit.

- **`MiniMaxProviderErrorMapper`** (in PR #165) — handles two error envelopes simultaneously: OpenAI-compatible at `/chat/completions` (used by `MiniMaxClient`) AND native `base_resp.status_code` at `/text/chatcompletion_v2` (used by partner-routed proxies). Native channel routes failures through HTTP 200 — the only signal is the JSON status code — so the mapper inspects `base_resp` first.

- **`ZhipuAIProviderErrorMapper`** (in PR #165) — closes the matrix at 13/13. Numeric-string codes clustered by family: 100x (auth/billing) → `AUTHENTICATION_FAILED`, 11xx (rate) → `MODEL_OVERLOADED`, 12xx (input/context) → `INVALID_REQUEST` / `MODEL_NOT_FOUND` / `CONTEXT_TOO_LONG`, 13xx (server) → `SERVER_ERROR`. Code shape normalisation handles both string and numeric JSON variants.

### Refactor — every LLM client routes through `executeRequest`

All 13 clients now share the same error-translation entry point (`AbstractLLMClient.executeRequest`), eliminating per-client `handleError` / `formatApiError` divergence. Each client's `chat` / `streamChat` (and `chat(List<ContentPart>)` for vision-capable clients) is wrapped with a `catch (LLMException) { throw e; }` guard before the generic `catch (Exception)` translator so typed exceptions aren't double-wrapped.

### Why patch (not minor)

- All 9 mappers are SPI-discovered (zero API surface change for consumers that don't read `LLMException.getProviderDetails()`)
- Refactored `chat` / `streamChat` paths preserve identical `ChatResponse` returns; the only observable difference is that failures throw typed `LLMException` instead of the legacy generic wrapper
- Tests added (~150 new mapper test cases) without touching any existing passing test

### Test coverage

195+ mapper-specific test cases across the 13 mappers. Aggregate `mvn -pl tnsai-llm test` reports 1072+ tests green.

Triggers `release.yml` on tag push. Downstream consumer-repo sync follows the rule codified in CLAUDE.md (PR #149) — separate PRs in the same session.

## [0.5.2] — 2026-04-27

Patch release. **4 PRs** (#150–#154) since `v0.5.1`. Pure additive — no public API removals, no behaviour regressions. Themes: hardening the error-report pipeline shipped in #86 with a deduplicating decorator, and finally connecting the `ProviderErrorMapper` infrastructure that had been dead in the tree since 0.5.0.

### Added — `DedupingErrorReportPublisher` (#86 follow-up, PR #150)

Decorator that wraps any `ErrorReportPublisher` and suppresses repeats of the same `ErrorReport.fingerprint()` within a configurable time window. First occurrence per window emits, rest are dropped and counted. Standard usage:

```java
ErrorReports.setPublisher(
    DedupingErrorReportPublisher.wrap(new Slf4jErrorReportPublisher())
);
```

Default 5-minute window matches Sentry / Rollbar's standard. Surfaces "suppressed N duplicates in the previous window" log line on window roll. `getSuppressedCount(fingerprint)` exposes per-fingerprint counts as a Prometheus gauge.

### Wired — `ProviderErrorMapper` SPI lookup (#87, PR #151)

Closes the wiring gap that left `ErrorEmitter`, `ProviderErrorMapper` SPI, and the OpenAI + Anthropic mappers (all shipped 0.5.0) entirely dead. `AbstractLLMClient.executeRequest` now snapshots HTTP error headers + body, looks up the SPI mapper for the provider, and throws the typed `LLMException` directly with `ProviderDetails` attached. Falls back to the legacy `IOException` path when no mapper is registered.

`OpenAIClient` and `AnthropicClient` catch blocks now `catch (LLMException) { throw e; }` before the generic catch so a mapper-derived exception isn't double-wrapped (`ProviderDetails` would have ended up on `ex.getCause()` otherwise).

`AnthropicClient.chat` / `streamChat` refactored from inline `response.isSuccessful()` handling to `executeRequest("Anthropic")` so the SPI mapper is actually reached — same applies to follow-ups below.

### Added — Two new `ProviderErrorMapper`s

- **`GeminiProviderErrorMapper`** (PR #152) — Google API-Gateway envelope (`error.{code,message,status}`). Routes the canonical `google.rpc.Code` strings (`RESOURCE_EXHAUSTED` → `MODEL_OVERLOADED`, `UNAUTHENTICATED` / `PERMISSION_DENIED` → `AUTHENTICATION_FAILED`, `INVALID_ARGUMENT` → `INVALID_REQUEST` with token-hint demotion to `CONTEXT_TOO_LONG`, etc.). Captures `x-goog-*` + `retry-after` headers. `GeminiClient.chat` / `streamChat` refactored to use `executeRequest`.

- **`OllamaProviderErrorMapper`** (PR #154) — heuristic on the free-text `error` string (Ollama has no structured codes): "not found" / "no such model" / "try pulling" → `MODEL_NOT_FOUND`; "context" / "exceeds" / "token" → `CONTEXT_TOO_LONG`; "out of memory" / "VRAM" / "OOM" → `MODEL_OVERLOADED`. HTTP fallback with 503 → `MODEL_OVERLOADED` (daemon temporarily unavailable). No headers captured — local Ollama doesn't carry diagnostic headers worth keeping. Defensive on envelope shape (handles both `error` as plain string and as object with `message` field). `OllamaClient.chat` / `streamChat` refactored to use `executeRequest`.

### Provider mapper coverage

The full default model lineup (`anthropic` / `openai` / `gemini` / `ollama`) now has typed error mapping for **4/4 providers**.

### Out of scope (follow-up issues)

Nine remaining `ProviderErrorMapper`s (Bedrock, Azure, Cohere, Groq, HuggingFace, OpenRouter, MiniMax, ZhipuAI, Mistral). Each follows the established pattern — one mapper class + one SPI registry line
+ unit tests + (if the client uses inline error handling) the `executeRequest` refactor.

## [0.5.1] — 2026-04-27

Patch release. **6 PRs** (#139–#147) since `v0.5.0`. Pure additive — no public API removals, no behaviour regressions. Themes: completing the build-time validation pipeline started in #85, wiring agent context capture for exception enrichment (#90), and shipping the error-report emission pipeline (#86).

### Added — build-time validators (`tnsai-core` / `agents/validation/validators/`)

Four new validators land in `AgentBuilder.VALIDATORS`, each running during `build()` against a `ValidationContext` snapshot:

- **`AGENT-V006` `ToolApprovalValidator`** — WARNING when a registered tool reports `requiresConfirmation()==true` but the agent has no built-in confirmation channel wired (the operator must call `agent.setToolCallFilter(...)` post-build, otherwise calls block indefinitely waiting for a confirmation that never arrives).
- **`AGENT-V007` `CapabilityClasspathValidator`** — ERROR when a `@Capability` interface implemented by a role can't be fully reflected on (parameter / return type or annotation value references a class missing from the runtime classpath).
- **`AGENT-V008` `ActionNameCollisionValidator`** — ERROR when two roles declare `@ActionSpec` methods with the same name; today the framework's name → action map silently keeps whichever role was registered last.
- **`AGENT-V009` `ResilienceConfigValidator`** — ERROR for clearly invalid `@Resilience` numerics (negative timeout / maxAttempts / backoff, multiplier < 1.0, failureRateThreshold outside 0–100); WARNING for configured-but-effectively-disabled subsystems (`@Retry` with non-default fields but `maxAttempts==0`, `@CircuitBreaker(enabled=true)` with non-positive `failureThreshold`, `@RateLimit(enabled=true)` with `maxRequests<=0`).

Suppress any individual issue with `AgentBuilder.relaxValidation("AGENT-V0xx")`.

### Added — error context capture wiring (`tnsai-core` / `agents/Agent.java`)

`Agent.java`'s 11 public chat / stream / executeAction entry points now wrap their delegation in `try (var ignored = AgentContext.enter(buildEntryContext(op)))`.

Net effect: any `TnsAIException` thrown deep inside the orchestrator auto-captures the current agent + role + traceId via `AgentContext.currentOptional()`, so `getMessage()` / `getContext()` surface attribution without consumer plumbing. Top-level entries get a fresh trace id; nested entries (agent-from-tool, agent-from- hook) inherit upstream `tenantId` / `sessionId` / `traceId` / `spanId` while overwriting `agentId` / `role`.

The entry op (`"chat"`, `"executeAction:summarize"`, …) lands in `EventContext.extensions["agent.entry.op"]` for log filtering.

### Added — error report emission pipeline (`tnsai-core` / `observability/errors/`)

The `ErrorEmitter` factory shipped with 0.5.0 had no companion publisher — consumers could construct an `ErrorReport` but had nowhere to send it. This release ships the pipeline mirroring the `AgentEventPublisher` pattern from #78:

- **`ErrorReportPublisher`** — single-method SPI. Discovered via `ServiceLoader`; consumers add Sentry / Loki / custom sinks by dropping a JAR with a `META-INF/services/com.tnsai.observability.errors.ErrorReportPublisher` entry.
- **`Slf4jErrorReportPublisher`** (default, SPI-registered) — JSON-serializes the report via Jackson + `Jdk8Module` (so `Optional<T>` unwraps to value-or-null) + `JavaTimeModule`. Logs at WARN for `TRANSIENT` / `RESOURCE` / `EXTERNAL` categories (self-healing or expected backpressure), ERROR for everything else.
- **`CompositeErrorReportPublisher`** — fan-out over multiple publishers with per-publisher failure isolation (one throwing publisher doesn't stop the others).
- **`CapturingErrorReportPublisher`** — in-memory test workhorse, not SPI-registered. Wire via `ErrorReports.setPublisher(...)` and tear down via `ErrorReports.resetForTesting()`.
- **`ErrorReports`** — static facade with lazy SPI discovery. The one-line emit entry point:
  ```java
  ErrorReports.publish(throwable, ErrorCategory.TRANSIENT,
          Map.of("provider", "openai", "model", "gpt-4o"));
  ```

### Tests

+30 new tests (4 validators × ~10, 5 entry-point context, 11 error report pipeline). Full `tnsai-core` suite green except for a pre-existing `CancellationTokenTest$Concurrency.registerWhileCancellingStillFires` flake (passes in isolation; unrelated to this release).

### Out of scope (follow-up issues)

- **#144** — `AGENT-V003` code collision (`ToolNameUniquenessValidator` and `LLMCapabilityValidator` both report under `V003`); rename pending operator approval (Protected Change per `CLAUDE.md`).
- **#145** — `AGENT-V004` LLM streaming/structured/vision capability validator needs a builder capability-declaration API first.
- **#146** — `AGENT-V012` tenant-scope validator + `#92` per-tenant error budget both blocked on a multi-tenant runtime feature that doesn't exist yet.
- `DedupingErrorReportPublisher` decorator (TTL'd fingerprint suppression) — natural #86 successor; deferred.
- Wiring `TnsAIException.<init>` to auto-publish — would double-publish for caught-and-rethrown chains; needs a different attach point.

## [0.5.0] — 2026-04-26

Feature-batch release. **19 PRs** (#119–#137) since `v0.4.0`. Major themes: agent lifecycle FSM, cooperative cancellation, tool-policy + risk-metadata SPI, Anthropic ephemeral prompt-caching wire format, Telegram retry-on-transient transport, build-time validation pipeline (one new validator), `@ToolFailureMode` annotation, `ModelFamily` / `TimeoutPolicy` / `DestructiveCommandDetector` standalones, and a SCOPBridge prompt-rendering overhaul that closes 4 drift bugs against `RolePromptBuilder`.

### Added — types

- **`AgentState` enum extended** to the full lifecycle FSM (`CREATED → STARTING → RUNNING → STOPPING → STOPPED`, plus terminal `FAILED`). The seed value `READY` from 0.4.0 is removed — see Removed.
- **`Agent.getState()`** — new public accessor on every `Agent`.
- **`com.tnsai.tools.ToolRiskLevel`** + **`SideEffect`** enums.
- **`com.tnsai.tools.policy`** package: `ToolPolicy` (`ALLOW_ALL` / `DENY_ALL` / `SAFE_ONLY`), `ToolPolicyDecision`, `ToolPolicyEvaluator`, plus `com.tnsai.hooks.policy.ToolPolicyHook` consuming it via `Hook<PreToolUse>`.
- **`com.tnsai.cancellation`** package: `CancellationToken` interface, `CancellationException`, `DefaultCancellationToken` (one-shot CAS), `NoopCancellationToken` (singleton no-op).
- **`com.tnsai.timeout.TimeoutPolicy`** record with `Category` enum (`LLM_CALL` / `TOOL_CALL` / `MCP_CALL` / `CHANNEL_SEND`) and `UNBOUNDED` sentinel.
- **`com.tnsai.prompt.ModelFamily`** enum + `fromModelId(String)` best-effort mapper covering Claude / GPT / Gemini / Llama naming conventions.
- **`com.tnsai.tools.spi.ToolFailureMode`** annotation + `ToolFailureModeReader` resolver — tool authors declare retryable / non-retryable exception classes; `nonRetryable` beats `retryable` in conflict resolution.
- **`com.tnsai.security.DestructiveCommandDetector`** in `tnsai-quality`: content-level `ToolCallFilter` with a 21-pattern catalogue (`rm -rf`, `git reset --hard`, `dd` to `/dev/`, `mkfs`, `chmod 777/000`, `kill -9` broadcasts, redirects to `/etc/*` / `~/.ssh/*`, `sudo rm/dd`, shutdown / reboot --force, shred -ru, wipefs --force).
- **`LLMCapabilityValidator`** — fourth validator in the `AgentBuilder` pre-flight pipeline (issue #85 slice). Catches the canonical "tools registered + LLM doesn't support function-calling" misconfiguration at build time with stable code `AGENT-V003`.
- **`DiscoveredRoleActions.getActions()`** — public list accessor.

### Added — interface extensions (default methods, additive)

- **`Tool.getRiskLevel()`** → `ToolRiskLevel.MEDIUM`, **`getRequiredSecrets()`** → empty `Set<String>`, **`getTimeout()`** → `Duration.ofSeconds(30)`, **`getSideEffects()`** → empty `Set<SideEffect>`.
- **`AgentPromptBuilder.buildSystemPrompt(..., ModelFamily)`** overload — Claude gets a softer suggestive register; GPT / Gemini / Llama / OTHER share the historical imperative wording byte-for-byte.
- **`AnthropicClient.Builder.enableEphemeralCaching(boolean)`** + **`cacheLastNTurns(int)`** — wires `cache_control: ephemeral` markers into system + last N user messages (capped at the 4-per-request Anthropic limit).
- **`OpenRouterClient.setFineGrainedToolStreaming(boolean)`** with auto-detection from model id — adds `x-anthropic-beta: fine-grained-tool-streaming-2025-05-14` on the wire when routing to Claude.

### Added — infrastructure & tests

- **PIT mutation-testing pilot** (`-Pmutation-testing` profile in `tnsai-core/pom.xml`) — baselines 74% mutation coverage. Doc: `tnsai-core/agent_docs/mutation-testing.md`.
- **`SourceHygieneTest`** in `tnsai-core` — regression gate forbidding `catch (Exception ignored)` and `catch (Throwable t)` in main sources (issue #10).
- **`ProviderEnvVarConsistencyTest`** in `tnsai-llm` — drift gate for `requireApiKey` call sites + README env-var matrix.
- **Harness evolution audit doc** at `tnsai-core/agent_docs/harness-audit.md`.

### Changed — behaviour

- **`Agent.chat()` rejects calls** when the agent is `STOPPING`, `STOPPED`, or `FAILED` with `IllegalStateException`.
- **`Agent.stop()` is idempotent.**
- **`ExternalScriptHook.apply()`** narrowed `catch (Throwable t)` → `catch (IOException | RuntimeException t)`. JVM-fatal `Error` subclasses propagate now.
- **`TelegramAdapter.send()`** retries 429 / 5xx with exponential backoff (1s, 2s) up to 3 attempts.
- **`BridgeLLMClient.streamChat()`** throws `LLMCapabilityException` (was silent degrade to single-element synthetic stream).
- **`BridgeLLMClient.chat()`** throws typed `LLMException` (was raw `RuntimeException`).
- **`BridgeLLMClient.getCapabilities()`** overrides model-id guess with honest transport-bound limits.
- **`SystemPromptBuilder` state + action sections** aligned byte-for-byte with `RolePromptBuilder`. `@PromptTemplates` + `@State.template` + `@State.invariants` honoured.
- **`LLMConfiguration` env lookups** switched from raw `System.getenv()` to Core's `EnvLoader.get()` (3 sites).

### Removed

- **`AgentState.READY`** — the 0.4.0 seed value (placeholder for the lifecycle FSM that landed in this release). Migrate to `AgentState.RUNNING`. No `@Deprecated` shim per project rule.

### Migration notes

- `AgentState.READY` → `AgentState.RUNNING` (search-and-replace).
- `Agent.chat()` after `stop()` now throws `IllegalStateException`.
- `BridgeLLMClient.streamChat()` now throws — install `tnsai-llm` for real streaming, or call `chat()` for buffered single-shot.
- `BridgeLLMClient.chat()` failures: catch `LLMException` (or parent `TnsAIException`) instead of `RuntimeException`.
- SCOP-rendered prompts changed format. Pinned-format tests should update to the canonical `RolePromptBuilder` shape.

### For Consumers

```xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.tansuasici</groupId>
            <artifactId>tnsai-bom</artifactId>
            <version>0.5.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
```

## [0.4.0] — 2026-04-22

### Major: Monorepo migration

The framework's 11 modules (previously hosted as 11 separate GitHub repositories) have been consolidated into a single `TnsAI-Framework/TnsAI` monorepo. Each module's full commit history is preserved under its subdirectory via `git filter-repo`.

### Added

- **`tnsai-parent`** — root POM aggregating all modules with shared plugin/dependency configuration, release profile, GPG signing, and Maven Central publishing.
- **`tnsai-bom`** — Bill of Materials artifact that pins every `tnsai-*` module to a single coherent version. Consumers import once and use modules without version declarations.
- **`@Capability` pattern** (from former `TnsAI.Core` 0.3.1 pre-release work): reusable action contracts as interfaces with `default` bodies that throw `Actions.dispatchedByFramework()`. See `tnsai-core/src/main/java/com/tnsai/capabilities/Capability.java`.
- **`ActionDiscovery` two-pass scanning**: walks role class methods first, then capability interface chains (including super-interfaces). Class-declared methods win de-duplication; role can override any capability's default with a concrete `ActionType.LOCAL` implementation.

### Changed

- Framework is now released as a single lockstep version. Bumping the version touches only the root `pom.xml`; children inherit via `<parent>`.
- CI consolidated into one `.github/workflows/build.yml` plus `release.yml`. Cross-repo clone / `DEPS_PAT` pattern retired.
- Each child `pom.xml` shrinks from ~400 lines to ~50–100 lines.

### For Consumers

Depend on the framework via the BOM:

```xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.tansuasici</groupId>
            <artifactId>tnsai-bom</artifactId>
            <version>0.4.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>io.github.tansuasici</groupId>
        <artifactId>tnsai-core</artifactId>
    </dependency>
    <!-- pick any module you need — versions come from the BOM -->
</dependencies>
```

## [0.3.0] - 2026-04-18

First coordinated release of the current 11-module shape, published to Maven Central via Central Portal.

> **Corrected 2026-08-01.** This entry used to add that *"previous 0.2.x releases were development-only (per-module Maven snapshots, never on Central)"*. That is not true — `io.github.tansuasici` carries published artifacts for 0.0.2, 0.0.4, 0.0.5, 0.1.0, 0.1.1 and 0.2.0 through 0.2.4, going back to 2025-12-03. The sentence is why nobody backfilled them: it said there was nothing to record. Those ten releases are now documented below, reconstructed from Central.

### Added
- All 11 framework modules (`tnsai-core`, `tnsai-llm`, `tnsai-intelligence`, `tnsai-coordination`, `tnsai-quality`, `tnsai-evaluation`, `tnsai-mcp`, `tnsai-tools`, `tnsai-channels`, `tnsai-integration`, `tnsai-server`) published to Maven Central under `io.github.tansuasici`

### Notes
For change details prior to this coordinated release, see per-module git history under each `tnsai-*/` subdirectory in the monorepo (history was preserved during the 0.4.0 monorepo migration).

## [0.2.4] - 2026-03-26

Published: `tnsai-core`, `tnsai-integration`.

## [0.2.3] - 2026-03-25

Published: `tnsai-coordination`, `tnsai-core`, `tnsai-integration`, `tnsai-intelligence`, `tnsai-llm`, `tnsai-mcp`, `tnsai-quality`, `tnsai-tools`.

## [0.2.2] - 2026-03-15

Published: `tnsai-coordination`, `tnsai-core`, `tnsai-integration`, `tnsai-intelligence`, `tnsai-llm`, `tnsai-mcp`, `tnsai-quality`, `tnsai-tools`.

## [0.2.1] - 2026-03-12

Published: `tnsai-coordination`, `tnsai-core`, `tnsai-integration`, `tnsai-llm`, `tnsai-quality`.

First release carrying `tnsai-coordination`, `tnsai-llm` and `tnsai-quality` — the point the module layout starts resembling today's.

## [0.2.0] - 2026-03-11

Published: `tnsai-core`, `tnsai-integration`.

## [0.0.5] - 2026-02-17

Published: `tnsai-core`, `tnsai-integration`, `tnsai-parent`.

## [0.0.4] - 2026-02-17

Published: `tnsai-core`, `tnsai-integration`, `tnsai-parent`. Same day as 0.0.5.

## [0.0.2] - 2026-02-06

Published: `tnsai-acp`, `tnsai-agui`, `tnsai-browser`, `tnsai-cli`, `tnsai-core`, `tnsai-distributed-redis`, `tnsai-eval`, `tnsai-examples`, `tnsai-integration`, `tnsai-mcp`, `tnsai-memory-vector`, `tnsai-openapi`, `tnsai-parent`, `tnsai-personal-ai`, `tnsai-rag`, `tnsai-research`, `tnsai-spring-boot-starter`, `tnsai-store-mongodb`, `tnsai-store-postgres`, `tnsai-store-sqlite`, `tnsai-tools`.

The widest release the project ever published — 21 artifacts, most of them modules that were later dropped.

## [0.1.1] - 2026-02-04

Published: `tnsai-core`, `tnsai-integration`, `tnsai-parent`.

## [0.1.0] - 2025-12-03

Published: `tnsai-acp`, `tnsai-agui`, `tnsai-cli`, `tnsai-core`, `tnsai-distributed-redis`, `tnsai-eval`, `tnsai-examples`, `tnsai-mcp`, `tnsai-memory-vector`, `tnsai-openapi`, `tnsai-parent`, `tnsai-rag`, `tnsai-spring-boot-starter`, `tnsai-tools`.

The first TnsAI release on Maven Central.
