Changelog
Release notes for the TnsAI framework. Newest version first; each section covers what changed, why, and what consumers need to do to upgrade.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
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
LLMConfigurationSourceputs an external settings layer in front of@RoleSpec.llm()and@AgentSpec.llm(). Register one withSCOPBridge.llmConfigurationSource(...)and it suppliesprovider,model,temperature,maxTokens,endpointorapiKeyEnvper agent, by the agent's own name. Two implementations ship:LLMConfigurationSource.folder(path)reads<folder>/<AgentName>.json, andLLMConfigurationSource.environment()readsTNSAI_LLM_<AGENT>_<FIELD>throughEnvLoader, extending to provider, model, temperature and maxTokens the System-property-before-environment precedence thatLLMConfigurationalready 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 throwsLLMConfigurationExceptionrather 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'sprovidermust name oneLLMConfigurationcan actually route (an unvalidated string would let the source pick the transport, andLLMSpec.Provider.HUGGINGFACEhas no endpoint of its own, so it would fall back to the Ollama default carrying whateverapiKeyEnvcame with it),temperaturemust lie in the 0.0-2.0 range@LLMSpecdocuments (1e40is a finite double that narrows toInfinityas a float),maxTokensmust be non-negative and is parsed withBigDecimal.intValueExact()so that4294967297fails instead of silently narrowing to1, 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:apiKeyEnvnames any environment variable or System property the process can see andendpointdecides where that value is sent, so each resolution logs both when the source sets them (#280).
Changed
SCOPBridge.resolveLLMSpecno 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 amodel— 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 —temperatureandmaxTokens— merges field by field across every tier, so a tier declaring only a temperature now contributes it where before the whole tier was skipped becausehasModel()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 yieldsOptional.empty()and logs the same warning. A tier declaringmodelwithoutprovidertherefore 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@LLMSpecdeclares 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, #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).
- This file states its own one-line rule, so release bodies stop regressing to hard-wrapped text.
release.ymlextracts 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.1and0.16.2were written correctly;0.16.3was hard-wrapped again immediately after#261reflowed the file, because the rule lived only in a merged PR description. It is now written down where the next author reads it. (#278).
Known issues
- A configured
endpointorapiKeyEnvis dropped on the Core SPI client path:LLMClientProvider.createcarries neither, so withtnsai-llmon 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, soSCOPBridgenow logs a warning when that path is taken with either configured. Tracked separately; the real fix is an additiveLLMClientProviderchange.
[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 Centralwithoutcontinue-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 carrycontinue-on-error, the artifact overwrites, and the summary path is asserted rather than falling back to a shared/tmp(ADR-005, #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.compilermodule 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 byparser=structuralandfallbackReason=jdk-compiler-unavailable, while a full JDK continues to use the compiler-backed Java parser. -
ContentExtractorRegistrykeeps 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
JSONLused to fail structural validation (JSON Lines content is blank), which abortedRoleRagBinding.buildand dropped sibling sources in the same Role — a blanktranscript.jsonlmade 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")fromSequentialUnitReader.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
-
SequentialUnitReaderexposessize(),remaining(), andisExhausted().@Sequentialstill consumesnext()before the action body, soremaining() == 0after the last delivered unit is the lifecycle signal to stop scheduling, andisExhausted()(lastnext()returned empty) is the signal to return an application sentinel such asDONE. Hosts no longer count the source file. -
SCOPBridge.executeActioncan take aMap<String, Path>of@KnowledgeSourcename → run-time location, andresolvedSourcePaths(...)sets the same map for subsequent calls. The map is copied onto the dispatch context underRetrievalSpi.RESOLVED_SOURCE_PATHS_KEY;DefaultRetrievalSpiconverts it throughRoleRagBinding.locateDeclaredSources— the same locate-and-override rules asforRole(Class, Map)— so@Retrievalon a source with emptypathgrounds without the action body calling the factory. Unknown names fail at dispatch. Omitting the map keeps annotation paths only.
Fixed
ResearchRoleJavadoc no longer teaches pre-chunking Phase 1 RAG (hash-only embeddings, whole-file ingest, deferred rerank / expansion / cache). It now matches the liveRoleRagBinding/FileIngestionServicepath 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_0OpenRewrite recipe (dev.tnsai.rewrite). Rewrites consumer coordinates todev.tnsaiand marks every call site of the APIs 0.16.0 removes or changes — theKnowledgeBasebridge, the two getters,ChatKnowledgeBinding.snapshot, the six-argumentIdempotencyResolver.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
@KnowledgeSourcecan 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 leavespathempty. 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 durableRedisIdempotencyStore/PostgresIdempotencyStoreimplementations 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 aKnowledgeSourceConfig. Initialization resolves nothing for such a source and creates no retrieval engine on its account; the agent starts ungrounded untilAgent.setChatKnowledgeBindingsupplies aChatKnowledgeBinding.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.setKnowledgeBasebridge, 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
@ChatKnowledgepath, builder-over-annotation precedence, and action-leveladdKnowledgeSource/knowledgeSources/retrievalare unchanged. -
SCOPBridge.prepareConversationForDispatchcreates a transport-neutral, request-local conversation copy with freshly rendered@Statevalues. 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
RoleRagBindingindexes (per-source@Retrieval(sources = ...)andstrategyFor(..., 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.awssdk2.44.12 → 2.54.1 (#219)com.fasterxml.jackson2.22.1 → 2.22.2 and 3.2.1 → 3.2.2 (#212)com.squareup.okhttp35.4.0 → 5.5.0 (#213)io.opentelemetry1.64.0 → 1.65.0 (#221)io.micrometer:micrometer-registry-prometheus1.16.5 → 1.17.1 (#220)org.mongodb:mongodb-driver-sync5.9.2 → 5.10.0 (#215)com.github.pengrad:java-telegram-bot-api9.6.0 → 10.1.0 (#216)ch.qos.logback:logback-classic1.6.2 → 1.6.3 (#214) — see Security
-
Build-only:
org.openrewrite:rewrite-bom8.89.1 → 8.90.4 (#235),spotbugs-maven-plugin4.10.3.0 → 4.10.4.0 (#217),actions/setup-java5 → 6 (#236). No effect on published artifacts. -
BREAKING (API):
IdempotencyResolver.execute(...)takes an additionalboolean strictStoreWritesand the six-argument form is gone. In-repo callers are updated; external callers passfalseto keep the previous behaviour, ortrueto 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
KnowledgeBasebridge is removed — ten declared members across five types (thirteen consumer-visible, as japicmp counts: interface defaults plusMETHOD_REMOVED_IN_SUPERCLASS):AgentBuilder.knowledgeBase(...),AgentBuilder.knowledgeBaseTopK(...)Agent.setKnowledgeBase/getKnowledgeBase,Agent.setKnowledgeBaseTopK/getKnowledgeBaseTopKAgentOrchestrator.setKnowledgeBase/setKnowledgeBaseTopKand their gettersAgentChatOrchestrator'sgetKnowledgeBase/getKnowledgeBaseTopKSPI defaultsChatKnowledgeBinding.snapshot(...)andChatKnowledgeBinding.SNAPSHOT_SOURCE
The
KnowledgeBasetype itself is not removed, nor is anything else incom.tnsai.knowledge. What goes is the bridge — the public entry points that attached a corpus to chat outside the canonical retrieval configuration.KnowledgeBaseRetriever,knowledgeBaseConfigand theChatKnowledgeBinding.create/livefactories all stay;live(...)is now the documented way to bind such a corpus.See Migration for the replacement.
-
BREAKING (coordinates): the
io.github.tansuasicirelocation POMs are no longer published (#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 declaringio.github.tansuasiciand 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 — theRetrievalQualityRegressionTestgate 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 Statesections 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). CI only; no library change. Other causes of those failures are tracked separately and not all of them are closed.
-
The
idempotencyHint = REQUIREDreliability gate now matches its own documentation: the key-strategy check is an allowlist (HASH_INPUTorEXPLICIT, asTool's Javadoc states) instead of a denylist, anEXPLICITpolicy is rejected at the gate when the tool target does not implementIdempotencyKeySupplier(with a message naming the fix), a failed store write after the guarded body ran surfaces asIdempotencyExceptioninstead of a warn line for REQUIRED tools, and running REQUIRED on the default in-memory store logs a startup-visible warning namingAgentBuilder.idempotencyStore(...).
Security
logback-classic1.6.3 carries the upstream response to CVE-2026-19880, which affectsMDCBasedDiscriminatoras used bySiftingAppender. TnsAI does not configure aSiftingAppenderitself; the bump matters for applications that do (#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:
<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:
AgentBuilder.create()
.knowledgeBase(corpus)
.knowledgeBaseTopK(5)
.build();now declares an application-supplied chat source and binds the corpus at runtime:
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.tansuasicitodev.tnsai(the reverse of tnsai.dev). Artifact IDs stay (tnsai-bom,tnsai-core, …). Java packages staycom.tnsai.*. 0.15.0 and earlier onio.github.tansuasicistay immutable. This release also publishes relocation POMs atio.github.tansuasici:*:0.15.1so a version bump follows with a Maven warning. Mechanical POM rewrite:dev.tnsai.rewrite.ChangeMavenGroupId.
Migration
<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:
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 builderKnowledgeSourceConfigto chat using the builder'sRetrievalConfigand top-K. This enables runtime-configured finalConfigurableAgentinstances without class annotations while rejecting unknown, duplicate, disabled, or legacy-conflicting sources. Existing action-levelknowledgeSourcesandretrievalsemantics remain unchanged; chat never guesses among multiple sources.SCOPBridge.principal,liabilitySink,authorityScope, andgetInstance(principal, sink, scope)are the public accountability wiring surface forexecuteAction. There is no silent no-op sink.RagDiagnosticsreports what a Role's@KnowledgeSourceand@Retrievaldeclarations 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, defaultAUTO) declares how a file becomes documents —DOCUMENT,LINE,HEADING,CHUNK.AUTOis the existing size-driven hybrid and remains byte-identical to the previous ingest path.LINEemits one position-addressed unit per non-blank line,HEADINGshares the Markdown#{1,6}boundary rule,DOCUMENTrequires the file to fit one retrieval unit, andCHUNKapplies the shared chunker even to small files. Invalid unit/format pairs fail before indexing. A non-AUTOunit is a fingerprint field, so changing the declaration rebuilds and re-embeds that source without globally bumpingCHUNKER_VERSION.- Opt-in contextual retrieval on
@KnowledgeSource.chunkContext.NONE(default) omits the extra fingerprint fields.STRUCTURALprepends origin/path/lines before embedding and BM25;LLMuses an installedChunkContextGenerator(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 atChunkContext.MAX_PREFIX_CHARS. Sequential reads, chat snapshot/live evidence, and both renderer overloads restoreChunkContext.DISPLAY_BODY_METADATA_KEY. A generator failure mid-batch does not publish a partial index.unit = LINErejects any non-NONEcontext. NormalizedDocumentChunkeroverlaps 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.LINEandHEADINGunits also suppress overlap: they emit position-addressed slices, not repeated windows.CHUNKER_VERSIONis now3; existing indexes must be rebuilt.NormalizedDocumentChunker.MAX_LINES_PER_DOCUMENTstates the document ceiling that was previously implicit inMAX_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, soLINE/HEADINGand 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:
MultiRoundContractNetcan no longer fabricate proposals: theMath.random()-based defaultrequestProposal()is deleted and the builder requires a caller-suppliedProposalRequester(contractor, task, round, target score, timeout) — constructing without one fails fast with a message naming the missing input.ContractNetAdapterlikewise needs a requester: the no-arg default registration now fails fast innegotiate()with a reason naming the missing input, and the one-arg constructor takes the source.proposalTimeoutis now handed to the requester. There is notnsai-rewriterecipe: the mechanically rewritable half of this break (subclass overrides ofrequestProposal) 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 whosecontractorIdnames a different contractor than the one asked is dropped, and — symmetrically, since the award decision trusts the proposal's economics — a proposal whosetaskIdbelongs 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 bothproposalTimeoutand the remaining session budget, and a missing, zero, or negativeproposalTimeoutnow 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;onContractAwardedlisteners 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 declaredstrategy+queryExpansionpair. The wording skeleton is shared withRerankerRegistryso 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 noEmbeddingFunctionprovider is installed, the Role warns once.KEYWORD,GRAPHandREASONINGstay silent — they do not depend on vectors. Acknowledge the fallback deliberately with-Dtnsai.rag.acknowledgeHashEmbedding=true.- A mistyped
rerankerModelorqueryExpansionModelno longer reaches the first query as a retrieval failure. Both resolve at the start of the retrieval — before any index work and outside theonFailureguard — and reportPROVIDER_UNAVAILABLE, so aCONTINUEorUSE_CACHEpolicy cannot absorb a deployment fault. Failures a provider raises while ranking or expanding still followonFailure.supports()must be answerable without a network call. SCOPBridge.executeActionrequires explicit principal, liability sink and authority scope through publicprincipal/liabilitySink/authorityScope(orgetInstance(principal, sink, scope)). Missing wiring fails before retrieval or the action body, so a@Sequentialcursor cannot advance and still return an accountability error. There is no silent no-op sink.SCOPBridge.sendToLLMgrounds from an exact-ownerChatKnowledgeBindingeven when the owner has no@ChatKnowledge, matching builderchatKnowledgewith the annotated SCOP path. The bundled server attaches the sessionRagServicecorpus throughAgent.setKnowledgeBaseinstead 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
ClassValueso disposable Role classloaders are not strongly retained. ContentExtractorRegistrytimeout now cancels the workerFuture(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
RetrievalEngineinstances 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 publicRetrievalEngineclose 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 PostgreSQLTEXTnever 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/CHUNKfixed-size windows) genuinely chunk differently — windows now overlap by 15 lines. LINEandHEADINGunits 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+@Retrievalon a non-Roletarget dispatched throughSCOPBridge.executeActionnow run before the method body._rag_contextis written, and a blank LOCAL return surfaces the retrieved text as narration. A non-blank method return still wins. No second retrieval engine (#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
-
BackwardChainingPlannerno 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 raisesPlanningFailureException, whosereason()separates anUNREACHABLEgoal from aCYCLIC_PRECONDITION, an exhausted depth budget, and an exhausted search budget;goalName()andmaxDepth()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()andgetPlan()surface the failure instead of a silent no-op. -
Tool registry name keys now use one trim +
Locale.ROOTnormalization contract for registration, lookup, removal, and search, preventing default-locale failures such as TurkishI→ıwhile preserving original display names and distinct Unicode spellings. Registrations that would silently shadow an existing normalized alias are rejected atomically. -
@Contractis now the sole runtime pre/postcondition gate. The STRIPS-style@ActionSpec.precondition/postconditionfields remain planner metadata and no longer change dispatch behavior based on whethertnsai-qualityhappens to be present; explicit@InvariantCheckstill validates@Stateinvariants after execution. -
@LLMSpecPhase 1 fieldsendpoint,apiKeyEnv,timeoutMs,frequencyPenalty, andpresencePenaltynow reach OpenAI and Ollama clients instead of being silently dropped. Other providers fail loud if those fields are set.fallbackModel/streaming/systemPromptremain Phase 2. -
AgentOrchestratorno 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. -
ServerShellToolsdrains stdout and stderr on separate threads beforewaitFor, matchingServerGitTools. 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 setsSandboxResult.outputTruncatedso a looping write cannot OOM the host JVM.ProcessSandboxandContainerSandboxshare the same pump.
Added
-
@InputGuardrailvalidator and sanitizer declarations now execute before action dispatch. Built-inPiiInputValidator/PiiInputSanitizercan 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. -
FeatureFlagSPI (isEnabled(flag, FlagContext)) with an env default (TNSAI_FLAG_<NAME>=on|off|0-100, optional_ALLOWtenant/agent list). Unset new flags are off.TNSAI_FLAG_GOAP=offskips planner auto-discovery; unset keeps today's classpath discover. No SaaS vendor. -
tnsai-serverhonours an opt-inIdempotency-Keyheader on POST/PUT/PATCH. The first 2xx is stored in the existingIdempotencyStore(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. -
DoclingToolsadds optional advanced document parsing through either the official Docling MCP server or a locally installeddoclingCLI. The immutableDocumentResultpreserves Markdown, tables, formulas, figures, and lossless Docling JSON;PdfTools.pdfToImagecan use configured Docling page exports when PDFBox cannot open a document. -
SpiLoaderprovides 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
@Toolmethods can declaretimeoutMsfor an independently enforced invocation deadline. Expiry raises the existing retryableToolTimeoutException; the default keeps current agent-level behavior. -
SelectiveReembedIndexkeeps 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 productionVectorIndexbackends. -
SmartDocumentSegmentersplits long papers on headings, keeps figure captions with their section, resolvesSection X.Yreferences, 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, andKnowledgeEntrytypes are removed. Shared working memory iscom.tnsai.communication.SharedBlackboard. The only publicKnowledgeSourcetype is the RAG annotationcom.tnsai.annotations.KnowledgeSource. -
Chat grounding always uses
ChatKnowledgeBinding/RetrievalEngine.AgentBuilder.knowledgeBase(...)andAgent.setKnowledgeBase(...)install a snapshot binding (source=knowledge-base) instead of a parallelKnowledgeBase.searchpath inAgentChatOrchestrator. 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/.tnsignoreapply 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 onFileIngestionServiceandFileIndexer. -
BREAKING: Explicit
format=PDF(and the other office formats) with unreadable content now fail asDocumentProcessingExceptionat extraction after the office extractor is registered. Callers that caughtIllegalArgumentExceptionfor 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 throwsIllegalStateExceptionwith a migration hint.provider = "inmemory"(the default) still works and now searches through a realVectorMemoryStoreviaVectorStoreProvider.
Fixed
-
Backward chaining can apply more than one helper for the same target precondition (for example
a && bvia two setters) instead of returning an empty plan. -
Tenant-scoped agents now bind
AgentBuilder.tenantId(...)to a nested, automatically clearedTenantContextfor public turns and lifecycle work.TenantAwarememory 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/consumeshare one atomic lifecycle. An expired approved token cannot be consumed, a consumed single-use token cannot be resurrected by a concurrent approve, and bothActionExecutorandInMemoryApprovalTokenStorerequire a successfulconsume()before dispatch. Multi-use tokens stay reusable. Authorization failures do not log token-bound identifiers. -
sql_queryrejects modifying CTEs andSELECT INTO, and opens the JDBC session read-only so a write cannot commit even if a dialect accepts aWITH … DELETEprefix. -
@Resiliencetimeouts run on a dedicated daemon pool and cancel a realFuture, so the worker is interrupted instead of continuing onForkJoinPool.commonPool()after the caller already failed. -
DefaultGroupEventBus.unsubscribeAll(agentId)now removessubscribeFor/subscribeFromregistrations for that agent instead of always returning 0. -
@ContractJEXL bindings now flatten public record components one level, sofrom.balanceworks ontransfer(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, matchingContextManagerHandle.noOp()/FeedbackCollector.noOp(). Agent init no longer constructsNoOpPaymentBrokerat call sites. -
LLMRoleExecutorthrowsActionExecutionExceptionwhen the action context has noLLMClient. The previousreturn nullwas logged as a fallback but@Fallbacknever 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
putIfAbsentrace loser no longerclear()s the remote store. -
ContentExtractorRegistry.discover()andFileIngestionServiceshare oneMETA-INF/servicessnapshot. 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. -
ReflectiveNeo4jSessionnow iterates Neo4jResultas anIterator(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
-
VectorStoreProviderSPI plus a bundledinmemoryimplementation. 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. -
InMemoryGraphStoreplus an optionalNeo4jGraphStoreProvider. FILE@KnowledgeSourceRoles get a zero-config in-process graph with chunk↔entity provenance and identity collapsing (Auth Service/the auth service). Neo4j opens only whentnsai.graph.neo4j.uriis set andorg.neo4j.driveris on the runtime classpath — it is not bundled intotnsai-coreand is not a required intelligence dependency.KnowledgeToolstriples 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. Defaultmvn teststays offline;mvn -pl tnsai-evaluation -Pembedding-bench testremeasures through local Ollama. Measured 2026-08-15 (seetnsai-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
ContentExtractorSPI. PDFBox and Apache POI stay<optional>true</optional>ontnsai-intelligence; EPUB uses the JDK zip API plus jsoup. Missing backends keep the typedUNSUPPORTED_FORMATerror on both single-file and directory paths. -
HybridRAGStrategyRRF-fuses a third knowledge-graph stream when aGraphStoreis available.@Retrieval(strategy = HYBRID)stays the name; a missing store keeps the BM25+vector fuse and recordsHybridGraphStream.SKIPPED_NO_GRAPH_STOREinstead of silently becoming VECTOR-only. Hits carryretrievalStreams(bm25/vector/graph) plus existing GraphRAG provenance. -
Optional Qdrant vector backend.
VectorMemoryStoreaccepts aVectorIndex;QdrantVectorMemoryStoretalks REST throughQdrantTransportwhentnsai.vector.qdrant.url(orTNSAI_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 onsize()and search surface as errors rather than an empty index. The official Qdrant client is not atnsai-coreor requiredtnsai-intelligencedependency, and no new parent-POM module is added. -
Optional pgvector backend on the same
VectorIndexseam.PgvectorVectorMemoryStoretalks JDBC throughPgvectorTransportwhentnsai.vector.pgvector.url(orTNSAI_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.matryoshkaderives an L2-normalized prefix from any coreEmbeddingFunction, allowing Matryoshka-compatible models to build smaller in-memory indexes without a second provider.VectorMemoryStorepins its first admitted dimension and rejects mixed add/query vectors withEmbeddingDimensionMismatchExceptionbefore mutation or search. -
BREAKING:
@RetrievalandRetrievalConfiggainREASONINGandnavigatorModel. Annotation users can select the already-shippedReasoningRAGStrategy; a blank model or an identifier noTreeNavigatorserves fails at strategy selection, above@Retrieval.onFailure.RetrievalConfiggrows one record component (builder default remains blank). Consumerswitchstatements overRetrieval.Strategystop 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.
IngestionResultgainsfingerprints; the previous eight-argument form is gone.KnowledgeBase.replaceDocumentsis the atomic swap;InMemoryKnowledgeBaseandBM25Streamhold their locks across the generation. -
@AgentSpec.knowledge()and@AgentSpec.retrieval()are the annotation counterparts ofAgentBuilder.addKnowledgeSource/retrieval(). Emptyknowledgeand an all-default@Retrievalare no-ops; builder-explicit values still win. Duplicate source names fail initialization. -
BREAKING:
@KnowledgeSourceandKnowledgeSourceConfiggainincludeandexcludeglobs 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— whentnsai-toolsis on the classpath,@Retrieval(strategy = GRAPH)reads liveKnowledgeToolstriples. Each tool instance still keeps its own bag; GraphRAG unions every live instance on retrieve, sokg_add_tripleis 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 fromHIERARCHICAL, which takes similarity hits and expands upward through deterministic parent edges; both read the sameHierarchyIndex, 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:
maxNodesVisitedcaps the descent across all roots,maxDepthcaps 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 inhierarchyStopReason, because a partial answer from a real subtree beats failing over one bad step.RAGContext.metadataFiltersandmaxResultsare applied; there is no score model, so landed nodes carry a constant score andminScoreadmits them all. Selecting it through@Retrievalcomes with the enum value, which is a separate change. -
TreeNavigatorSPI — 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 followsQueryExpanderandRerankerexactly: providers are discovered throughServiceLoader, 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 intasks/specs/2026-08-12-reasoning-rag/spec.md. -
Core RAG now defines a dependency-light
ContentExtractorSPI with immutable byte input, normalizedExtractedDocument/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
topKandcontextWindowselect relevant sections instead of whole files. The server file indexer uses the same core language/heading/fallback implementation, while the existing publicCodeChunkerAPI delegates to that canonical implementation. -
BREAKING:
@Retrieval.queryParamnames the action parameter that carries the retrieval query, andRetrievalConfiggains 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 firstStringparameter in declaration order. That rule is right for a single-parameter action and positional for any other, soanswer(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 theonFailureboundary, so a typo cannot be absorbed intoCONTINUEand answered ungrounded; a declared parameter carrying no usable value skips retrieval as before. Actions with more than oneStringparameter 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 aftersourcesto mirror the annotation. Callers usingRetrievalConfig.from(...),RetrievalConfig.builder()or the annotation are unaffected. This is the same shape asKnowledgeSourceConfiggainingformatearlier in this release.CanonicalRagConfigTestenforces 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. -
SCOPBridgenow offers an additive owner-aware chat dispatch overload that validates@ChatKnowledgeagainst 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.ChatKnowledgeBindingadds exact-ownercreate/liveoverloads 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
EmbeddingFunctionthroughMETA-INF/servicesand 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
DocumentFormatcontract.@KnowledgeSource,KnowledgeSourceConfig, and its builder default toAUTO; explicit textual formats validate registered extension aliases before any source file is read, whileTEXTintentionally 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-nothingIOException. Declarativeingest()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
AUTOsources 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 typedKnowledgeSourceFormatExceptionwhen content contradicts the declaration; XML validation disables DTD and external-entity access. -
Action-level retrieval routes through the unified
RetrievalEngineinstead 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.Paramis deleted. It never did anything. Nothing in the framework readParam.class, no annotation nested aParam[]member, and@ToolSpec— the usage its own Javadoc documented it against — had already been removed.ActionDiscovery.discoverParametersbuilds everyParamSpecfromParameter.getName()under the-parameterscompiler 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
@WebServicecarried@Param(name = "q") String query— readers were told the model would seeqwhen it always sawquery.
Fixed
-
@Contracton a method without@ActionSpecnow fails action discovery with the method FQN instead of being skipped as a helper, so the clause cannot silently never run. -
api-compat-check.shno longer reports success when japicmp fails.$?afterif ! mvnwas the negation (0), so the release gate printed "build passed" on aBUILD FAILURE. -
LocalFileSourceLoadernow usesContentExtractorRegistry.discover()instead of a hard-coded bundled extractor. A consumerContentExtractorwins the formats it claims;StructuredTextContentExtractoris 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.orgintotarget/japicmp-baseline/somvn installcannot shadow it, andapi-compat-check.shfails when old and new jars have the same digest. -
InMemoryKnowledgeBase.searchno longer serializes concurrent queries on the mutation monitor. Mutations take a write lock; search, embedding search,getDocumentandsizetake a read lock, so readers overlap while incremental DF stays consistent. -
The bundled
KnowledgeToolsGraphStoreProviderno longer makes a consumerGraphStoreProviderambiguous. 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
avgdlon each chunk.InMemoryKnowledgeBasekeeps incremental document frequencies and applies IDF at query time;BM25Streammaintains a running token total.FileIndexeradds a file's chunks in oneaddDocumentscall. -
LLMRoleExecutornow reads_rag_stale_fallbackand_rag_context_truncated. StaleUSE_CACHEdocuments are still spliced but labelled as expired, so they cannot be read as fresh grounding. AcontextWindowthat drops every matched document fails withVALIDATIONinstead of answering as if the corpus were empty. -
Bundled-server
AgentFactoryno longer silently substitutes the default LLM whenAgentAddnames a provider other thanollama. Unknown providers now fail withAGENT_ADD_FAILED(matchingTnsServerMain), and a successfulAgentStatereports the live client rather than the requested name. -
@Retrieval.metadataFiltersis now applied by the memory-backed strategies.VectorRAGStrategy,KeywordRAGStrategyandHybridRAGStrategyconverted 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 themaxResultscut, and the candidate pool widens when filters are present so a filtered query still returns up tomaxResultsdocuments 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.0over the non-empty keyword/vector streams before applying@Retrieval.minScore. The defaultminScore = 0.5therefore 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:
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_0recipe — migrates consumer code across this release's 61 type moves withmvn rewrite:run, so the three package consolidations below cost an import review rather than a manual sweep: the agent-group contractscom.tnsai.coordination.groups.*→com.tnsai.agents.groups.*, the quality-ownedcom.tnsai.{observability,security,validation.parallel}.*→com.tnsai.quality.*, and the duplicateevaluation.evaluators.agentic.*→ the canonicalevaluators.agent.*set.UpgradeTnsAI_0_12_0is 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.auditis split across two modules —FileAuditStoreandInMemoryAuditStoremoved whileAuditEvent,AuditQueryandAuditStorestayed — andcom.tnsai.security,com.tnsai.observabilityandcom.tnsai.coordination.groupseach kept members too. AChangePackageover 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 deletedcom.tnsai.security.sandboxpair has no replacement to rewrite to, and the@Retrievalmembers that stopped being inert change behaviour without changing any import — auto-inserting an opt-out likecache = falsewould 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 immutableKnowledgeSourceConfig/RetrievalConfigbuilder APIs, while chat-level retrieval supports@ChatKnowledgeover a named@KnowledgeSource. Both paths preserve the existingRetrievalSpiinvocation andKnowledgeBasecontracts. A builder declaration and the equivalent annotation converge on oneRetrievalConfigbefore validation, provider resolution, or cache lookup, so the two spellings retrieve identically and share oneRetrievalResultCacheentry.@Retrieval.topKis validated before retrieval runs, alongside the existing reranking, caching, failure-policy, expansion, deduplication, and context-assembly checks. A non-positivetopKretrieves 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_0migrates the 0.12.0 breaking renames (com.tnsai.identity.AgentSpecrecord →AgentDescriptor;@com.tnsai.roles.annotations.RoleIdentityannotation →@RoleDeclaration), run via therewrite-maven-plugin. Seetnsai-rewrite/README.mdfor the invocation.tnsai-llm: task-aware embeddings — newEmbeddingTaskenum andEmbeddingProvider.embed(text, task)/embedBatch(texts, task)default overloads.OllamaEmbeddingProviderprepends 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 — newRerankerSPI (supports(model)/rerank(Request), discovered viaServiceLoader) wired into the@Retrieval(rerank, rerankerModel, topN)path byDefaultRetrievalSpi. The runtime validates the provider's output (no documents outside the candidate set, no nulls) and appliestopNafter 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 — includingrerank,rerankerModelandtopNalongside the Role, sources, strategy,topKandminScore.tnsai-core: retrieval provenance in the action context — newRetrievalSpi.RETRIEVED_STALE_FALLBACK_KEY(_rag_stale_fallback, Boolean), written by the same path that writes_rag_context:falsefor fresh grounding,truewhen@Retrieval.onFailure = USE_CACHEserved 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_contextand a positive_rag_document_count, whichLLMRoleExecutor's splice guard reads as "fully grounded" — so executors, interceptors, and evaluators had no way to surface or refuse degraded grounding.ActionExecutorscrubs the key from a caller-reused context alongside the other three reserved keys.tnsai-intelligence: model-backed query expansion — newQueryExpanderSPI (supports(mode, model)/expand(Request), discovered viaServiceLoader) andMultiQueryRAGStrategy, 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 index0and never counts against the variation budget; provider output is normalised, deduplicated case-insensitively, and truncated toexpandedQueriesas 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 — newRetrievalSpi.RETRIEVED_CONTEXT_TRUNCATED_KEY(_rag_context_truncated, Boolean), written by the same path that writes_rag_context:falsewhen every retrieved document reached the context in full,truewhen thecontextWindowforced 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— whichLLMRoleExecutor's splice guard reads as "retrieval matched nothing" while the model answers ungrounded. The new key separates the two: count0with the flagfalseis an empty corpus, count0with the flagtrueis acontextWindowtoo small to hold anything, and that case additionally logs at WARN.ActionExecutorscrubs the key from a caller-reused context alongside the other three reserved keys.tnsai-intelligence: graph retrieval SPI — newcom.tnsai.intelligence.rag.graphpackage with theGraphStoreProviderSPI (name()/open(Request), discovered viaServiceLoader), the vendor-neutralGraphStoredata contract (findSeeds/neighbors, plus theNode,Edge,SeedandNeighborrecords), andGraphCapabilityException. 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 returnsOptional.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 — newcom.tnsai.intelligence.rag.hierarchypackage with theHierarchyMetadatakey vocabulary (hierarchyId/hierarchyParentId/hierarchySourcedeclared by loaders, andhierarchyHitId/hierarchyDepth/hierarchyPath/hierarchyFlat/hierarchyTruncated/hierarchyStopReasonemitted as provenance), theHierarchyIndexdocument index with itsvalidate()configuration gate, andHierarchyException. TheSourceLoaderSPI gains ametadata(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 theSourceLoaderparameter change below; themetadatahook itself is what stays optional.)HierarchicalRAGStrategyuses 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.sourcesfence — so two deployments over the same documents rank identically.tnsai-intelligence: temporal retrieval — newcom.tnsai.intelligence.rag.temporalpackage with theTemporalMetadatakey vocabulary (temporalTimestampdeclared by loaders andtemporalSourceassigned by the binding, plustemporalContentScore/temporalDecay/temporalScoreFactor/temporalAgeSeconds/temporalDated/temporalStatus/temporalFutureSkewSecondsemitted as provenance), theTemporalIndexdocument index with itsvalidate()configuration gate, andTemporalException.TemporalRAGStrategyuses 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.sourcesfence — so two deployments over the same documents rank identically. TheClockis injectable, so the ranking is reproducible in tests.tnsai-core: unified retrieval engine contract — new public types incom.tnsai.raggiving chat and action retrieval one shared shape:RetrievalEngine(a final, agent-scopedAutoCloseablewrapper that enforces the invariants and delegates to aRetrieverbackend), theRetrievalEngineProviderSPI discovered viaServiceLoader,RetrievalRequestwith its builder,RetrievalScope,RetrievalResult,RetrievalEvidence(with nestedProvenance),RetrievalDiagnostics, andRetrievalException. 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 returnsRetrievalEvidencerather than assembled prompt text and so has nothing to bound; evidence volume is governed bytopK,topN, andminScore. Whichever entry point renders evidence into a prompt enforces the window and reports truncation through its own channel. Left unsaid,contextWindowwould 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.Reasonnow carriessurfacesAboveFailurePolicy():CONFIGURATION,PROVIDER_UNAVAILABLE, andENGINE_CLOSEDare declaration or lifecycle faults that must reach the caller, whileTIMEOUTandEXECUTIONare transport faults@Retrieval.onFailuremay 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.
RetrievalScopegives 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 becausemetadataFiltersis an unordered immutable map whose iteration order varies between JVM runs, entries are sorted before digesting.
- Windowing is the caller's. The engine neither validates nor applies
tnsai-intelligence: unified retrieval runtime —DefaultRetrievalEngineProviderimplements theRetrievalEngineProviderSPI and is registered throughMETA-INF/services, soRetrievalEngineProvider.discover()now returns a working engine instead of nothing. Everycreate(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 returnsRetrievalEvidencewith 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.Reasonfirst and only then routed onsurfacesAboveFailurePolicy(), so a declaration fault cannot reach@Retrieval.onFailure—CONTINUEwould report success on an ungrounded answer, andUSE_CACHEwould answer from stale evidence while the declaration stays unhonourable. Branching on the exception type instead would route both classes the same way, since both areRetrievalException. 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 beyondmaxFutureSkew), are clock- and request-dependent, stay transport-class, and do reach the policy. Cancellation is checked before the predicate and propagates unconditionally. contextWindowis 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
RetrievalScopeand 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_SIMPLEcaps its retry attopNwhenever the primary pipeline was reranked: the retry deliberately runs without reranking, and reranking is the only placetopNis 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 ownDefaultRetrievalSpipipeline — migrating them onto this engine is later work in this release, for action and for chat.
- Every failure is classified into a
Changed
-
BREAKING:
@KnowledgeSourcenow declares only what it can deliver: ingestion. Eleven elements are gone —provider,index,topK,minSimilarity,embeddingModel,dimensions,namespace,filter,cache,cacheTTL,priority— leavingname,type,path,connection,queryandenabled.KnowledgeSourceConfig(added this release, never published) narrows with it, from 17 record components and 15 builder setters to 6 and 4.UpgradeTnsAI_0_13_0strips the removed attributes from your declarations. Removing them preserves behaviour rather than changing it. Not one was ever read:git grepfinds 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. ASourceLoaderruns 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, sotopK,minSimilarity,cache,cacheTTLandprioritycould never have worked here — and@Retrievalalready 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:
@Retrievaland@KnowledgeSourceno longer declare the same knobs.topK,cache,cacheTTLand the score threshold existed on both with no rule for which won — and the thresholds even disagreed on their default,@Retrieval.minScore0.5 against@KnowledgeSource.minSimilarity0.7.@Retrievalis 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.typedefaults toFILEinstead ofVECTOR_DB. The old default named a type the framework ships no loader for, so omittingtypefailed 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 toinvalidate(Class)and is now a supported operation. It shippedpublicin 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. Callinvalidate(roleClass)after the corpus behind a Role's@KnowledgeSources changes; the next dispatch re-ingests, and retrieval already in flight finishes against the snapshot it started on.UpgradeTnsAI_0_13_0migrates the call. Worth being explicit about what this does not mean:cacheTTLnever 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 — socache = falsewas never a workaround for a changed corpus, only a way to pay for the same answer more often. -
BREAKING:
SourceLoadernow consumes the canonicalKnowledgeSourceConfigruntime model instead of reflection-backed@KnowledgeSourceannotation instances. Annotation and builder declarations are normalized before loader invocation, preventing two divergent loader configuration paths. Bothload(source)and the optionalmetadata(source, document)hook change their parameter type, so an out-of-tree loader must be recompiled againstKnowledgeSourceConfig. -
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, aminScoreoutside0.0..1.0or non-finite, a blank name in@Retrieval.sources, a blank@KnowledgeSource.name, and a non-positivetopK,dimensions, or (with caching on)cacheTTLon@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-qualityundercom.tnsai.quality.*, ending the half-finished migration that leftcom.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.groupssplit package — the 16 agent-group contracts (AgentGroup,AgentGroupFactory,GroupRegistry,MembershipManager, …) shipped fromtnsai-corewhile their implementations ship fromtnsai-coordination, so the same package came from two modules (blocks JPMS, muddies layering). The contracts moved to the core-ownedcom.tnsai.agents.groups(joiningAgentGroupManager);com.tnsai.coordination.groupsis now coordination-only (impls + topology subpackages). A new ArchUnit guard (GroupsPackageArchitectureTest) keeps the contracts from leaking back. -
BREAKING:
@Retrieval.rerankis enforced instead of ignored. Until nowrerank = truewas accepted and silently dropped — retrieval injectedtopKdocuments in strategy order and the annotation was decorative. It now resolves aRerankerprovider, and because the model identifier is annotation configuration rather than request data, a blankrerankerModel, a non-positivetopN, 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 throughCONTINUEset_rag_document_count = 0and left_rag_contextunset, whichLLMRoleExecutor'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 aRerankerprovider module on the classpath serving itsrerankerModel, orrerank = false— including annotations copied from the@Retrievaljavadoc example, whose"provider:model-id"is a placeholder. -
BREAKING:
@Retrieval.cacheis enforced instead of ignored. Until nowcacheandcacheTTLwere accepted and silently dropped — every dispatch re-ran retrieval. Becausecachedefaults totrueandcacheTTLto 300, every existing@Retrievalnow serves results up to five minutes stale without any annotation change; setcache = falseto keep the previous per-dispatch behaviour. Following thererankprecedent above, a non-positivecacheTTLis annotation configuration that cannot be honoured, so it is a configuration error that fails the annotated action at dispatch time rather than somethingonFailure = CONTINUEcan 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 misconfiguredrerankerModelto 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_CACHEandRETRY_SIMPLEare implemented instead of degrading toCONTINUE. Until now both enum constants were accepted and quietly handled asCONTINUE— a retrieval failure logged a warning, set_rag_document_count = 0and 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; setonFailure = CONTINUEexplicitly to keep the old behaviour. Specifically:USE_CACHEfalls back to the expired result-cache entry for the exact same key — every document is taggedtnsai.retrieval.provenance=stale-cache, and the dispatch is flagged in the action context under the newRetrievalSpi.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 thererank/cacheTTLprecedent above,onFailure = USE_CACHEwithcache = falseis a contradiction the annotation cannot honour and is now a configuration error that fails the annotated action at dispatch time.RETRY_SIMPLEmakes exactly one further attempt, againstStrategy.KEYWORDwith 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. Underrerank = truethat retry is still capped attopNeven though no reranker runs —topNis otherwise enforced only inside the reranking stage, so an uncapped fallback would inject up totopKdocuments, more context than the healthy reranked path delivers and at the moment relevance ordering is at its worst. Underrerank = falsethe cap does not apply:topNis unvalidated there and no other path honours it, so capping would let atopN = 0annotation 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_CACHEserving a hit, andRETRY_SIMPLEsucceeding — 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 atDEBUGinstead. The diagnostic gap is widest on the latter two: both return a populated_rag_contextand a positive_rag_document_countand let the action succeed, so the warning is the only evidence retrieval failed at all. To keep stale-if-error possible,RetrievalResultCachenow retains expired entries until a successful refresh, an explicit clear, or LRU eviction, instead of purging them on read. -
@Retrieval.maxStalenessbounds how long past expiry a cached result may still serveonFailure = USE_CACHE, defaulting to 3600 seconds.cacheTTLbounds 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 > 0and a populated_rag_contextread as fully grounded. The two windows are measured in sequence, not from the same instant: an entry is fresh forcacheTTL, eligible as a stale fallback for a furthermaxStaleness, 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 HTTPstale-if-error(RFC 5861), whose max-age argument likewise counts from the end of the freshness lifetime.maxStaleness = -1opts out and restores an unbounded fallback; any other non-positive value is rejected whenUSE_CACHEis selected, on the same terms ascacheTTL. 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 whileUSE_CACHEhad 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/expandedQueriesare enforced instead of ignored, andStrategy.MULTI_QUERYperforms multi-query retrieval instead of silently degrading toSEMANTIC. Until nowqueryExpansionandexpandedQuerieswere read in exactly one place — building the result-cache key — and produced no retrieval behaviour at all, whilestrategy = MULTI_QUERYfell back toSEMANTICwith one warning per Role. Existing annotations already declaring either value change behaviour with no compile error: a Role that setqueryExpansion = HYDEand got plain semantic retrieval now issues provider calls and retrieves against the generated variations. SetqueryExpansion = NONE(and a strategy other thanMULTI_QUERY) to keep the old behaviour. Following thererank/cacheTTL/onFailureprecedent above, configuration that cannot be honoured is a configuration error that fails the annotated action at dispatch time rather than a degraded retrieval:expandedQueries <= 0with expansion active is rejected before any retrieval runs, and an unresolvable or ambiguousqueryExpansionModelis rejected outside theonFailureguard — no policy, includingUSE_CACHEandRETRY_SIMPLE, may soften a misconfigured expander into ungrounded or stale-cached output. A provider that raises is a transport concern and stays underonFailureas before; theRETRY_SIMPLEretry deliberately runs unexpanded, so a failing expander is not called a second time, and stays capped attopNunderrerank = true. New annotation memberqueryExpansionModel(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 publicRoleRagBinding.strategyFor(Strategy)see the same break:MULTI_QUERYnow throwsIllegalArgumentExceptioninstead of returning the SEMANTIC fallback, because expansion needs annotation-bound configuration the method has no access to — composing it isDefaultRetrievalSpi's job.GRAPH,HIERARCHICAL, andTEMPORALwere still in the Phase-2 fallback group at this point; later items in this release emptied it. -
BREAKING:
@Retrieval.deduplicate/dedupeThresholdare 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. Becausededuplicatedefaults totrueanddedupeThresholdto0.9, every existing@Retrievalnow injects fewer documents than before, with no compile error: near-duplicate passages that previously reached the model as separate documents are collapsed into one. Setdeduplicate = falseto 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 asretrieval.deduplicatedDocumentCount,retrieval.deduplicatedSourcesandretrieval.deduplicatedEntryIds, which aRerankerprovider sees on the candidates it is handed. Only the representative'ssourcereaches the prompt, so a merged document is attributed to its highest-ranked origin.dedupeThreshold = 1.0means "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-duplicatesdedupeThresholdexists 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;deduplicateanddedupeThresholdwere already covered by the cache key, and the threshold is now normalised out of that key whilededuplicate = false, so a setting that cannot change the document list no longer splits the cache.onFailure = RETRY_SIMPLEkeeps deduplication even though it drops reranking — a degraded retry must not widen the prompt with repetition — and itstopNcap applies afterwards, so the cap counts distinct documents on both paths. Following thererank/cacheTTL/onFailure/queryExpansionprecedent above, a non-finitededupeThresholdor one outside the inclusive0.0–1.0range is, withdeduplicate = 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. Withdeduplicate = falsethe 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.contextWindowandincludeSpecare enforced instead of ignored. Until now both were accepted and silently dropped — retrieval concatenated every document in full, socontextWindowbounded nothing andincludeSpec = falsestill rendered${source}and${score}into the prompt. Neither had a single call site in main sources; they were inert annotation surface. Every existing@Retrievalchanges what reaches the model with no annotation change and no compile error:contextWindowdefaults to 4000 tokens, so a Role whose retrieved documents exceed that budget now has its context truncated, and any Role declaringincludeSpec = falsestops emitting source and score values it previously leaked. There is no opt-out flag —contextWindowis a hard bound by construction — so a Role that relied on the old unbounded behaviour must raisecontextWindowto cover its corpus; the new_rag_context_truncatedkey (above) makes it observable when it does not. The rendered context is measured with the canonical content-awareTokenEstimator, includingcontextFormatoverhead, and content is cut only at Unicode code-point boundaries, so truncation can never emit half a surrogate pair.contextFormatis 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 thererank/cacheTTL/onFailure/queryExpansion/dedupeThresholdprecedent above — a configuration error that fails the annotated action at dispatch time, as is a non-positivecontextWindow. Assembly runs in the injection path, after the result-cache lookup rather than inside its loader: the cache stores documents, not rendered text, socontextWindow,includeSpecandcontextFormatcannot 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 toSEMANTIC. Until nowGRAPHlogged 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, andmaxDepth(2),maxVisitedNodes(1024),minScore,topKand thesourcesfence 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 carriesgraphNodeId,graphDepth,graphPath,graphEdgePathandgraphProvidermetadata. 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
rerankerModelandqueryExpansionModel. So with noGraphStoreProvideron the classpath — or none that claims the Role, or more than one that does — the annotated action fails at dispatch withGraphCapabilityException, raised above the@Retrieval.onFailureguard. It does not fall back to vector retrieval, and no failure policy can soften it:onFailure = CONTINUEcannot turn an absent adapter into a silently ungrounded action, andUSE_CACHEcannot answer it from a stale entry. Only failures a provider raises while traversing are runtime concerns and stay underonFailureas before. Direct callers of the publicRoleRagBinding.strategyFor(Strategy)see the same break:GRAPHnow resolves a provider or throws, joiningMULTI_QUERYoutside the Phase-2 fallback group;HIERARCHICALandTEMPORALwere 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 toSEMANTIC. 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 bytopK, and carrying hierarchy provenance in each document's metadata. When nothing on the classpath supplies hierarchy metadata — the built-in FILE loader does not overrideSourceLoader.metadata— retrieval does not throw and does not fall back: the documents form a flat hierarchy and are returned exactly as before, now explicitly markedhierarchyFlat=trueinstead of being indistinguishable from vector retrieval. This is the deliberate difference fromGRAPH, 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 = CONTINUEcannot turn it into a silently ungrounded action andUSE_CACHEcannot answer it from a stale entry. Only failures raised while traversing a valid hierarchy remain runtime concerns underonFailure. Direct callers of the publicRoleRagBinding.strategyFor(Strategy)see the same break:HIERARCHICALnow returns a hierarchical strategy or throws, joiningMULTI_QUERYandGRAPHoutside the Phase-2 fallback group, which leftTEMPORALas its only member until a later item in this release removed it. -
BREAKING:
@Retrieval(strategy = TEMPORAL)performs temporal retrieval instead of silently falling back toSEMANTIC. 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 bytopK, 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 optionalSourceLoader.metadatahook asTemporalMetadata.TIMESTAMP, an ISO-8601 instant; documents that declare none are retained at the conservative relevance floor and markedtemporalDated=false, so a corpus with no timestamps at all is returned as before rather than throwing. This is the same deliberate difference fromGRAPHthatHIERARCHICALmakes: 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 oneInstant.parserejects — 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 = CONTINUEcannot turn it into a silently ungrounded action andUSE_CACHEcannot answer it from a stale entry. Only clock-relative faults stay underonFailure: 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 publicRoleRagBinding.strategyFor(Strategy)see the same break:TEMPORALnow 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.cacheTTLjavadoc now records thatStrategy.TEMPORALranks 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: theEvaluatorSPI registry now registers the canonicalevaluators.agentset (TNS-373) instead of the legacyevaluators.agenticduplicates, and additionally registers the previously-deadstep_efficiencyevaluator. AServiceLoaderuniqueness test now guards against duplicate evaluator names (benchmark aggregation keys on the name, so duplicates silently collide). -
tnsai-evaluation: extractedAbstractJudgeEvaluator— a template-method base for single-call LLM-as-judge evaluators (build prompt → judge once → parse verdict, with a sharedprecheckhook and the common"LLM judge error: …"handling). The 12 single-call evaluators (ragFaithfulness/ContextualRecall/AnswerRelevancy, safetyHallucination/Bias/Toxicity, multiturnKnowledgeRetention/TurnRelevancy/ConversationCompleteness, agentPlanAdherence/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 implementingEvaluatordirectly.
Removed
- BREAKING: removed the
KnowledgeTypevaluesVECTOR_DB,WEB_SEARCHandCACHE. These are not sources a loader can ingest: a vector database and a search API are queried per request, whileSourceLoader.loadruns 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 tocom.tnsai.rag.RetrievalEngineProvider, which is invoked per retrieval; that is the contract a Qdrant or pgvector module should implement.FILE,URL,DATABASEandMEMORYremain — they fit the ingest shape — and of those onlyFILEhas 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 aRetrievalEngineProvideror to an ingest type. - BREAKING: deleted the orphaned
com.tnsai.security.sandboxpackage (SandboxedExecutor/SandboxSpec) — unreferenced dead code whose own javadoc admitted itsnetworkAccess/fileAccessflags 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.agenticpackage (PlanAdherenceEvaluator/TaskCompletionEvaluator/ToolCorrectnessEvaluator) — superseded by the canonicalevaluators.agentset with the same evaluator names. tnsai-core:AgentBuilderBDI seeding surface removed (TNS-541). BREAKING: deleted the fluentbelief/beliefs/desire/desires/intention/intentions/capability/capabilitiesmethods (plus their backing fields and package-private getters) fromAgentBuilder. The surface was dead — values were collected atbuild()but never read:ConfigurableAgentnever 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 viaCognitiveEngine/ContextSnapshot, where BDI state is populated at runtime.AgentBuilderplan handling is unaffected (still read byConfigurableAgent). Migration: none — these methods had no runtime effect; delete any stray.belief(...)/.desire(...)/.intention(...)/.capability(...)calls. #444
Fixed
dry-deploy-validate.shnow validatestnsai-paymentsartifacts — the module is published but had been missing from the staging-validation module list.tnsai-llm: semantic cache no longer embedsnomic-embed-textwithout 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.InMemorySemanticCachenow embeds with the symmetricclustering:task on both store and lookup.tnsai-payments:X402PaymentBroker.settleno 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 (putIfAbsentof 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@Retrievalhad 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:@MemorySpeccapacity is enforced, and theRELEVANCE/SUMMARIZEprune strategies are implemented rather than declared-but-inert.tnsai-core:FileMemoryStorepersistence hardened — atomic writes and corruption-safe loads.tnsai-intelligence:FileSessionStoredurability and robustness hardened.tnsai-core:@Memory(shared=true)actually shares state between agents — it previously produced an isolated store per agent.tnsai-core: thehasMemorySpecgate covers every@MemorySpecmember, so specs that set only a non-default member are no longer treated as absent.tnsai-core:@KnowledgeSource.KnowledgeType.FILEno longer claims to read PDF and DOCX. The bundledLocalFileSourceLoaderis 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 asfailed to read filewhen 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@KnowledgeSourcewhose type has no installed loader is a hard configuration error.RoleRagBindingfails 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 declarativeSEMANTICretrieval lexical.PhaseDeferralGuardTestfails the build if such a promise reappears on the RAG surface.
Security
tnsai-mcp:SSETransportno longer sends credentialed requests to a server-chosen host. AnendpointSSE 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 tohttps://evil.example.com/...and receive theAuthorization: Bearertoken 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'sHttpClientnow pinsfollowRedirects(NEVER)explicitly — it was already the JDK default, but the guard depends on it, since a followed cross-host redirect carries theAuthorizationheader with it.
Migration
- Run
mvn rewrite:runwithio.github.tansuasici.rewrite.UpgradeTnsAI_0_13_0(moduletnsai-rewrite) for the mechanical half: 61 type moves, 11@KnowledgeSourceattribute removals, and theRoleRagBinding.evictForTesting→invalidaterename. Activate the recipe for the version you are moving to. - Read your
@Retrievaldeclarations 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 particularcachedefaults totrue(300s TTL) — callRoleRagBinding.invalidate(Class)after a corpus change, or setcache = falseto keep 0.12.0 behaviour. rerank,queryExpansionandstrategy = GRAPHresolve through SPI seams with no bundled provider. Enabling them without an add-on module now throws at dispatch instead of being ignored.- The removed
KnowledgeTypevalues (VECTOR_DB,WEB_SEARCH,CACHE) have no replacement to rewrite to — a remote index is queried per request, which the ingest-timeSourceLoaderseam cannot express. Move those behindcom.tnsai.rag.RetrievalEngineProvider. SourceLoaderimplementers:load(...)and the optionalmetadata(...)hook takecom.tnsai.rag.KnowledgeSourceConfiginstead of the@KnowledgeSourceannotation.KnowledgeSourceConfig's six components are name-for-name and type-for-type identical to the narrowed annotation's six members, so a body that readsname/type/path/connection/query/enabledcompiles unchanged and only the parameter type needs editing. A loader that read one of the 11 members removed above —topK,provider,embeddingModeland the rest — has that separate break to resolve too; those are retrieval knobs, andSourceLoaderis 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— keylessEmbeddingProviderover Ollama/api/embed(nomic-embed-textdefault;OLLAMA_BASE_URL/OLLAMA_API_KEY; batch input) via the sharedHttpClientFactory.tnsai-core:AuthorityScope.permanent()— no-expiry authority scope (nullablevalidFor);expiresAt()→Instant.MAX,isExpired()→false. Removes theDuration.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 onActionMetadata(mirroring howContractSpec/TNS-637 is carried, since builder actions have no reflectiveMethod), and the enforcers resolve it at method-level precedence: method annotation > per-action config > role builder config > 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 ofInputGuardrailConfig(below). Newcom.tnsai.guardrails.OutputGuardrailConfigrecord mirrors the runtime-enforced subset of@OutputGuardrail(maxChars/minChars/onFailure/fallback/logFailures) withfrom(@OutputGuardrail),defaults()/NONE, and a fluent builder; the annotation's not-yet-wired scaffold fields are deliberately excluded (notemaxRetriesis inert because Phase-1RETRYdegrades toREJECT).OutputGuardrailEnforcernow resolves and enforces this record, with the annotation path adapting throughfrom(...). Resolution precedence: method@OutputGuardrail>RoleBuilder.outputGuardrail(...)(viaRole.getOutputGuardrailConfig()) > class@OutputGuardrail. Existing annotation behaviour is unchanged. Both guardrail config records now also reject a contradictory bound pair (maxChars/maxLength<minChars/minLengthwhen 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. Newcom.tnsai.guardrails.InputGuardrailConfigrecord mirrors the runtime-enforced subset (maxLength/minLength/blockPatterns/allowPatterns/onFailure/errorMessage/logFailures) withfrom(@InputGuardrail),defaults()/NONE, and a fluent builder; the annotation's not-yet-wired scaffold fields are deliberately excluded to avoid inert surface.InputGuardrailEnforcernow resolves and enforces this record, with the annotation path adapting throughfrom(...). Resolution precedence: method@InputGuardrail>RoleBuilder.inputGuardrail(...)(viaRole.getInputGuardrailConfig()) > class@InputGuardrail. Existing annotation behaviour is unchanged (the priorenforce(@InputGuardrail, …)entry point is preserved as a thin adapter).tnsai-core:@AgentSpec.toolCallFilterdeclarative tool-call filter (TNS-611) — the top-level annotation counterpart ofAgentBuilder.toolCallFilter(ToolCallFilter). The suppliedClass<? 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 publiccom.tnsai.agents.execution.AllowAllToolFilterdoubles as the annotation default and the "not set" sentinel — the extractor surfaces it asnullso 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 (applyInitializationResultonly adopts the resolved filter when no pending filter was set).tnsai-core:@AgentSpec.maxContextTokensdeclarative context budget (TNS-609) — the top-level annotation counterpart ofAgentBuilder.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.maxContextTokensalready drive).AgentInitializerresolves it with precedence: template >@AgentSpec.maxContextTokens(top-level shortcut) >@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 documentedMcpToolBridge.stdio(...).toTnsAITools()path) now reads the MCP 2025-03-26 tool annotations:destructiveHint=true → requiresConfirmation=true(so a destructive MCP tool registered without aToolCallFilterraises AGENT-V006 instead of dispatching unattended) andidempotentHint=true → idempotent=true(was hardcodedfalse). Both default tofalsewhen the annotation is absent, so tools with no annotations behave exactly as before.readOnlyHint/openWorldHintare not mapped (their counterpartsideEffectis not yet modelled — see TNS-556). Thetnsai-serverWebSocket path (McpProxyTool) is unchanged: its wire recordWsProtocol.McpToolDefcarries 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) gainedrequiresConfirmation+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 noToolCallFilterwired raises AGENT-V006 — previously only@ToolPOJOs were scanned and dynamic tools silently escaped the check. Non-breaking: the pre-existing 5-arg shape is preserved asDynamicToolMethod.of(...)and a delegating 5-arg constructor (both default the new fields to the safe "no claim" values); a fluentDynamicToolMethod.builder(name)sets them. (sideEffect/idempotencyHintfrom@Toolare intentionally not modelled yet — no runtime consumer reads them off aToolMethod, 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.ActionExecutorapplies it when no@Resilienceannotation is present. Scoped to the runtime-enforced subset (retry + timeout); circuit-breaker / rate-limit / bulkhead await TNS-565 Phase 2. #430tnsai-core: programmaticMemoryConfig(TNS-546) —AgentBuilder.memoryConfig(...)closes the@MemorySpecparity gap (8 fields, onlymaxContextTokenshad a builder shortcut before).MemoryConfigrecord mirrors@MemorySpecwithfrom()/defaults()/builder();MemoryStoreFactory.create(MemoryConfig)is the canonical factory the annotation path now delegates through. #429tnsai-core: programmaticContractSpec(TNS-637) — the builder-path form of@Contract.ActionConfig.withContract(ContractSpec.builder()…build())attaches Design-by-Contract gates (pre/post/invariants) to aRoleBuilder.addAction(...)action, enforced identically to the annotation.ContractValidatornow operates onContractSpecfor both paths (the annotation adapts viaContractSpec.from). #428tnsai-core: build-time@Contractexpression 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
Changed
tnsai-core:com.tnsai.identity.AgentSpecrecord renamed toAgentDescriptor. BREAKING: resolves the simple-name collision with the@com.tnsai.annotations.AgentSpecannotation (which keeps its name, per the@*Spec= annotations convention). All referrers updated.tnsai-core:@com.tnsai.roles.annotations.RoleIdentityrenamed to@RoleDeclaration. BREAKING: resolves the collision with thecom.tnsai.models.role.RoleIdentityclass (unchanged).tnsai-core: role export reads@RoleSpec.llm()(@LLMSpec) instead of@LLM(TNS-642, follow-up to TNS-568). BREAKING: theRoleSpecExtractor.LLMSpecnested record is renamed toRoleSpecExtractor.LLMExportSpec(it collided on simple name with the@LLMSpecannotation), andRoleSpecExtractor.hasLLMAnnotation(...)is renamed tohasLLMConfig(...). The role-export subsystem (ExportedRole,YamlRoleExporter,JsonRoleExporter) now sources LLM config from the nested@LLMSpeca 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.endpointpopulates the export record'sbaseUrl. #435tnsai-llm: canonicalproviderId()for error-mapper resolution (TNS-624). TheProviderErrorMapperSPI lookup keyed off the lower-casedexecuteRequestdisplay name, which drifts from the mapper's clean token ("Together.ai"→together.ainever matchedtogether), 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. #423tnsai-llm: OpenRouter / Mistral / HuggingFace folded ontoAbstractOpenAICompatibleClient(TNS-636, follow-up to TNS-621), net −629 LOC. Each kept only its real divergence: OpenRouter's ranking / Claude-beta headers move toaddProviderHeaders(), HuggingFace keeps aparseChatResponseoverride for usage tokens, Mistral has zero overrides. OpenAI, ZhipuAI and MiniMax stay bespoke (JsonCapableLLMClient+ per-callresponse_format). No public-API change. #425
Removed
tnsai-core:@SystemPrompt,@ChannelSpec,@Pipeline,@PipelineStepremoved (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: theSystemPromptBuilder, the channelChannelinterface, andPipelineBuilder(tnsai-coordination) — these were always the wired surfaces; the annotations only mirrored their names.@Pipeline/@PipelineStepreferenced only each other (Javadoc). Migration: none — delete any stray applications. #439tnsai-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 (@Repeatablecontainers). Stale Javadoc references in kept files were cleaned (ChannelSpec,resilience/package-info). Migration: none — these had no runtime consumer; delete any stray applications. #438tnsai-core: 9 pure-orphan annotations removed (TNS-590, Section 13 cleanup). BREAKING: deleted@ContextCompaction,@SlashCommand,@WorkspaceSpec,@Property,@ConfigProperty,@Trigger,@Delegate,@Sanitize,@ContentFilterfromcom.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. (@NormTypeis intentionally retained — it is the value enum for the@Norm/@Normsdeontic-logic wiring tracked under TNS-591.) #437tnsai-core:@LLMannotation removed (TNS-642, follow-up to TNS-568). BREAKING: the TYPE-levelcom.tnsai.annotations.LLMis gone — it was a parallel, export-only LLM-config surface with zero production usage that never instantiated anLLMClientand 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 theLLMSpecsimple-name collision and unblocks TNS-570 (@LLMSpecfield wiring) + TNS-571 (programmaticLLMConfig). #435
Fixed
tnsai-llm: danglingEmbeddingProviderjavadoc —EmbeddingProvider,CachedLLMClient, andSemanticCachereferenced a non-existentOpenAIEmbeddingProvider; examples now use the realOllamaEmbeddingProvider.tnsai-quality:@AuditLogis now wired intoAuditLogger(TNS-579) — the annotation was declared ontnsai-corebut never read, so marking an action@AuditLog(action = "...")produced no audit trail.SecurityEnforcer.audit(...)(already invoked per action via theSecurityEnforcerHandleSPI) now also reads the method's@AuditLogand emits a declarative named-action entry through the newAuditLogger.auditAction(...). It is independent of@Security(an action with only@AuditLogis audited) and complementary (an action with both emits both records).includeArgs/includeResultgate whether args/result are logged, and both honour@Securitymasking —includeArgsroutes through the samemaskForLogging(so@Security(maskFields = …)fields are masked) and the result is masked when@Security(sensitive = true), matching the level-audit so neither path leaks. Notnsai-corechange (the annotation already existed; the wiring lives entirely intnsai-quality).tnsai-core:@LLMSpec.topPis now honored by@RoleSpec-driven role LLM init (TNS-570, partial).Role.initializeLLMFromAnnotation()— the live path that builds a role'sLLMClientfrom@RoleSpec(llm=@LLMSpec(...))— passednullfor thetopPargument ofLLMClientProvider.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-default1.0fmaps tonull, matching theLLMClientFactoryconvention). The remaining@LLMSpecfields (frequencyPenalty/presencePenalty/timeoutMs/endpoint/apiKeyEnv) are not yet honored — they need a client-layer config change (the LLM clients' constructors take onlymodel/temperature/topP/maxTokens[/baseUrl/apiKey]); tracked under TNS-570's corrected scope.tnsai-llm:BedrockClient.streamChat()now works (TNS-626) — it was anUnsupportedOperationException("Streaming not yet implemented")placeholder in production. Implemented against the Anthropic Messages event stream via a lazily-builtBedrockRuntimeAsyncClient(the sync client has no event-stream API);content_block_deltaevents are parsed to text and returned as aStream<String>. Buffered for now (gathered before the stream is consumed); true per-token delivery is a follow-up. Claude-3-only, same aschat(). #421tnsai-llm: typed errors for the remaining 18 providers (TNS-622). Only 13 of the ~31 wired providers shipped aProviderErrorMapper; 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 untypedLLMException(UNKNOWN, noProviderDetails), soOnHttpStatus/OnErrorTypefallback rules never matched and the chain couldn't fail over on rate-limit/5xx for them. Each now has a mapper (15 share a newAbstractOpenAICompatibleErrorMapper; Vertex AI sharesAbstractGoogleErrorMapperwith Gemini; Watsonx/Replicate parse their own envelopes), resolved by the canonicalproviderId()from #423. #424tnsai-llm:AbstractOpenAICompatibleClientno longer double-wraps typed errors (TNS-636).chat()/streamChat()re-wrapped every exception into a genericLLMException(UNKNOWN, noProviderDetails), discarding the typed exception aProviderErrorMapperhad produced — latent since TNS-621, it meant the 16 migrated OpenAI-compatible clients silently lost the typed errors #424 added. TypedLLMExceptions now propagate unchanged. #425tnsai-llm: OpenAI-compatible clients now report real usage tokens (TNS-639).AbstractOpenAICompatibleClient.parseChatResponsedidn't read theusageblock, so the ~18 clients on the base returned empty token counts andCostAwareLLMClientfell back to a character-count estimate. It now parsesprompt_tokens/completion_tokens; cost tracking uses the provider's actual counts.HuggingFaceClient's bespoke parse override (its only one) is removed. #426
Migration
- Replace
com.tnsai.identity.AgentSpec→com.tnsai.identity.AgentDescriptor(the@AgentSpecannotation is unaffected). - Replace
@com.tnsai.roles.annotations.RoleIdentity→@RoleDeclaration(thecom.tnsai.models.role.RoleIdentityclass is unaffected). - The 0.11.1-era orphan-annotation and
@LLMremovals (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:@AgentSpecannotation-parity onAgentBuilder(TNS-534). Six@AgentSpecmetadata fields had a runtime consumer but no builder counterpart, so programmatic agents silently fell back to defaults. Builder now exposesdescription,version,autoStart,idleTimeoutMs,did(DIDConfig),groupMembership(GroupMemberSpec), with precedence builder explicit > annotation > default applied inAgentInitializervia newInitializerContextoverride hooks. #411tnsai-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. #411tnsai-core:GroupMemberSpec.of(String...)convenience factory for builder-side group membership. #411tnsai-core:RoleBuilder.addAction(ActionConfig)— programmatic role actions (TNS-551). Builder-built roles can now register dispatchable actions; previously only@ActionSpec-annotated methods on aRolesubclass worked, soConfigurableRolewas capability-less. NewActionConfig+ActionHandler(com.tnsai.metadata);ActionExecutordispatches the handler lambda andRole.discoverActionsmerges them with annotation-discovered actions (duplicate names error). Scoped toLOCALactions. #414tnsai-core:@AgentSpec.roles(TNS-536) — declare an agent's role classes (Class<? extends Role>[]) in the annotation, the counterpart ofAgentBuilder.role(...). Instantiated via public no-arg constructor at init; precedence is programmatic > annotation. A missing no-arg ctor fails with an actionableAGENT-V009message. #415tnsai-tools:tnsai diagnoseCLI (TNS-525) —com.tnsai.tools.diagnosticsprints a paste-ready environment report for bug reports:tnsai_version(lockstep),jdk(version/vendor/GC/max heap),os, andproviders_configured(known LLM provider env vars asset/missing, never the value). Flags--json(default),--issue-template(GitHub markdown block),--minimal,--no-redact(secret redaction via the frameworkPatternRedactoris 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. #416tnsai-core:@ContractDesign-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 bindresultand resolveold(expr)to the pre-execution value; invariants run before and after. Honorsvalidate/strict/message. Newcom.tnsai.actions.contracts(ContractEvaluator,ContractValidator,ContractViolationException); addscommons-jexl3. Additive to the existingActionContract/@ActionSpec.precondition/@State.invariantspaths. ProgrammaticContractSpec+ build-time syntax validation are follow-ups. #418
Changed
tnsai-core: removed the dead, unusedAgentSpecExtractor.DIDInforecord (0 callers, verified across the full reactor) — its role is now served byDIDConfig, andAgentSpecExtractor.generateDIDis DRYed throughDIDConfig.toDid. Internal cleanup; no consumer impact. #411tnsai-llm: collapsed 16 duplicated OpenAI-compatible provider clients into a newAbstractOpenAICompatibleClient(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 onAbstractLLMClient— follow-up. No public-API change; net ~-3,290 LOC; 1644 tests green. #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:HttpInterceptorprimitive forHttpTransport(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 onHttpTransportcallers. #374tnsai-payments: new umbrella module +PaymentBrokerSPI skeleton (TNS-449 P2). New optional module —tnsai-paymentscarries thePaymentBrokerSPI (quote,settle,verify) plus thex402protocol-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 (0.10.0). #375tnsai-payments:X402PaymentBrokerend-to-end x402 settlement (TNS-449 P3). Implements thePaymentBrokerSPI against the x402 HTTP-402 payments protocol over Base USDC.PaymentRequirementparses the 402-body wire format;TransferAuthorizationbuilds EIP-3009 typed data with inline EIP-712 encoding (skips web3j's heavierStructuredDataEncoderfor tighter hot-path);Web3jWalletwraps secp256k1ECKeyPair.signfor 65-byter||s||vsignatures;X402PaymentBroker.quote()probesservice.metadata["x402.resource"], parses the 402, picks the compatiblePaymentRequirement, mints aQuote;settle()builds typed data, signs, replays the request withX-PAYMENT(base64-JSON header), returns a sealedSettlementvariant. Idempotency:Quote.idempotencyKeymaps deterministically (Keccak-256) to the EIP-3009 nonce, so retried settles returnSettlement.AlreadySettledrather 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%. StubHttpServerfacilitator in-process; no live testnet calls in CI. #389 (rebased successor to #377)tnsai-payments:EnvKeystoreWallet+ mandate enforcement + liability records (TNS-449 P4, partial).EnvKeystoreWallet.fromEnv(prefix, network)reads<PREFIX>_PRIVATE_KEYfrom env for a zero-config local wallet (encrypted JSON keystore decryption deferred — needs the fullweb3j-coreartifact, kept opt-in for now).X402ConfiggainsliabilitySink+authorityScopeoptional builder fields.X402PaymentBroker.settle()runs mandate enforcement before signing: sums priorx402.settlerecords from the configuredLiabilitySink, 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 (rebased successor to #378)
Changed
-
Dependency bumps (#388 batch + #333):
aws.sdk2.44.4 → 2.44.7 (patch)jsoup1.22.1 → 1.22.2 (patch)javalin7.1.0 → 7.2.2 (minor)slf4j2.0.17 → 2.0.18 (patch)junit5.14.3 → 6.0.3 (major; we run JDK 21 so compatible)telegram-bot-api8.3.0 → 9.6.0 (major)angus-mail2.0.3 → 2.0.5 (patch)greenmail-junit52.1.5 → 2.1.8 (patch)opentelemetry-semconv1.41.0 → 1.41.1 (patch)
No code edits required — pure pom property changes. The two major bumps (
junit5 → 6,telegram-bot-api8 → 9) ride the samemvn verifyreactor; 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, #375, #389, #390) plus the dependency batch (#333, #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-paymentsjoins 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.handleToolApprovewalked the session'sapprovalFiltersmap but calledfilter.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 oneWsToolApprovalFilterper session), an approval whosetoolCallIdbelonged 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.handleApprovalnow returnsboolean(trueiff it owned the id and consumed it);WsHandleriterates every filter in the session until one returnstrue, then breaks. Added a package-privateregisterPendingForTestingseam 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 acancelAllregression guard.
Added
tnsai-channels:WhatsAppChanneladapter (Cloud API) (TNS-352). Sixth channel adapter and the first one that embeds an HTTP server intnsai-channels— WhatsApp Cloud API delivers inbound events via webhook only (no WebSocket or polling alternative), so the adapter binds a JDKcom.sun.net.httpserver.HttpServerto a configurable port + path. Inbound: Meta GETs the path withhub.challengeat subscription time → we echo iffhub.verify_tokenmatches; Meta POSTs JSON events signed withX-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}/messagesto 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 sharedWebhookReceiverwith path-based routing; YAGNI for now.
Changed
tnsai-llm:WatsonxClientIAM 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:DiscordChanneladapter (Gateway WebSocket) (TNS-351). Fifth channel after Telegram, CLI, Email, and Slack — and the second WebSocket adapter. Inbound: opens a Discord Gateway v10 connection viaGET /gateway/bot→ WSS, then handles the full handshake (HELLO→ schedule heartbeats →IDENTIFYwith token + intents →READYcaptures the bot user_id →MESSAGE_CREATEbecomesUnifiedMessage). Heartbeats run on a single-threadedScheduledExecutorServicewith the interval Discord supplies inHELLO, carrying the last observedssequence number on each beat. Outbound:POST /channels/{channel_id}/messageswith theBot <token>header and amessage_reference.message_idwhen the response targets a specific reply. Bot-echo filtering: messages authored by other bots (or our own bot onceREADYlands) are silently dropped. v1 scopes out slash commands (needsPOST /applications/{id}/commandsregistration +INTERACTION_CREATEhandling), 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 privilegedMESSAGE_CONTENTintent).tnsai-channels:SlackChanneladapter (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 viaPOST /apps.connections.openwith thexapp-...app token, then routesevents_apienvelopes (andapp_mentionevents) intoUnifiedMessageafter acking with the matchingenvelope_id. Outbound:POST /chat.postMessagewith thexoxb-...bot token; thread continuity preserved by carrying the inboundthread_tsforward 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.SlackChannelConfigreadsSLACK_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 withSLACK_SIGNING_SECRET; v1 chose Socket Mode instead because the framework has no embedded HTTP server intnsai-channelsand 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-verifyingSlackWebhookChannelsibling can be added later if a use case needs it.tnsai-llm:WatsonxClientprovider (TNS-340). Twenty-fifth LLM provider — talks to IBM watsonx.ai (enterprise LLM platform) via its/ml/v1/text/chatendpoint. The response is OpenAI-shaped but the request body uses IBM-specific fields (model_idinstead ofmodel, plus a requiredproject_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 againsthttps://iam.cloud.ibm.com/identity/token, caches the resulting token until itsexpires_inwindow 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 defaultus-south; overrideWATSONX_BASE_URLforeu-de/jp-tok/ etc. Required env:WATSONX_API_KEY,WATSONX_PROJECT_ID. Registered inLLMClientFactoryunderwatsonxandibmaliases.tnsai-llm:DeepInfraClientprovider (TNS-329). DeepInfra's cost-leader open-model hosting — typically the cheapest hosted Llama-70B option. OpenAI-compatible chat-completions API athttps://api.deepinfra.com/v1/openai(note the/v1/openaisuffix — 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 viaDEEPINFRA_BASE_URL. Registered inLLMClientFactoryunderdeepinfraanddeep-infraaliases. API key viaDEEPINFRA_API_KEY.tnsai-llm:FireworksAIClientprovider (TNS-328). Fireworks.ai's open-model hosting + FireFunction (function-calling-tuned) — Llama 3.x, Mixtral, Qwen 2.5, DeepSeek V3, plusfirefunction-v2. OpenAI-compatible chat-completions API athttps://api.fireworks.ai/inference/v1(override viaFIREWORKS_BASE_URLfor mirrors / proxies). Same wire format as every other OpenAI-shape provider — structurally identical client. Registered inLLMClientFactoryunderfireworks,fireworksai, andfireworks-aialiases. API key viaFIREWORKS_API_KEY.tnsai-llm:TogetherAIClientprovider (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 athttps://api.together.xyz/v1(override viaTOGETHER_BASE_URLfor mirrors / proxies). Same wire format asGroqClient/NvidiaNIMClient/XAIGrokClient/CerebrasClient— structurally identical client. Registered inLLMClientFactoryundertogether,togetherai, andtogether-aialiases. API key viaTOGETHER_API_KEY.tnsai-llm:CerebrasClientprovider (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 athttps://api.cerebras.ai/v1(override viaCEREBRAS_BASE_URLfor mirrors / proxies). Catalog at time of writing:llama-3.3-70b,llama3.1-8b,qwen-3-32b. Same wire format asGroqClient/NvidiaNIMClient/XAIGrokClient— structurally identical client. Registered inLLMClientFactoryundercerebras. API key viaCEREBRAS_API_KEY. Headline use case: latency-sensitive agent inner loops (tool-call coordination, REPL chat, realtime UI agents).tnsai-llm:VertexAIClientprovider (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 nativegenerateContentendpoint athttps://{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-styleassistanthistory roles mapped to Gemini'smodelrole. Auth (v1 scope): takes a pre-fetched OAuth access token viaVERTEX_AI_API_KEY(e.g.gcloud auth print-access-tokenin 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 ingoogle-auth-library-javaor hand-rolled RS256 crypto. Required env:VERTEX_AI_API_KEY,VERTEX_AI_PROJECT_ID. Optional:VERTEX_AI_LOCATION(defaultus-central1; host derived from it),VERTEX_AI_BASE_URL(full override). Registered inLLMClientFactoryundervertexai,vertex-ai, andvertexaliases.tnsai-llm:ReplicateClientprovider (TNS-331). Twenty-sixth LLM provider — talks to Replicate (community model marketplace) via its native predict/poll HTTP API athttps://api.replicate.com/v1. Unlike every other provider in this module, Replicate is not OpenAI-shaped — the request takes an arbitraryinputobject 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 identifierowner/nameauto-resolves the latest version;owner/name:versionpins. 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 (streamChatemits the final answer as a single chunk), no tool calling. Env:REPLICATE_API_KEY(Replicate's docs call itREPLICATE_API_TOKEN; this module standardises on_API_KEY). Registered inLLMClientFactoryunderreplicatealias.tnsai-llm:DeepSeekClientprovider (TNS-324). Twenty-fourth LLM provider — talks to DeepSeek (Chinese frontier lab) via its OpenAI-compatible chat-completions endpoint athttps://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 isdeepseek-reasoner, responses include areasoning_contentfield carrying R1's visible chain-of-thought trace alongside the standardcontenttext. The sharedChatResponse/ChatChunkSPI 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 inLLMClientFactoryunderdeepseekalias. API key viaDEEPSEEK_API_KEY.tnsai-llm:LMStudioClientprovider (TNS-333). Sixteenth LLM provider — talks to a locally-running LM Studio desktop server via its OpenAI-compatible chat-completions endpoint athttp://localhost:1234/v1(override viaLMSTUDIO_BASE_URLfor LAN rigs or proxies). LikeOllamaClient, the API key is optional: when unset theAuthorizationheader 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 asBearer <key>. Model name comes from the loaded model in the LM Studio UI — discover viaGET /v1/models. Streaming, tool calls, and topP follow the same wire format as the other OpenAI-shape providers. Registered inLLMClientFactoryunderlmstudioandlm-studioaliases.tnsai-llm:PerplexityClientprovider (TNS-330). Twenty-third LLM provider — talks to Perplexity's search-augmented Sonar family via its OpenAI-compatible chat-completions endpoint athttps://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 acitations[]array alongside the standard OpenAI text content; the sharedChatResponseSPI doesn't yet carry citation metadata, so citations are dropped — text-only answer is returned. Surfacing citations is a follow-upChatResponseextension. Registered inLLMClientFactoryunderperplexityandpplxaliases. API key viaPERPLEXITY_API_KEY.tnsai-llm:DatabricksClientprovider (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 athttps://{workspace}.cloud.databricks.com/serving-endpoints, so there is no sensible default — every caller must supplyDATABRICKS_BASE_URL(or the constructorbaseUrlargument). 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 asmodel. Auth viaDATABRICKS_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 inLLMClientFactoryunderdatabricksandmosaicaliases.tnsai-llm:QwenCloudClientprovider (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 athttps://dashscope-intl.aliyuncs.com/compatible-mode/v1(the client's default) and China athttps://dashscope.aliyuncs.com/compatible-mode/v1(select viaDASHSCOPE_BASE_URLenv var or constructorbaseUrlargument). 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 inLLMClientFactoryunderqwen,dashscope, andalibabaaliases. API key viaDASHSCOPE_API_KEY(single-token "DashScope" label inrequireApiKeyto satisfy the env-var pairing test).tnsai-llm:LlamaCppServerClientprovider (TNS-334). Twentieth LLM provider — talks tollama-server(the HTTP server binary from the llama.cpp project) via its OpenAI-compatible chat-completions endpoint athttp://localhost:8080/v1(override viaLLAMACPP_BASE_URL). MirrorsOllamaClient/LMStudioClient/VLLMClient: API key is optional —Authorization: Bearer <key>is sent only whenLLAMACPP_API_KEY(or constructorapiKey) is set. Model name comes from the GGUF file that llama-server's--modelflag 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 inLLMClientFactoryunderllamacpp,llama-cpp, andllama.cppaliases.tnsai-llm:VLLMClientprovider (TNS-332). Nineteenth LLM provider — talks to a self-hosted vLLM inference server (typically invoked aspython -m vllm.entrypoints.openai.api_server) via its OpenAI-compatible chat-completions endpoint athttp://localhost:8000/v1(override viaVLLM_BASE_URLfor remote inference rigs). MirrorsOllamaClient/LMStudioClient: API key is optional —Authorization: Bearer <key>is sent only whenVLLM_API_KEY(or constructorapiKey) is set, supporting vLLM instances started with--api-keyor behind auth proxies. Model name comes from vLLM's--modelflag; discover viaGET /v1/models. Streaming, tool calls, and topP follow the standard OpenAI-shape wire format. Registered inLLMClientFactoryundervllmalias.tnsai-llm:TencentHunyuanClientprovider (TNS-336). Eighteenth LLM provider — talks to Tencent's Hunyuan family via its OpenAI-compatible chat-completions endpoint athttps://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 inLLMClientFactoryunderhunyuan,tencent, andtencent-hunyuanaliases. API key viaHUNYUAN_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:YiClientprovider (TNS-337). Seventeenth LLM provider — talks to 01.AI (Kai-Fu Lee's lab) via its OpenAI-compatible chat-completions API athttps://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 inLLMClientFactoryunderyi,01ai, and01.aialiases. API key viaYI_API_KEY.tnsai-llm:XAIGrokClientprovider (TNS-323). Fifteenth LLM provider — talks to xAI's Grok models via the OpenAI-compatible chat-completions API athttps://api.x.ai/v1(override viaXAI_BASE_URLfor mirrors / proxies). Catalog at time of writing:grok-3,grok-3-mini,grok-2-vision, plus the gatedgrok-betapre-release tier. Streaming, tool calls, and topP all follow the same wire format asGroqClient/NvidiaNIMClient— structurally identical client. Registered inLLMClientFactoryunderxai,grok, andxai-grokaliases. API key viaXAI_API_KEY.tnsai-channels:EmailChanneladapter (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 throughMessage-ID/In-Reply-To/Referencesheaders — replies in the same thread land on the sameconversationId. Outbound SMTP replies setIn-Reply-ToandRe:-prefix the subject.EmailChannelConfigis 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:NvidiaNIMClientprovider (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 athttps://integrate.api.nvidia.com/v1(default) and any self-hosted NIM container via thebaseUrlconstructor parameter orNVIDIA_BASE_URLenv 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 asGroqClient/OpenRouterClient. Registered inLLMClientFactoryundernvidia,nvidia-nim, andnimaliases. API key viaNVIDIA_API_KEY.
Added (ops)
tnsai-server: Docker image + multi-arch publish workflow (TNS-520). Multi-stageDockerfile(Maven 3.9 + Eclipse Temurin 21 → distrolessgcr.io/distroless/java21-debian12:nonroot) ships a self-containedtnsai-serverJAR runnable withdocker run. The JVM is PID 1 soSIGTERMreaches the existingRuntime.addShutdownHook()drain path; image defaults toTNSAI_HOST=0.0.0.0+TNSAI_ALLOW_PUBLIC=truebut deliberately leavesTNSAI_TOKENunset — operators must supply a token before exposing to anything other than a private network. New/healthz+/readyzroute aliases (Kubernetes-conventional) added additively alongside the existing/health/live+/health/readyendpoints, sharing the same handler lambdas.maven-shade-pluginlives in an opt-indockerprofile somvn installfor downstream consumers stays fast and Maven Central isn't polluted with a-shadedclassifier. New.github/workflows/docker-publish.ymlsmoke-tests on everyv*tag (boots the image, polls/healthzfor 30s, verifies SIGTERM-driven graceful shutdown) then publishes multi-arch (linux/amd64+linux/arm64) to Docker Hub. Skipped for forks. RequiresDOCKERHUB_USERNAME+DOCKERHUB_TOKENrepo secrets. #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 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
Internal
tnsai-core:PaymentBrokerSPI record test coverage (TNS-518). Three new test files incom.tnsai.payment(SettlementTest,QuoteTest,ServiceTest) pin every validation invariant on the shared SPI records — sealed-variant exhaustiveness onSettlement, null-checks + blank-string rejection +priceUSD ≥ 0+expiresAt > issuedAtonQuote, defensive-copy isolation onService.metadata. 31 tests, all green. No production code touched. #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
Unreleasedcarried 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@Toolmethods on thePROJECT_TOOLStoolkit.agentsmd_parsereturns a structuredAgentsMdContentrecord (intro+ orderedsections: [{level, title, body}]) parsed fromAGENTS.md, with case-variant +CLAUDE.md+README.mdfallback — letting agents route on individual sections (e.g. pull just "Setup") rather than treating the document as an opaque blob.agentsmd_generateproduces a draftAGENTS.mdby detecting the build system frompom.xml/package.json/pyproject.toml/Cargo.toml/go.modand filling in language-appropriate setup + test commands — returns the markdown string, the caller decides whether to write it. #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:StreamingChannelAdaptermixin interface +UnifiedChunkrecord (TNS-440). Lets adapters opt into per-token delivery without changing the existingChannelAdaptercontract.UnifiedChunkcarriesconversationId,delta,doneflag, free-formmetadata(tool-call markers etc.), andtimestamp. The mixin extendsChannelAdapterso anyStreamingChannelAdapteris also a regular adapter — gateway code caninstanceof-check and dispatch chunks viasendChunk(...)as they arrive, then still callsend(UnifiedResponse)once with the assembled reply for non-streaming downstreams (logging, audit). Adapters that don't implement the mixin keep working unchanged. #335tnsai-channels:CliChannelnow implementsStreamingChannelAdapterwithcapabilities().streaming() = true. REPL mode emits theassistant:prefix once on the first chunk then concatenates deltas inline; JSON mode emits one{"type":"chunk","content":"...","done":...}record per chunk. The post-streamsend(UnifiedResponse)is suppressed (the reply was already rendered chunk-by-chunk); thesuppressNextSendstate resets after one consumption so subsequent standalonesend()calls render normally. #335
Fixed
- CI: artifact upload steps in
.github/workflows/build.ymlnow usecontinue-on-error: trueand 3-day retention (down from 7). Previously, a GitHub Actions free-tier storage quota hit would mark the wholeBuild & Testjob red even thoughmvn verifyhad passed. Soft-fail makes the build status reflect code health, not artifact store availability. #336 - Release tooling:
make releaseCHANGELOG extract now uses literal-substring match instead of regex, so versions with.(every version) extract correctly (TNS-419). #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 verify13/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) — secondChannelAdapterafter Telegram. Two modes: REPL (interactive>prompt with/exit/quit/clearlocal slash commands intercepted, everything else flows to the gateway as aUnifiedMessage) and JSON (newline-delimited{"text":"..."}in /{"type":"text","content":"..."}out for scripting). SPI-discoverable viaMETA-INF/services, mode selected from a config string ("json"case-insensitive → JSON, default REPL). Closes TNS-353 Phase 1+2. #317, #318tnsai-quality: OTLP-native LLMCallLog exporter (OtlpLLMCallExporter implements LLMCallPublisher) — every capturedLLMCallLognow flows to any OpenTelemetry collector (Langfuse, LangWatch, Phoenix, Honeycomb, Tempo, Loki) via the GenAI semconv wire shape. OneCLIENTspan per call (chat <model>/chat_stream <model>), three metrics (gen_ai.client.token.usagelong counter partitioned bygen_ai.token.type,gen_ai.client.cost.usd+gen_ai.client.operation.durationhistograms). Cardinality discipline: 7 ctx fields on the span, onlytenant + roleon 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. #319tnsai-quality: Sampling + Redacting decorators forLLMCallPublisher— companion to the existingSampling*/Redacting*Publisherpair onAgentEventPublisher.SamplingLLMCallPublisherreuses theEventSamplingPolicySPI by mapping eachLLMCallLoginto aSamplingInput(eventKind"llm.called", levelERRORwhenisFailure()elseINFOsoErrorAlwaysPolicypasses failures regardless of nominal sample rate).RedactingLLMCallPublisherscrubs every leaky surface:prompt.systemPrompt/prompt.messages/prompt.parameters(LLM_PROMPTscope) andresponse.content/response.toolCalls[].arguments/response.reasoningContent/error.errorMessage/providerExtensions(LLM_RESPONSEscope). 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. #321tnsai-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-privateJudgeScoreParsermirrorsGEvalEvaluator.extractScoregeneralised to any[min, max]range; returns-1on no match so callers fail explicitly instead of guessing a middle value. Closes TNS-373. #320
Fixed
tnsai-quality: typo in two test method names —nullCallProapagates→nullCallPropagates. Cosmetic, JUnit method-name agnostic. #322tnsai-channels: stale@since 0.9.4onCliChannel→@since 0.10.1. Author wrote the tag pre-0.10.0 cut; next release after 0.10.0 is 0.10.1. #318tnsai-evaluation+tnsai-quality: 8 stale@since 0.10.2Javadoc 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, noRemoved. - Reactor
mvn verify13/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).
[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)IdempotencyStoreimplementations, MCPidempotentHintflag wired through tool-call routing. Closes TNS-224. #301tnsai-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. #302tnsai-core: checkpoint + resume + idempotent retry primitives —CheckpointStoreSPI, in-memory default, automatic snapshot on agent state transitions, replay-safe retry. Closes TNS-299. #303tnsai-quality: durableCheckpointStoreimplementations — Redis (Lettuce) for fast volatile checkpoints, S3 (AWS SDK v2) for cold long-term snapshots. Closes TNS-312. #304tnsai-core: agent identity + accountability + payment SPIs —AgentIdentity(DID + cryptographic key),AccountabilityLog(signed event chain),PaymentRail(x402 / settlement abstraction). Closes TNS-298. #305tnsai-core: on-demand modular knowledge layer —@Skillannotation,SkillActivationEvent(added toTnsAIEventsealed hierarchy), lazy skill loading via SPI, runtime skill discovery. Closes TNS-289. #307tnsai-quality: unified file/doc guardrails — sandbox execution + size limits + extension whitelist, applied uniformly to file-write and document-export tools. Closes TNS-342. #308tnsai-quality:SandboxSPI — isolated execution primitive (process / container / WASM strategies), pluggable resource limits. Closes TNS-296. #309tnsai-quality: code review pipeline harness — pluggable, idempotent, deepsec pattern; routes proposed code changes through configurable checks before commit. Closes TNS-291. #312tnsai-server: five-layer security hardening — auth, rate-limit, input validation, output redaction, audit log on every request. Closes TNS-302. #313- CI: nightly reactor build (
.github/workflows/nightly-reactor.yml) —mvn verifyonmaindaily at 06:00 UTC, surfaces time-bomb tests and cross-module compile drift before consumers hit them. #314 - Process:
PULL_REQUEST_TEMPLATE.mdwith mandatory reactor-build checkbox when Protected Changes / sealed-type permits are touched, plus a downstream-PR section linking consumer repos. #314
Changed
- BREAKING:
tnsai-core:AgentBuilder.accountability(...)is now mandatory when@Accountableis 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 tnsai-tools: Python and JavaScript execution tools migrated from per-tool isolation to the sharedSandboxSPI — single hardening surface, consistent resource limits across languages. Closes TNS-343. #311
Removed
- BREAKING:
tnsai-core:logback-classicmoved 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 tnsai-core: no-op accountability fallback shims (NoOpAccountabilityLog,NoOpPaymentRail) — replaced by explicit-wire failure mode. #306
Fixed
tnsai-intelligence:ContradictionDetectortime-bomb fixed —Clockis now injected so tests can pin time; theFUTURE = NOW + 30dconstant no longer leaks wall-clock dependency into CI. Closes TNS-341 (CI broke 2026-05-07T12:00 UTC when the original constant expired). #310
Migration
Accountability (TNS-298 follow-up): if your agent declares @Accountable, wire the SPI explicitly in the builder:
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:
<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 verify13/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.
[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@ToolExampleend-to-end. Bedrock routes via the extractedAnthropicToolConverter; Cohere has its ownCohereToolConverter(JSON-Schema → flatparameter_definitionsshape, types translatedstring→str/integer→int/number→float/boolean→bool/array→list/object→dict). #297tnsai-core:@ToolExamplenow renders in the system-prompt prose under each action's## Available Actionsblock. Positive examples appear underExamples:, negatives underAvoid (anti-patterns):. Wired through bothRolePromptBuilder(in-process role) andSystemPromptBuilder(SCOP bridge). #299tnsai-core:ActionMetadata.getExamples()accessor — returns the combined positive + negative example list in declaration order. #299tnsai-core:com.tnsai.prompt.format.PromptFormat— shared formatter for prompt-building call sites.renderConstraints(mustAlways/mustNever) andrenderExamples(@ToolExample). Used byRolePromptBuilder,RoleSpecReader, andSystemPromptBuilder(SCOP). #298, #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
Fixed
tnsai-llm:@ToolExampleannotations on tool methods are no longer silently dropped on the Anthropic provider. Positives are mapped into the nativeinput_examplesfield on each tool definition; negatives are folded into the tool description as anAVOID:section (Anthropic's tool API has no first-class anti-pattern field). #291tnsai-llm:@ToolExampleannotations no longer silently dropped on OpenAI and Gemini. Both providers receive examples folded into the function description asEXAMPLES:(positives) andAVOID:(negatives) sections — neither provider has a native examples API. #292tnsai-llm:@ToolExampleannotations 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
Documentation
@ToolExampleJavadoc now cites the Anthropic source for the "72% → 90% accuracy on complex parameter handling" claim (Introducing advanced tool use) instead of a bare assertion. Also fixes outdated@sincetag (2.14.0template-artefact →0.3.0) and a broken@see Actioncross-reference (now@see ActionSpec). #295tnsai-core/README.md: annotation count refreshed100+→98(verified viagrep -rh "public @interface") on both prose and feature-table sites. #293- 8 module READMEs (core, llm, mcp, tools, intelligence, coordination, integration, quality): per-module
LICENSElink now correctly resolves to the monorepo-rootLICENSEfile ((LICENSE)→(../LICENSE)) — submodules don't carry their own LICENSE files post-monorepo. #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 verifyreactor 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()— theString[]field is gone. Move strings tomustAlways(positive obligations) ormustNever(negative prohibitions). - BREAKING:
ActionMetadata.invariantsfield +hasInvariants()+getInvariants()accessors. - BREAKING:
ContractConfig.invariantsfield — record arity drops 6 → 5. - BREAKING:
ActionExecutorbefore/after method-level invariants check; onlycheckPrecondition/checkPostcondition/checkStateInvariantsremain on the action lifecycle. - BREAKING:
InvariantCheckerHandle.checkActionInvariants(Method)SPI method. - BREAKING:
InvariantChecker.checkActionInvariants(Method)impl + references incheckBeforeAction/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 byInvariantChecker.checkStateInvariants()after any state change.@ActionSpec.precondition/postcondition— Hoare-triple Method contracts; still wired throughActionExecutor.@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.
[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.
[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.ActionMetadata.getMustNever()/getMustAlways()+hasMustNever()/hasMustAlways()— runtime accessors for the new constraints; consumed byRolePromptBuilder,RoleSpecExtractor.extractResponsibilitiesFromActions, and the SCOPSystemPromptBuilder.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
LLMCallLogevents emitted per LLM invocation, decoupled from the legacy raw-stringLLMObserver. Wires theLLMCallLogrecord (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 +EventContextinto LangFuse / Helicone / Phoenix dashboards or custom cost trackers.LLMCallPublisher(SPI) — single-methodpublish(LLMCallLog);NOOPis 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 anyLLMClient. Captures success and failure paths; for streaming, captures TTFT + chunk count. Multimodalchat(List<ContentPart>)andstreamChatWithSpecroute through the delegate without capture in this PR — separate hot-path refactor.JsonLLMPricingRegistry— loads rate cards from classpath JSON. Ships/pricing/2026-05.jsonwith rates foropenai/gpt-4o,openai/gpt-4o-mini,openai/o1-preview,anthropic/claude-sonnet-4,anthropic/claude-opus-4, and anollama/*wildcard (zero — local). Other 7 framework providers' rate cards land incrementally.ToolSurfaceHasher— single canonical entry point that turns the framework's rawList<Map<String,Object>>tool shape into aToolSurfacewith a stable SHA-256 hash. Sorted-key Jackson serialisation so identical tool sets across calls correlate (prompt-cache friendly).(provider, model)cost attribution viaLLMCallLog.context()— every captured event carries the activeEventContext(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.jsonexpanded from 3 providers / 6 models to 7 providers / 13 models, so thecostfield onLLMCallLogis 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) declarecached_per_1k: null. 10 new tests (1099 total intnsai-llm).
Removed
- BREAKING:
@com.tnsai.annotations.Responsibilityannotation (used inside@RoleSpec.responsibilities). - BREAKING:
@com.tnsai.roles.annotations.Responsibilitiesannotation + nestedDuty,SafetyConstraint,Severitytypes. - BREAKING:
com.tnsai.models.role.Responsibilitymodel interface +CoreDuty/SafetyProperty/Responsibilities(container) implementations. - BREAKING:
com.tnsai.enums.role.SafetyTypeenum (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@ActionSpecannotations on aRolesubclass instead. - BREAKING:
@RoleSpec.responsibilities()field. - BREAKING:
RolePromptBuilder.generateResponsibilitiesSection(...), the secondbuildMinimalRolePrompt(identity, responsibilities)overload (only identity is needed now). - BREAKING:
RoleSpecExtractor.extractResponsibilities(...)/hasResponsibilitiesAnnotation(...). UseextractResponsibilitiesFromActions(...)for the action-bound replacement. - BREAKING:
RoleSpecReader.RoleSpec.ResponsibilityMeta+getResponsibilities()/setResponsibilities(...). - BREAKING:
ExportedRole.responsibilitiesfield +hasResponsibilities().autoResponsibilities(per-action) is now the single source.
Changed
RolePromptBuilderrendering — the## Responsibilitiesmarkdown block is gone. Per-actionMust never:/Must always:bullets render under each action in the## Available Actionssection.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 fromrole.getResponsibilities().size()torole.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—responsibilitiesblock is now sourced from per-actionmustNever/mustAlwaysaggregated intoactionName: constraintstrings rather than from the deleted role-level annotation.DeclarativeRole— the auto-generated declarative role no longer overridesgetResponsibilities()(no template method to override).ConfigurableRole— drops theroleResponsibilitiesfield; instances built viaRoleBuildernow carry only identity + LLM.
Migration
Before:
@RoleIdentity(name = "Researcher", goal = "Find academic information")
@Responsibilities(
duties = {"Search databases"},
mustNever = {"fabricate references"},
mustAlways = {"cite sources"}
)
public class ResearcherRole extends Role { }After:
@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:
@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; needsMeterRegistryplumbing intnsai-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.mdpage + integration test (100 calls × 3 providers).AgentBuilder.captureLLMCalls(...)opt-in helper — once builder API ergonomics are signed off.
PRs: #284 (consolidation), #285 (LLMCallLog phase 1), #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. SeverityWARNING(soft-launch). Newcom.tnsai.enums.LLMCapabilityenum (STREAMING/STRUCTURED_OUTPUT/VISION;FUNCTION_CALLINGintentionally NOT here — already covered by V003 via tool-presence). Co-located with V003 inLLMCapabilityValidator(shared introspection prologue, independent emit). Closes #238. PR: #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 MCPinitializehandshake at build time. Newcom.tnsai.spi.McpClientFactorySPI intnsai-core+DefaultMcpClientFactoryadapter intnsai-mcp(auto-discovered viaMETA-INF/services). Validator graceful-degrades when the adapter is absent. Closes #237. PRs: #244, #252. - AGENT-V012 tenant scope validator (Phase 1 + 2) —
AgentBuilder.tenantId(String)declares per-tenant agent scope;com.tnsai.spi.TenantAwareis a marker SPI consumers'MemoryStoreimplementations opt into to advertise tenant safety.TenantScopeValidatorfiresWARNINGwhentenantIdis set but the wired store is notTenantAware(or is the build-time defaultInMemoryStore). Suppressible via.relaxValidation("AGENT-V012")(e.g. for one-process-per-tenant deployments). Closes #235. At this release, runtime tenant propagation, tool-side hooks, and audit-event tenant ids remained follow-up work; 0.14.0 later added runtimeTenantContextpropagation while tool and audit integration remain separate. PRs: #248, #250. @Idempotentwired intoToolMethodDispatcher— the primitives shipped in PR #108 (annotation, SPI, key derivation, in-memory store, exception) now have an active call site. NewIdempotencyResolverorchestrator handles all fourKeyStrategyvalues + all threeRetryBehaviorpolicies + failure caching opt-in + store unavailability. NewIdempotencyKeySupplieropt-in interface forKeyStrategy.EXPLICIT. Tools without@Idempotentbypass the resolver entirely (zero overhead, per-Methodcache for reflection-free dispatch). PR: #251.Idempotency-KeyHTTP header injection onWEB_SERVICEactions —WebServiceExecutorinjects the same key the resolver uses internally onto outgoingPOST/PUT/PATCH/DELETErequests 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.deriveKeypromoted topublic staticso the header path and the cache path land on the same key (EXPLICIT suppliers must be deterministic — called twice per call). PR: #253.- Release CI hardening (3 of 7 #203 items) — `release.yml` now runs three independent gates before the Maven Central deploy step:
- Preflight (#254, #203 item 2) — same
scripts/release-preflight.shmake preflight VERSION=X.Y.Zruns 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, #203 item 4) —
mvn deploy -P releaseto a local file repo + per-module artifact validation (poms + jars + sources + javadocs + signatures) before the immutable Central deploy. - Downstream drift check (#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.
- Preflight (#254, #203 item 2) — same
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;LLMCapabilityValidatorrow clarified "(FC head)" / "(declared head)" to disambiguate V003 + V004 sharing one class. PRs: #244, #246, #250.IdempotencyResolver.deriveKey— promoted fromprivatetopublic static. Necessary so the HTTP header injection path inWebServiceExecutorderives the same key the resolver's internal cache lookup uses; otherwise the header value and the cache lookup would diverge forKeyStrategy.HASH_INPUT. PR: #253.
Fixed
- Bare
<NNNpatterns escaped in 0.8.5 CHANGELOG entry —<100msand<500msparsed as MDX tag-opens byfumadocs-mdxin 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.
Stats
- 11 commits since
v0.8.5 - All 12
#85design validators now shipped (was 9):AGENT-V004,AGENT-V006,AGENT-V011,AGENT-V012previously deferred, all in - 1 new
tnsai-coreSPI (McpClientFactory) - 1 new
tnsai-coreSPI marker (TenantAware) - 1 new
tnsai-coreenum (LLMCapability, 3 values) - 1 new
tnsai-mcpadapter (DefaultMcpClientFactoryviaServiceLoader) - 4 new
AgentBuildersetters (tenantId,toolCallFilterwas 0.8.5 — addingtenantIdhere) - 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
@ToolPOJO hasrequiresConfirmation = trueAND noToolCallFilterhas been wired throughAgentBuilder. Suppressible via.relaxValidation("AGENT-V006"). SeverityWARNING(soft-launch). Closes #234. PR: #243. @Tool.requiresConfirmation()— boolean annotation field for tool authors to declare a runtime safety gate. Independent of@Tool.idempotent()(retry hint) andToolRiskLevel(gradient classification) — the existingToolRiskLeveljavadoc already pre-referenced this field as "the boolean gate". PR: #243.AgentBuilder.toolCallFilter(ToolCallFilter)— pre-build setter parallel to the existing post-buildAgent.setToolCallFilter(). Wired via the same pending-pattern used forKnowledgeBasesobuild()stays lazy. Null filter clears the slot symmetrically with the post-build setter. PR: #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.ValidationPipelinePerformanceTest— pins the<100msSLA from #85 acceptance ("typical agent validates in<100ms(static checks only)"). Asserts<500mswith 5× margin for CI runner jitter; locally completes in 10–30ms. PR: #241.tnsai-core/agent_docs/validation.md— first-class consumer-facing docs covering all 9 shipped validators, theHealthcheckableSPI with implementation contract, opt-in reachability + per-probe timeout, suppression model (single + bulk), performance SLA,AgentValidationExceptionshape, and the 3 deferred validators with their blocker issues. PR: #241.
Fixed
slf4j-simpleno longer leaks at compile scope fromtnsai-evaluationto consumers. The dep was declared without an explicit<scope>, defaulting tocompileand 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. PR: #242.
Stats
- 3 commits since
v0.8.4 - 1 new public annotation field (
@Tool.requiresConfirmation) - 1 new
AgentBuildersetter (toolCallFilter) - 1 new
AgentBuilderoverload (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
<100msSLA - ~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
ImageGenToolswith three@Toolmethods (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 todata:image/png;base64,…URI for shape parity. NewBuiltInTool.IMAGE_GEN_TOOLSenum entry. PR: #221 (closes #93 Phase 1). - Audio generation toolkits — two POJOs (
TextToSpeechTools+SpeechToTextTools) covering the canonical non-OpenAI alternatives toMediaTools. 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). NewBuiltInTool.TEXT_TO_SPEECH_TOOLS+SPEECH_TO_TEXT_TOOLSentries. PR: #222 (closes #93 Phase 2). ChannelScopedIdvalue type intnsai-channels— typed(channelId, senderId)record replacing the ad-hocchannelId + ":" + senderIdstring concat. Compact constructor refuses a separator-bearing channelId;parse()splits on the first colon so a senderId with internal colons (SlackT123:U456) round-trips losslessly.UnifiedMessage.scopedId()convenience helper added (parallel to existingsessionKey()— sender-scope vs conversation-scope). PR: #224 (closes #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,.envreads,~/.aws/credentialsreads, env-var dumps,curlPOSTs of secret files,display:none/visibility:hidden/white-on-white HTML, zero-width-character payloads, HTML-comment overrides. NewContextFileSourceenum (TNSAI_MD / CLAUDE_MD / AGENTS_MD / README_MD / OTHER) tags audit-log entries. NewInjectionTypeenum values:CREDENTIAL_EXFILTRATIONandHIDDEN_INSTRUCTION. PR: #228 (closes #35). - AGENT validator family expansion —
LLMCapabilityValidator(AGENT-V003, function-calling capability check) andHealthcheckableSPI + reachability validation infra (AGENT-V005). PRs: #233, #236 (advances #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 thequalityprofile. PR: #206 (closes #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. - JaCoCo coverage gate added for
#9protected 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, #227, #229, #230, #231, #232 (advances #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
@Toolmethods (3 image + 6 audio) + 2 new AGENT validators (V003, V005) - 3 new
BuiltInToolenum entries - 2 new
InjectionTypeenum 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.shouldHandleProcessExitflaky test (#205): the startup race inwaitForProcessStart()polledprocess.isAlive()in a 50 ms loop, treating "alive" as a proxy for "started". Wrong for fast-exiting processes — a one-shot likeechowrites its line and exits between two polls, soisAlive()returnsfalseeven though the process started, ran, and produced output successfully (pipe data persists in the kernel buffer after the writer exits). Replaced the loop withreturn process != null—ProcessBuilder.start()is synchronous on POSIX/macOS, so by the time we hold aProcessreference exec(2) has succeeded. Test also migrated fromThread.sleep(500)to aCountDownLatch(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@WebServicejavadoc has documented this auth type since the annotation landed, but the enum only definedNO_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 inWebServiceExecutor.addAuthHeadersthat reads from@WebService.authTokenEnvand uses@WebService.apiKeyHeaderfor the header name (defaulting toX-API-Keywhen empty). The orphanapiKeyHeaderannotation 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.
[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[]asInvariants: rule1, rule2, rule3(single comma-joined line). Fix emits each rule on its own bullet:Invariants: - rule1 - rule2 - rule3@State.invariantsis 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
[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 Javarecord, a POJO withgetX/isXaccessors, or a class with public fields into aMap<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 toParamBeanMapper.toMap(input)then to the existingexecuteAction(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
[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 aBuiltInToolentry's backing class is missing from the classpath (typically becausetnsai-toolsis not a dependency)BuiltInToolenum: per-entrygetClassName()accessor +instantiate()reflective constructor
Changed
BuiltInToolenum 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@Toolmethod the toolkit exposesBuiltInTool.AI_TOOLSrenamed →VISION_TOOLS(the backingAiToolsPOJO ships onlyimage_analyze; the previous name was misleading)LLMRoleExecutornow readsllmTemperature/llmSystemPromptfrom@ActionSpecdirectly (was@LLMToolnested annotation)ActionExecutorLLM-branch routing simplified — everyActionType.LLMaction now goes throughLLMRoleExecutorregardless of (former)availableToolscontent; tool calls are dispatched by the agent-levelToolMethodDispatcher
Removed
- BREAKING:
@com.tnsai.annotations.LLMToolannotation (every field:tools,customTools,maxToolCalls,parallelToolCalls,systemPrompt,temperature,maxIterations,stopSequences,includeToolHistory,mcpServers,bindings,returnKey) - BREAKING:
@com.tnsai.annotations.ToolBindingannotation +@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@Toolmethod parameters that the LLM populates directly from its function-call arguments - BREAKING:
@ActionSpec.llmTool() : LLMToolannotation field - BREAKING:
com.tnsai.metadata.LLMToolConfigrecord (use the@ActionSpec.llmSystemPrompt()/.llmTemperature()accessors onActionMetadatadirectly) - 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 theDataAnalystRoleAnnotationRoundTripTest— they demonstrated the deleted cookbook against an executor that no longer existed LLMToolConfigTesttest class- 7 stale
LLMToolsExecutorJavadoc references inactions/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/searchwith no static index materialised; now wirescreateFromSource(source).staticGET()to astatic.jsonroute handler and passessearch={ options: { type: 'static', api: '/static.json' } }toRootProvider. ~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 rootCLAUDE.md,tnsai-core/.../annotations/{ActionSpec,LLMTool,ToolBinding}.javaJavadoc, and the entire docs-sitecapabilities/tools/+tutorials/- Quick-Start surface — all rewritten against the post-RFC-#188 reality (no
Toolinterface, no*Toolclasses, no@LLMTool, noLLM_TOOL/LLM_ROLEaction types, accurate tool counts)
- Quick-Start surface — all rewritten against the post-RFC-#188 reality (no
ActionSpec.javaJavadoc: action-types table +@LLMTool-using example swapped for the newllmSystemPrompt/llmTemperatureshape
Migration
The compile-time fix for any consumer using @LLMTool:
// 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:
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
[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
DynamicToolMethodrecord — runtime-defined tool variant for proxies and plugin systemsStaticToolMethodrecord — extracted reflection-dispatch logic fromToolMethodDispatcherAgentBuilder.dynamicTool(DynamicToolMethod)and.dynamicTools(List<DynamicToolMethod>)AutoTeamBuilder.dynamicTool(...)/.dynamicTools(...)mirror APIsMcpProxyTool.toDynamicToolMethod(...)static factory — replacesimplements ToolToolMethodDispatcher.lookup(name)and.registry()accessorsTnsAIToolProvider.fromDynamic(DynamicToolMethod...)and.from(pojos, dynamicTools)factories
Changed
ToolMethodis now asealed interface(was a record); permitsStaticToolMethod,DynamicToolMethodMcpToolBridge.toTnsAITools()returnsList<DynamicToolMethod>directly (no wrapper)ActionExecutorconstructor now takesToolMethodDispatcher(wasList<Tool>)ActionExecutor.executeExternalTool(String, Map<String, Object>)(was(String, String))UnifiedContextAssembler.tools(List<ToolMethod>)(wasList<Tool>)
Removed
- BREAKING:
com.tnsai.tools.Toolinterface - BREAKING:
AgentBuilder.tool(Tool),.tools(List<Tool>),.getToolsList() - BREAKING:
ConfigurableAgent.getExternalTools() - BREAKING:
ToolSchemaGenerator.generateToolSchema(Tool) - BREAKING:
McpToolBridge.TnsAIToolWrapperadapter class - BREAKING:
TnsAIToolProvider.fromTools(Tool...)factory + legacy dispatch branch - BREAKING:
AutoTeamBuilder.tool(Tool)/.tools(List<Tool>) ToolMethodAdapterbridge class (no consumers left after migration)ToolFailureModeannotation +ToolFailureModeReaderhelper- Orphan
tnsai-integration/.../CsvLoaderRoleBindingTest(referenced llmtools deleted in 0.6.0)
Fixed
CancellationTokenconcurrency race: concurrentcancel()+onCancel()could fire a callback twice. Now exactly-once via per-registrationAtomicBooleanguard.
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
DynamicToolMethodconstructed 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
[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.toolstype changedList<ToolDefinition>→List<Map<String, Object>>(JSON-Schema fragments, Anthropic-style tool-use format) Toolinterface slimmed (369L → 138L) — kept core contract + safety/policy hints
Removed
- BREAKING:
ToolDefinitionrecord + builder +fromMap/toMapshelpers - BREAKING:
ToolSchemaGenerator.generateToolDefinition*methods (3 overloads) - BREAKING:
Toolinterface metadata-discovery surface — 12 default methods removed:getCategory,getUsageExamples,getMetadata,getSearchKeywords,getPriority,canHandle,getAllowedCallers,isParallelizable,getReturnFormat,getLatencyCategory,getShortDescription,executeAsync - BREAKING:
ToolMetadata,ToolCategory,ToolLatencytypes - Legacy
actions/llmtools/subsystem @ToolSpecand@ToolActionannotations + reflective extractor- Hooks/policy/validators ecosystem (
Pre/Post/Error/Register ToolUseevents,ToolPolicy*,ToolApprovalValidator,LLMCapabilityValidator) ToolMetrics(628L) +ToolExecutionMetricToolRegistry(225L) +ToolProviderSPIAgentBuilder.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, #195, #196, #197, #198, #199, #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-fastToolMethodDispatcher— Jackson type-aware coercion +Method.invokedispatchJsonSchemaGenerator— derives JSON Schema fragments from@Tool/@ToolParammetadataToolMethodAdapter— bridges function-shapeToolMethodto legacyToolinterface (deleted in 0.7.0)AgentBuilder.toolPojos(Object...)registration pathTnsAIToolProvider.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.javalegacy implementations undertnsai-tools - 32
*ToolProvider.javaSPI factory classes - 18 framework infrastructure files (
AbstractTool,AbstractCategoryToolProvider, validation/health/manifest/enhancement helpers) - 152 corresponding
*Test.javafiles tnsai-toolsSPI 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.
[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.jsonpathoptional dep) andCSVParserTool(com.opencsvoptional dep) were still on the eagertoolSuppliers()list. SameLinkageErrorfailure mode as 0.5.5 (#184) — taking the whole provider down when a consumer pulledtnsai-toolswithout those transitives. Moved both toreflectiveToolClassNames()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)AbstractCategoryToolProviderIsolationTestupdated 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
[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— eagerXYZTool::newmethod-references intoolSuppliers()resolved theirMethodHandleatList.of(...)evaluation time, outside the per-tool try/catch. Missing PDFBox or MarkItDown transitive →LinkageErrorfrom 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/catchAbstractCategoryToolProviderIsolationTest(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 fromtoolSuppliers()toreflectiveToolClassNames().toolSuppliers()keeps the 5 base-JDK + opencsv tools.
Migration
None — pure bug fix. Behaviour-changing only when an optional dep is missing.
PR: #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
@LLMToolruntime path surfaced viaLLMToolsExecutor([#167]) +DataAnalystRolereference +BuiltInToolEnumAuditTest@WebServiceruntime path surfaced viaWebServiceExecutor([#174]) +WeatherRolereferencecom.tnsai.guardrailspackage —@InputGuardrail/@OutputGuardrailenforcement withminLength/maxLength/blockPatterns/allowPatterns+onFailure∈{REJECT, WARN, SANITIZE, REVIEW}([#176])- Optional
RetrievalSpiintnsai-core+ default impl intnsai-intelligence(RoleRagBinding,LocalFileSourceLoader,DefaultRetrievalSpi) — wires@KnowledgeSource/@Retrievalend-to-end ([#178]) @ToolBindingdeclarative tool-input mapping with${param}/${role.name}/${action.name}/${env:VAR}substitution ([#179])com.tnsai.resiliencedecorators —@Traced(MDC trace-id),@Metered(in-memoryResilienceMetrics),@Fallback(forActionbinding + 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 liveActionExecutorpipeline ActionParams.firstStringValueInDeclarationOrdershared helper
Changed
@ToolBindingsimplified to single-fieldtool()(was two mutually-exclusivebuiltIn+customfields withBuiltInTool.NONEsentinel) — single source, identical syntax for built-in and custom tools ([#181] refactor of [#179])FallbackResolver.tryRecoverandRetryCallback.invoke()narrowedcatch (Throwable)→catch (Exception)(caught bySourceHygieneTest.noBroadThrowableCatchesfrom #41)LLMToolsExecutornon-deterministicparameters.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/@ContentFilterstandalone enforcement (#171 follow-up)InputValidator/InputSanitizerSPI for customClass[]hooks@MemorySpecresolver (Persistence.REDIS / DATABASE / FILE)- KnowledgeType source loaders (URL / VECTOR_DB / DATABASE / WEB_SEARCH)
- Embedding SPI replacing
HashEmbeddingFunction @RateLimited,@Resilience(circuitBreaker),@Idempotentkeyed cache (need distributed-state SPI)- OpenTelemetry SPI for
@Traced; Micrometer/Prometheus sink for@Metered - Build-time validation (fail-fast on
@ToolBindingtypos) AuthType.API_KEYenum value ([#175])
PRs: #167, #174, #176, #178, #179, #181, #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 ProviderErrorMappers (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. Handlesrequests_too_many(alongsiderate_limit_exceeded) →MODEL_OVERLOADEDand Mistral's strictermodel_quota_exceededsemantics.MistralAIClient.chat/streamChatrefactored toexecuteRequest("Mistral"). -
BedrockProviderErrorMapper(PR #157) — first mapper that works against AWS SDK exceptions, not HTTP responses.BedrockClient.mapAwsExceptionextracts the AWS error code, reconstructs an AWS-shape envelope, propagatesx-amzn-requestidvia headers, and feeds the SPI mapper's HTTP-style API. Same SPI contract handles both code paths so consumers see typedLLMExceptionregardless 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) toMODEL_OVERLOADEDso consumer fallback chains treat them as transient. Capturesgroq-regionheader for routing-issue triage. -
OpenRouterProviderErrorMapper(in PR #165) — aggregator envelope. Surfaces the upstream provider name viametadata.provider_nameso consumers triaging an OpenRouter failure can see which downstream provider actually misbehaved. -
AzureOpenAIProviderErrorMapper(in PR #165) — OpenAI-compat body, Azure deployment-id model field, capturesapim-request-id/x-ms-regionheaders for Azure-specific triage. Distinguishes Azure'scontent_filter(Azure's responsible AI gating) from OpenAI's lexical codes. -
CohereProviderErrorMapper(in PR #165) — Cohere's RAG-focused API. Maps thecommand-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 toSERVER_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 byMiniMaxClient) AND nativebase_resp.status_codeat/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 inspectsbase_respfirst. -
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/streamChatpaths preserve identicalChatResponsereturns; the only observable difference is that failures throw typedLLMExceptioninstead 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:
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 ProviderErrorMappers
-
GeminiProviderErrorMapper(PR #152) — Google API-Gateway envelope (error.{code,message,status}). Routes the canonicalgoogle.rpc.Codestrings (RESOURCE_EXHAUSTED→MODEL_OVERLOADED,UNAUTHENTICATED/PERMISSION_DENIED→AUTHENTICATION_FAILED,INVALID_ARGUMENT→INVALID_REQUESTwith token-hint demotion toCONTEXT_TOO_LONG, etc.). Capturesx-goog-*+retry-afterheaders.GeminiClient.chat/streamChatrefactored to useexecuteRequest. -
OllamaProviderErrorMapper(PR #154) — heuristic on the free-texterrorstring (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 botherroras plain string and as object withmessagefield).OllamaClient.chat/streamChatrefactored to useexecuteRequest.
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 ProviderErrorMappers (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
executeRequestrefactor.
[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-V006ToolApprovalValidator— WARNING when a registered tool reportsrequiresConfirmation()==truebut the agent has no built-in confirmation channel wired (the operator must callagent.setToolCallFilter(...)post-build, otherwise calls block indefinitely waiting for a confirmation that never arrives).AGENT-V007CapabilityClasspathValidator— ERROR when a@Capabilityinterface 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-V008ActionNameCollisionValidator— ERROR when two roles declare@ActionSpecmethods with the same name; today the framework's name → action map silently keeps whichever role was registered last.AGENT-V009ResilienceConfigValidator— ERROR for clearly invalid@Resiliencenumerics (negative timeout / maxAttempts / backoff, multiplier < 1.0, failureRateThreshold outside 0–100); WARNING for configured-but-effectively-disabled subsystems (@Retrywith non-default fields butmaxAttempts==0,@CircuitBreaker(enabled=true)with non-positivefailureThreshold,@RateLimit(enabled=true)withmaxRequests<=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 viaServiceLoader; consumers add Sentry / Loki / custom sinks by dropping a JAR with aMETA-INF/services/com.tnsai.observability.errors.ErrorReportPublisherentry.Slf4jErrorReportPublisher(default, SPI-registered) — JSON-serializes the report via Jackson +Jdk8Module(soOptional<T>unwraps to value-or-null) +JavaTimeModule. Logs at WARN forTRANSIENT/RESOURCE/EXTERNALcategories (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 viaErrorReports.setPublisher(...)and tear down viaErrorReports.resetForTesting().ErrorReports— static facade with lazy SPI discovery. The one-line emit entry point: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-V003code collision (ToolNameUniquenessValidatorandLLMCapabilityValidatorboth report underV003); rename pending operator approval (Protected Change perCLAUDE.md). - #145 —
AGENT-V004LLM streaming/structured/vision capability validator needs a builder capability-declaration API first. - #146 —
AGENT-V012tenant-scope validator +#92per-tenant error budget both blocked on a multi-tenant runtime feature that doesn't exist yet. DedupingErrorReportPublisherdecorator (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
AgentStateenum extended to the full lifecycle FSM (CREATED → STARTING → RUNNING → STOPPING → STOPPED, plus terminalFAILED). The seed valueREADYfrom 0.4.0 is removed — see Removed.Agent.getState()— new public accessor on everyAgent.com.tnsai.tools.ToolRiskLevel+SideEffectenums.com.tnsai.tools.policypackage:ToolPolicy(ALLOW_ALL/DENY_ALL/SAFE_ONLY),ToolPolicyDecision,ToolPolicyEvaluator, pluscom.tnsai.hooks.policy.ToolPolicyHookconsuming it viaHook<PreToolUse>.com.tnsai.cancellationpackage:CancellationTokeninterface,CancellationException,DefaultCancellationToken(one-shot CAS),NoopCancellationToken(singleton no-op).com.tnsai.timeout.TimeoutPolicyrecord withCategoryenum (LLM_CALL/TOOL_CALL/MCP_CALL/CHANNEL_SEND) andUNBOUNDEDsentinel.com.tnsai.prompt.ModelFamilyenum +fromModelId(String)best-effort mapper covering Claude / GPT / Gemini / Llama naming conventions.com.tnsai.tools.spi.ToolFailureModeannotation +ToolFailureModeReaderresolver — tool authors declare retryable / non-retryable exception classes;nonRetryablebeatsretryablein conflict resolution.com.tnsai.security.DestructiveCommandDetectorintnsai-quality: content-levelToolCallFilterwith a 21-pattern catalogue (rm -rf,git reset --hard,ddto/dev/,mkfs,chmod 777/000,kill -9broadcasts, redirects to/etc/*/~/.ssh/*,sudo rm/dd, shutdown / reboot --force, shred -ru, wipefs --force).LLMCapabilityValidator— fourth validator in theAgentBuilderpre-flight pipeline (issue #85 slice). Catches the canonical "tools registered + LLM doesn't support function-calling" misconfiguration at build time with stable codeAGENT-V003.DiscoveredRoleActions.getActions()— public list accessor.
Added — interface extensions (default methods, additive)
Tool.getRiskLevel()→ToolRiskLevel.MEDIUM,getRequiredSecrets()→ emptySet<String>,getTimeout()→Duration.ofSeconds(30),getSideEffects()→ emptySet<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)— wirescache_control: ephemeralmarkers into system + last N user messages (capped at the 4-per-request Anthropic limit).OpenRouterClient.setFineGrainedToolStreaming(boolean)with auto-detection from model id — addsx-anthropic-beta: fine-grained-tool-streaming-2025-05-14on the wire when routing to Claude.
Added — infrastructure & tests
- PIT mutation-testing pilot (
-Pmutation-testingprofile intnsai-core/pom.xml) — baselines 74% mutation coverage. Doc:tnsai-core/agent_docs/mutation-testing.md. SourceHygieneTestintnsai-core— regression gate forbiddingcatch (Exception ignored)andcatch (Throwable t)in main sources (issue #10).ProviderEnvVarConsistencyTestintnsai-llm— drift gate forrequireApiKeycall 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 isSTOPPING,STOPPED, orFAILEDwithIllegalStateException.Agent.stop()is idempotent.ExternalScriptHook.apply()narrowedcatch (Throwable t)→catch (IOException | RuntimeException t). JVM-fatalErrorsubclasses propagate now.TelegramAdapter.send()retries 429 / 5xx with exponential backoff (1s, 2s) up to 3 attempts.BridgeLLMClient.streamChat()throwsLLMCapabilityException(was silent degrade to single-element synthetic stream).BridgeLLMClient.chat()throws typedLLMException(was rawRuntimeException).BridgeLLMClient.getCapabilities()overrides model-id guess with honest transport-bound limits.SystemPromptBuilderstate + action sections aligned byte-for-byte withRolePromptBuilder.@PromptTemplates+@State.template+@State.invariantshonoured.LLMConfigurationenv lookups switched from rawSystem.getenv()to Core'sEnvLoader.get()(3 sites).
Removed
AgentState.READY— the 0.4.0 seed value (placeholder for the lifecycle FSM that landed in this release). Migrate toAgentState.RUNNING. No@Deprecatedshim per project rule.
Migration notes
AgentState.READY→AgentState.RUNNING(search-and-replace).Agent.chat()afterstop()now throwsIllegalStateException.BridgeLLMClient.streamChat()now throws — installtnsai-llmfor real streaming, or callchat()for buffered single-shot.BridgeLLMClient.chat()failures: catchLLMException(or parentTnsAIException) instead ofRuntimeException.- SCOP-rendered prompts changed format. Pinned-format tests should update to the canonical
RolePromptBuildershape.
For Consumers
<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 everytnsai-*module to a single coherent version. Consumers import once and use modules without version declarations.@Capabilitypattern (from formerTnsAI.Core0.3.1 pre-release work): reusable action contracts as interfaces withdefaultbodies that throwActions.dispatchedByFramework(). Seetnsai-core/src/main/java/com/tnsai/capabilities/Capability.java.ActionDiscoverytwo-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 concreteActionType.LOCALimplementation.
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.ymlplusrelease.yml. Cross-repo clone /DEPS_PATpattern retired. - Each child
pom.xmlshrinks from ~400 lines to ~50–100 lines.
For Consumers
Depend on the framework via the BOM:
<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.tansuasicicarries 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 underio.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.
Migration
TnsAI is pre-1.0, so the public API can shift between minor versions. The current published Maven Central release is listed on Installation. The framework deliberately does not ship long-lived @Deprecated shims — when a replacement lands, the old surface goes in the same release. That means every upgrade is potentially a small surgical step rather than a sprawling deprecation cleanup, but it also means you have to read the right release notes to know what to change.
Migrate 0.11 → 0.12
This is the combined 0.11.0 → 0.12.0 path. Source of truth is the framework CHANGELOG [0.12.0] - 2026-06-04 (published via Changelog). The current lockstep line is 0.14.0 — after this page, read 0.13.0 and 0.14.0.