---
title: Migrate to 0.18
description: The breaking changes of each 0.18.x release, in full, and what to change for each.
---

# Migrate to 0.18

This page holds the breaking changes of each 0.18.x release in full, and what to change for each. The [Changelog](../help/changelog.md) lists every change in one line and links here for the detail. Read every section from the release after the one you are on, up to the one you move to, newest first. Each change is marked as in the changelog: **BREAKING** fails to compile or link, and a marker such as **BREAKING (behavior only)** changes what running code does without failing to compile.

## 0.18.7

### Case-insensitive knowledge selectors

**BREAKING (behavior only):** `@Knowledge` `include` and `exclude` selectors now match paths ignoring case, so both widen: `**/*.pdf` now also picks `B.PDF` and `c.Pdf`, and `drafts/**` now also drops `Drafts/x.md` and `DRAFTS/y.md`. Pattern and path are lower-cased one code point at a time with `Character.toLowerCase(int)`, and the final `ς` folds to `σ`, so a Turkish default locale changes nothing: `I`, `i` and `İ` match one another, the dotless `ı` matches only itself, and `ς`, `σ` and `Σ` match one another. Every path a selector matched before still matches it. A source whose include picked only files of one case now ingests their other-case siblings too, and one whose exclude dropped only one case now drops the others. `IngestionPathSelector.matches(...)` ignores case the same way. Lines in `.gitignore` and `.tnsignore` still match case-sensitively, so `secret.md` there does not drop `SECRET.md`.

## 0.18.6

The japicmp gate is lifted for 0.18.6 alone, so some of these changes fail to compile. Run `dev.tnsai.rewrite.UpgradeTnsAI_0_18_6` from `tnsai-rewrite` first: it marks (`/*~~>*/`) every call to the removed send methods and rewrites nothing.

### Relative FILE paths follow the base directory

**BREAKING (behavior only):** a relative FILE `@Knowledge` path written in an annotation, and one a `@Config` file entry keeps without giving `path`, now resolves against the base directory `ClassConfig.useBaseDirectory` set, when one is set, instead of always against the working directory. Without a base directory such a path still resolves against the working directory, and absolute paths and URLs are kept as written. A relative path a `@Config` file or a run file gives already resolved against the base directory and still does. The path resolves when the source is read, not when the annotation is declared, so a base directory set later applies from the next run. The loader, the setup-time readiness check (`KnowledgePreflight`), the startup presence check and `tnsai-server`'s `FileIndexer` resolve it the same way. So does any other relative FILE path a source is given: one built in code with `KnowledgeSourceConfig.file`, or a relative location passed to `RoleRagBinding.forRole`, while `FileIndexer.index(Path, …)` still resolves its `Path` against the working directory. Under a base directory, a `@Config` file that restates the annotation's relative path, or names its absolute path under the base, gives the same source as the annotation, so a source with presence `WHEN_CONFIGURED` named that way is not configured and is skipped. An absolute path under the working directory that it named before is now a different source and is configured. A source skipped because a `@Config` entry gives the annotation's own path logs a WARN naming the resolved path. A FILE location passed to `RoleRagBinding.forRole` or given at dispatch that resolves to the source's declared path, written relative or absolute and with or without a base directory, now gives the declared source, so it shares the host's own binding and SEQUENTIAL cursor. Before, any such location passed to `forRole`, and an absolute one given at dispatch, had a binding of its own. A relative location now reaches the source as an absolute path, so a stored ingestion fingerprint of such a source no longer matches and the source is ingested once more. A base directory switch reaches the process's cached bindings through the reset `useBaseDirectory` runs, not through the binding key. Bindings already built in an open `KnowledgeScope` keep the base they were built under until the scope closes. Migration: with `@Knowledge(name = "kb", path = "kb")`, a corpus in `/srv/app/kb` and the working directory `/srv/app`, a host that calls `ClassConfig.useBaseDirectory(Path.of("/runs/r1"))` now reads `/runs/r1/kb`. To keep reading `/srv/app/kb`, give `path: /srv/app/kb` in a `@Config` entry or the run file, or move the corpus under the base.

### AgentSpec is inherited

**BREAKING (behavior only):** `@AgentSpec` is now `@Inherited`, as `@RoleSpec` already is. An agent class without its own `@AgentSpec` takes its nearest superclass's whole, and every reader of the annotation sees it as the subclass's own. Fields are not merged: a subclass that declares its own `@AgentSpec` replaces the superclass's entirely, and an `@AgentSpec` on an interface is not inherited. A subclass that must not take the superclass's settings declares an `@AgentSpec` of its own.

- **What a subclass takes.** An existing subclass of an annotated agent, where before it had none of these, now takes the superclass's knowledge and knowledge policies, its `@LLMSpec`, its `@Config` file, its DID settings, including its public key and service endpoint, its groups, which it joins on start when `autoJoin` is set, its `roles`, which it plays when it is given no roles in code, and its `autoStart`, `idleTimeoutMs`, `toolCallFilter`, `memory`, `communication`, `maxContextTokens`, description and version. The common case is an annotated abstract base whose concrete agents declare no `@AgentSpec`: before, they ran as if unannotated, and now they take the base's annotation, so DIRECT knowledge it declares is given on every chat turn of each of them and needs a retrieval runtime such as tnsai-rag at start, and a `memory` it declares with `FILE` persistence makes each of them write its conversation history to disk, or with `REDIS` or `DATABASE` makes each of them open a connection to its `connectionUrl`. Each concrete agent binds and indexes the inherited knowledge on its own, as two annotated classes declaring the same source do, so the sources load once for each concrete agent that uses them. A playground class that `SCOPBridge` resolves likewise gives its superclass's `llm` as the playground tier.
- **The subclass's own code still wins over an inherited memory or communication.** A `getMemoryStore()` or `getCommunication()` override in any class below the annotated superclass, or a `@CommunicationStyle` on the agent's own class, wins over the inherited `memory` store or `communication`. A class that declares its own `@AgentSpec` keeps its annotation's `memory` and `communication` first, so an override of either method on the annotated superclass itself, which its subclasses got before, now gives way in them to the annotation they inherit. The override replaces the inherited store or style rather than wrapping it: `super.getMemoryStore()` and `super.getCommunication()` return the superclass's implementation, `Agent`'s default unless a class in between overrides it, not the annotation's. Bridge and synthetic methods do not count as an override, and a generated subclass, such as a proxy, whose override of either method is neither counts as the subclass's own code. So a proxy that intercepts either method, even only to call `super`, gives the agent the superclass's implementation of the method instead of the annotation's. A proxy that must keep the annotation's leaves both methods out of what it intercepts. A generator that copies the class's annotations onto its generated class, such as Mockito's subclass mock maker, makes that class declare the `@AgentSpec`, so there the subclass's own override gives way to the annotation as it does in a class that declares its own. The inherited `memory`'s `maxContextTokens` still applies unless the subclass overrides `getMaxContextTokens()`.
- **A fixed DID is not inherited.** A class that takes its `@AgentSpec` from a superclass whose `did` names a fixed `agentId` now fails at start, even when it is the only subclass (`… inherits @AgentSpec from …, whose did names the fixed agentId '<id>'; a fixed DID is not inherited, so declare it on <Class>'s own @AgentSpec`), and a builder-given DID does not lift this. A class that wants a fixed DID declares it on its own `@AgentSpec`. An inherited `did` without an `agentId` still gives each agent a DID of its own id.
- **Roles sections.** An inherited `@Config` file's `roles` sections now bind the subclass's roles, so a subclass that plays fewer roles than they name fails at start (`roles [<name>] are not played by this agent; …`). A run file `roles.<Role>` section keyed by the superclass is not the subclass's, so when the superclass and a subclass that plays that role through the inherited `roles` start in one run, the second fails (`Role … is configured by … and differently by …`) unless the subclass's key repeats the section.
- **A subclass that declares only `@RoleSpec`** is now a class with both annotations rather than a role class. Its knowledge is its role's and the inherited agent's together, and its role knowledge now reaches its chat as well as the actions that name it, so role DIRECT knowledge is given on every chat turn and needs a retrieval runtime such as tnsai-rag at start. The rules over a class's whole knowledge now run over both together, so it fails at start when, for example, its `@RoleSpec` and the inherited `@AgentSpec` both name a `@Config` file (`… names a @Config file in both @AgentSpec and @RoleSpec; keep one`) or both declare knowledge of one name (`… declares knowledge '<name>' twice`). A run file section `Agents.<Subclass>` is now read as its agent section. Before, the run's `ClassConfig` refused it as naming a role class when keyed by the binary name or when the host gave the run's agent types, and ignored it otherwise. A simple-name key it shares with another agent class of the run is now ambiguous and fails, where before the role class gave way.
- **Knowledge given in code or by a run file.** An unannotated subclass's own `getKnowledge()` entries now layer by name over the inherited knowledge instead of standing alone, and a run file section may now give it knowledge, where before that was refused for a class without `@AgentSpec`. A run file section keyed by the superclass still configures only the superclass, not its subclasses. Messages and run file sections name the subclass.

### A single-owner turn follows the multi-owner rules

**BREAKING (behavior only):** a `SCOPBridge.sendToLLM(ChatTurn)` turn with one owner gets the knowledge, checks, rendering and fail-open rules of a turn with several. An owner whose class carries `@RoleSpec` without `@AgentSpec` now gives its `@RoleSpec` knowledge as the turn's one owner, as it already did on the 0.18.5 list call: its enabled DIRECT knowledge is given on every chat turn, which needs a retrieval runtime such as tnsai-rag, and a binding passed for it is checked against its `@RoleSpec` SEARCH declaration, so one whose source or `topK` does not match is refused with `IllegalArgumentException` and nothing is dispatched. Before, the 0.18.5 single-owner `sendToLLM` read only `@AgentSpec` knowledge for chat, so such an owner gave no knowledge and its binding was searched after an ownership check alone. A binding that belongs to another owner is now refused with `IllegalArgumentException` when the turn is built, before anything is sent, and its message still names the owner's class. An owner whose class carries `@AgentSpec` gets what it got before.

### The principal moves to the invocation

**BREAKING:** `SCOPBridge.principal(AgentPrincipal)` and `SCOPBridge.getInstance(AgentPrincipal, LiabilitySink, AuthorityScope)` are removed, a binary and source break. The principal no longer sits on the bridge, where every later call of every agent shared it: pass it on the `InvocationContext` of `executeAction(target, action, params, resolvedPaths, InvocationContext)`, for example `InvocationContext.autonomous(executingAgent, principal.id(), scope, principal)`, and set the sink and scope with `liabilitySink(...)` and `authorityScope(...)`. A dispatch whose invocation carries no principal, and a call with no invocation, now fail the wiring check (`principal=missing`) before retrieval or the action body.

### New components on ChatRetrievalContext and RetrievalRequest

**BREAKING:** `ChatRetrievalContext` gains the components `turnMessage`, `conversationId` and `sourcePaths`, and `RetrievalRequest` the same three, a binary and source break for a call of either canonical constructor and for a record pattern over either record. Their builders are unchanged and leave the new components empty. `turnMessage(String)`, `conversationId(String)` and `sourcePaths(Map)` set them, and the canonical constructors refuse a null `Optional` or map and a blank message, conversation or source name. `ChatKnowledgeBinding.retrieve` copies the three from the context into the request as components, not as metadata filters, and searches the turn message, when there is one, instead of the message the provider renders. A turn's `sourcePaths` move the chat's DIRECT sources to the locations it names, as an action dispatch's locations do (see [Where a FILE source is read](../rag/index.md#where-a-relative-file-path-resolves)). A cached result is kept apart per turn message, conversation and source location.

### PathRedactor moves package

**BREAKING:** `PathRedactor` moved from `com.tnsai.integration.activity` in tnsai-integration to `com.tnsai.redaction` in tnsai-core, a binary and source break: change the import. It now implements `Masker`, which is a `UnaryOperator<String>`, so it is still accepted wherever a `UnaryOperator<String>` is, such as the `AgentActivity` constructor, and its constructor and `apply` are unchanged.

### Source locations are given per call

**BREAKING:** `SCOPBridge.resolvedSourcePaths(Map)` is removed, a binary and source break. Source locations are given per call through `executeAction`'s `resolvedPaths` argument and the `requireLoaded`/`requireReady` overloads that take one. `null` there now means no locations, where before it fell back to the map the setter kept.

### Agent record lines carry a conversation field

**BREAKING (format and behavior):** the `.history` and `.log` lines that `AgentRecordFormat` gives a host carry a conversation field after the episode state and before the writer's name: `[<conversation>]` from the `conversation-id` MDC key, or `[-]` on a line written outside any conversation, as in `2026-10-21 18:00:00 [running] [conv-1] Alice has started episode 1.`. A host that writes its agents' files with these patterns, such as SCOP's logger factory, writes the new field, and a reader that splits these lines by position, such as a tail view, must skip it. A message of several lines is now written with two spaces before each line after its first, so a line in a message, such as a forged record of another conversation, continues the record it was written in. `RecordParser` parses a line only from its start, so an indented line never starts a record. Known limit: only a message's own lines are indented. Lines written before 0.18.6, and the stack trace logback appends after a message, are not, so a line in them that looks like a record, such as one of another conversation, is read as a record of that conversation. `RecordParser` reads lines without the field too. For `IncrementalTextFilesRetriever`, a line without the field, such as every line of a run written before the field, is a fieldless record: a fieldless line in the current episode does not enter evidence and is reported with one WARN per file, while a fieldless line of another episode enters, and a line before a file's first record counts as fieldless. This applies only to a request with both a current episode and a conversation id. A request without either is given no `.history` or `.log` record at all.

### PatternRedactor hides more

**BREAKING (behavior only):** tnsai-quality's default `PatternRedactor` catalog now takes its credential patterns from `CredentialPatterns`, and two of them hide more. `jwt` also matches a token whose signature segment is empty or shorter than 10 characters, such as an unsigned (`alg: none`) token. `ssh_private_key_block` also matches a PKCS #8 `PRIVATE KEY` block, a PGP `PRIVATE KEY BLOCK` and a key type with digits, and a block cut off before its END line is hidden to the end of the text. Text these now match is redacted where before it passed through, and the pattern names and severities are unchanged.

### The old sendToLLM overloads are removed

**BREAKING:** `SCOPBridge.sendToLLM(LLMConfiguration, JSONArray, JSONArray)`, `sendToLLM(LLMConfiguration, JSONArray, JSONArray, Object, ChatKnowledgeBinding, ChatRetrievalContext)` and both `sendToLLMForOwners` overloads are removed, a binary and source break. Each call becomes `sendToLLM(ChatTurn.builder(config, conversation)…build())`, setting only what the call used: `owners(owner)` and `bindings(binding)` for the six-argument call, `owners(list)` and `bindings(list)` for `sendToLLMForOwners`. A call that sent a conversation from `prepareConversationForDispatch(conversation, owners…)` becomes a turn of the stored `conversation` with `owners(owners…)`, leaving out `null` and repeated entries, which `prepareConversationForDispatch` skipped and `build()` refuses: the bridge prepares every turn itself, and a prepared copy in a turn without its owners goes out with their state removed. A `null` binding, or a `null` bindings list for `sendToLLMForOwners`, meant none. Leave `bindings` out instead, since `build()` refuses a `null` binding and `bindings` refuses a `null` list with `NullPointerException`. These behaviors change with them. The three-argument call sent the conversation as it was given. Every turn now sends a copy with any `@State` the bridge marked for an earlier owner removed, and a system message that held only that state left out, so state an earlier owner's turn left in a conversation no longer reaches a turn without owners. An empty owner list was refused by `sendToLLMForOwners`. A turn without owners is now a plain dispatch, and only a retrieval context with no owner is refused, when the turn is built. A `null` configuration came back as an error `LLMResponse`, and the three-argument call sent a `null` conversation to the provider with no messages. `ChatTurn.builder` now refuses a `null` configuration or conversation with `NullPointerException`. A `null`, `Collection` or repeated owner and a binding of no listed owner are refused by `build()` instead of by the send. The messages for a `Collection` owner and for bindings or a context given without owners name `ChatTurn.Builder.owners`. `prepareConversationForDispatch` is unchanged. Run `dev.tnsai.rewrite.UpgradeTnsAI_0_18_6` from `tnsai-rewrite` to mark (`/*~~>*/`) every call to the removed methods. It rewrites nothing.

The removed send overloads map onto the builder:

| Removed in 0.18.6 | Turn |
|---|---|
| `sendToLLM(config, conversation, tools)` | `ChatTurn.builder(config, conversation).tools(tools).build()`, plus `.owners(..)` when the conversation carries their state |
| `sendToLLM(config, conversation, tools, owner, binding, context)` | `.owners(owner).bindings(binding).context(context)` |
| `sendToLLMForOwners(config, conversation, tools, owners, bindings, context)` | `.owners(owners).bindings(bindings).context(context)` |
| `sendToLLMForOwners(..., requestSection)` | the same, plus `.requestSection(section)` |

A `null` binding or binding list used to mean none. Now leave `bindings`
out. A call that sent a conversation from
`prepareConversationForDispatch(conversation, owners...)` becomes a turn of
the stored `conversation` with `.owners(owners...)`, leaving out `null` and
repeated entries. The three-argument call sent the conversation as given.
A turn without owners now goes out with any state an earlier owner's turn
left in it removed. The `dev.tnsai.rewrite.UpgradeTnsAI_0_18_6` recipe marks calls to the
removed methods with `/*~~>*/` and rewrites nothing.

## 0.18.5

0.18.5 is additive: nothing to change.

## 0.18.4

Nothing fails to compile: these changes are behavior only.

### Failed action bodies are narrated

**BREAKING (behavior only):** a failed action body now tells the model why it failed. When the body itself throws, `ActionExecutor` raises an `ActionExecutionException` of category `INVOCATION` whose cause is the body's own exception, an `Error` included, and whose message is the root cause's simple type name and message on one line, for example `IOException: disk full`. In that message, Unicode format characters such as a zero-width space or a bidirectional override are removed, a run of whitespace or control characters becomes one space, e-mail addresses, phone numbers, payment cards, IBANs and national identifiers are replaced by the tokens the audit parameters use, and the message is cut to 200 characters and marked with `...`. Inside an agent context, the message also ends with the suffix every TnsAI exception's message carries, such as `[agent=…, role=…, session=…, tenant=…]` with whichever of the agent, role, session, tenant and group are set, added after the cut and not redacted. When the root cause is itself a TnsAI exception, its own suffix is part of the narrated text and is redacted and cut with it. Nothing else is removed from the message: a file path or any other detail it holds reaches the model and the record as the body wrote it. A cause chain holding a `java.lang.SecurityException` or a TnsAI `SecurityException` says only `Failed to execute action: <name>`, and a cyclic chain is read to its first repeat. A `VirtualMachineError` from the body, such as an `OutOfMemoryError` or a `StackOverflowError`, is not narrated: it propagates unchanged. From a reflective body, a `@Resilience` retry, and on the `Role` path a `@Fallback` method, can still handle it first, as they do any other body failure. From the body of an action added through `ActionConfig`, it propagates before either sees it. Otherwise the body of an action added through `ActionConfig` fails the same way as a reflective one. Before, it depended on the path. On plain-object dispatch, the path `SCOPBridge` takes for a target that is not a `Role`, a body failure became category `UNKNOWN` with the message `Failed to execute action: <name>` and the reflective `InvocationTargetException` as its cause. On the `Role` path without tnsai-quality it was category `INVOCATION` with the message `Action invocation failed: <message>`, and an `Error` raised a `ClassCastException` instead. An `Error` from the body of an action added through `ActionConfig` escaped as itself on both paths, except on a `Role` action with a tnsai-quality security timeout, where it became the `GENERAL` violation. On the `Role` path with tnsai-quality's `SecurityEnforcer` it was not an `ActionExecutionException` at all but a `SecurityException` of type `GENERAL` (see the next entry). A caller that reads the category, the cause or the message of this exception sees the new value. The liability record outcome and the SCOP action narration (`Action <name> failed: <message>`) change with them, and the new text reaches the model in the agent tool loop. An UNAUTHORIZED failure and a retrieval failure narrate as before. This applies to a failure of the action body, on the `Role` path and on plain-object dispatch alike, with or without tnsai-quality. A failure raised outside the body, such as an LLM, web-service or MCP executor error, is not narrated this way: it reaches the caller as the executor raised it, except that an `IllegalArgumentException` is a `PARAMETER` failure reading `Parameter error: <message>`, as it was before without tnsai-quality.

### SecurityEnforcer no longer masks failures

**BREAKING (behavior only):** tnsai-quality's `SecurityEnforcer.executeWithTimeout(ActionMetadata, Role, Callable)` no longer reports a failed task as a security violation. An unchecked exception or an `Error` from the task passes through unchanged and a checked one becomes a `TnsAIException` whose cause it is, as without tnsai-quality. Only the security timeout and an interrupt of the waiting thread are a `SecurityException` of type `GENERAL`. Before, every exception from the task, and with a security timeout an `Error` too, became `SecurityException(GENERAL, "Action execution failed")`. On the `Role` path the task is the whole dispatch, not only the body, so a failure raised outside the body, such as an LLM, web-service or MCP executor error, now reaches the caller as it does without tnsai-quality, where before tnsai-quality masked it as that `GENERAL` violation. Such a failure is not narrated: an exception's text reaches the caller, the SCOP action narration, the agent tool loop and the liability record with no masking, personal-data redaction or cut, and an `Error` propagates past the SCOP action narration. A checked one reaches the caller as `Action execution failed: <message>`. The SCOP action narration shows an `ActionExecutionException`'s own message and, for any other exception, the message of its cause when it has one. A caller that catches the `GENERAL` violation for a failed `Role` action sees the action's own failure instead. The parts that read the failure see what they see without tnsai-quality, not the `GENERAL` violation: a `@Fallback` method's failure parameter, `@Resilience` `retryOn` and `noRetryOn`, and the agent retry policy's `retryOn` and `abortOn` and the agent's error-recovery scenarios, which match by exception type. For a body failure, `@Fallback` and `@Resilience` see a `TnsAIException` whose cause is an `InvocationTargetException` holding the body's own exception, and the agent's retry policy and error-recovery scenarios see the `INVOCATION` `ActionExecutionException`.

### sendToLLM refuses a Collection owner

**BREAKING (behavior only):** the single-owner `SCOPBridge.sendToLLM(LLMConfiguration, JSONArray, JSONArray, Object, ChatKnowledgeBinding, ChatRetrievalContext)` refuses a `Collection` as its conversation owner with an `IllegalArgumentException` that names `sendToLLMForOwners`, with or without a retrieval context, before anything is dispatched. Before, a collection that declares no knowledge or `@State` of its own was taken as the one owner, so the knowledge and `@State` of the objects it held were never read: the turn was dispatched without them and without an error, and a supplied binding failed unless it was made for the collection itself. A class that implements `Collection` is refused too, even one that declares its own knowledge or `@State`. Several owners go to `sendToLLMForOwners`. An array or a `Map` passed as the owner is not refused.

The single-owner `sendToLLM` is removed in 0.18.6, whose `ChatTurn` `build()` refuses a `Collection` owner the same way.

### prepareConversationForDispatch refuses a Collection

**BREAKING (behavior only):** `SCOPBridge.prepareConversationForDispatch(JSONArray, Object...)` refuses a `Collection` among its conversation owners with an `IllegalArgumentException` that names the entry's index and type and asks for the owners it holds, one by one, before the conversation is copied. Before, a collection passed as an owner, as in `prepareConversationForDispatch(conversation, List.of(agent, role))`, where the list is one varargs entry, was ignored as an object without annotations, so the `@State` of the owners it held was left out of the request without an error. Pass the owners themselves, one argument each or as `owners.toArray()`. A class that implements `Collection` is refused too, even one that declares its own `@State`. An array or a `Map` entry is not refused.

## 0.18.3

Nothing fails to compile: the changes are behavior only, and one resource leaves the `tnsai-core` jar.

### Security features read every declaration

**BREAKING (behavior only):** `@Security` encryption, timeout and audit now read every `@Security` the access gate already resolves for the action: on the target's class and each of its superclasses and interfaces, on the action method, and on each method it overrides. This applies on the `Role` path and on plain-object dispatch alike. Before, they read the dispatched method's `@Security`, or, when it had none, only the one declared directly on the target's class. A declaration on a superclass, an interface or an interface method, an overridden method that does not repeat it, and the host a proxy subclass extends now take effect. No declaration replaces another: an `encryptParams`, `encryptResult` or `sensitive` set on any of them applies, the shortest positive `securityTimeout` applies, the highest audit level applies, and the mask fields are the union. A method's `@Security` therefore no longer hides its class's: a class audit level, for example, now applies to a method that declares only `encryptParams`. No declaration opts out of another: a nearer `false`, `0` or `NONE` does not undo an ancestor's setting, so a method's longer `securityTimeout`, for example, no longer lifts its class's shorter one. The access checks (`allowedCallers`, `allowedRoles`, `requiredPermissions`) already used this resolution and are unchanged. `@AuditLog` is resolved over the same methods: the action method's own, else the first found on a method it overrides, walking the target's class and each superclass up the chain, then the interfaces of those classes from the topmost superclass's down to the target class's own, each class's interfaces in declaration order, depth-first through the interfaces each extends, skipping an interface already walked. An override that does not repeat it therefore no longer drops it. One `@AuditLog` applies, and the action is audited once. On the `Role` path, interrupting the dispatching thread while it waits for a body under a `securityTimeout` now also interrupts the body.

### Plain-object dispatch applies Security features

**BREAKING (behavior only):** plain-object dispatch, `ActionExecutor.execute(ActionMetadata, Object, Map, Map)` on a target that is not a `Role`, now applies a declared `@Security` `encryptParams`, `encryptResult`, `securityTimeout` or `audit` level, and `@AuditLog`, at the points the `Role` path applies them. Before, these declarations were ignored on this path. With `SecurityEnforcer` from tnsai-quality:

- The body receives its `String`-valued parameters encrypted, and values of other types pass unchanged. Encryption runs after BeforeAction and the input guardrail, which see the caller's values, and before parameter validation, `@Contract`, knowledge retrieval and the body, which see the ciphertext, as does the dispatch's liability record. A `String` that the body's parameter type converts, such as `"42"` for an `int`, no longer converts.
- The caller receives a `String` result encrypted, after AfterAction, which sees the plaintext.
- The key is generated for each `SecurityEnforcer` instance, so only the enforcer that encrypted a value decrypts it, and nothing encrypted survives a restart.
- Audit entries are written after a dispatch that succeeds, and a body that fails writes none.
- A body with a `securityTimeout` runs on a `security-timeout` worker thread and is refused with `SecurityException` when it exceeds the timeout. The worker carries the agent and invocation contexts and the dispatch's knowledge lease but, as with a `@Resilience` timeout, no other state of the calling thread. It is interrupted, not stopped, when the timeout fires: a body that ignores the interrupt runs on to its end, holding its knowledge lease, after the dispatch has failed. The timeout runs inside any `@Resilience` retry, which retries the refusal: each attempt gets its own worker and its own timeout, and an attempt that ignored the interrupt can still be running when the next one starts. A failure of the body under a `securityTimeout` reaches the caller as it would without one, not wrapped in `SecurityException` as on the `Role` path.
- A handle that does not override the new methods refuses an action that declares any of these features with `SecurityException` `UNAUTHORIZED` in `requireHonored`, before `processParameters`. BeforeAction and the input guardrail have run by then, but the body has not, and the refusal is recorded as a failure. This includes the no-op handle used when tnsai-quality is absent: it already refused an action that declares `@Security` at the entry authorization check, and now also refuses one that declares only `@AuditLog`. On a `Role` the no-op handle still runs such an action and writes no audit entry.

### Plain-object dispatch needs a wired executor

**BREAKING (behavior only):** `ActionExecutor.execute(ActionMetadata, Object, Map, Map)` on a non-`Role` target now refuses with `AccountabilityWiringException` before the body runs when the executor has no principal or authority scope, as the `Role` path already did. On such an executor an action that declares an execution policy is refused earlier, at the entry authorization check, with `SecurityException`, and neither refusal runs the body or writes a record. A direct caller of this overload must wire the executor first. `SCOPBridge` dispatch is unaffected when the bridge is wired. See the [migration](#plain-object-dispatch-needs-a-wired-executor).

Wire the executor before a direct call of `ActionExecutor.execute(ActionMetadata, Object, Map, Map)` on a target that is not a `Role`: `executor.setPrincipal(principal)` with the `AgentPrincipal` the calls are attributed to, and `executor.setDefaultScope(scope)` with the `AuthorityScope` recorded on each dispatch, `AuthorityScope.unrestricted(...)` when not narrowing is the deliberate choice. An executor taken from an agent built with `AgentBuilder.principal(...)` and `.authorityScope(...)` already carries both. A `SCOPBridge` whose executor is wired needs no change.

### Resilience on plain objects

**BREAKING (behavior only):** a `@Resilience` declared on a non-`Role` host, on the method or the class, now takes effect on plain-object dispatch, as it does on a `Role`. A retry re-invokes the action body, so a body with side effects repeats them on each attempt. The executor's circuit breaker for that action name wraps the retries, so it counts a dispatch once, whatever its attempts. It judges the last 20 dispatches that reached it since it last closed, or all of them while fewer than 20 have, once at least 10 have: it opens when half of them failed or three quarters of them took longer than 10 seconds, even if they succeeded. After 15 seconds open it lets 5 trial dispatches through, and their outcomes decide whether it closes or opens again. While it is open, a dispatch fails with `CallNotPermittedException` without running the body and is recorded as a failure. A timeout wraps both and bounds how long the caller waits for all attempts together, not each attempt, and not the attempts themselves. With a timeout the body runs on a `tnsai-resilience-timeout-*` worker thread, which carries the agent and invocation contexts and the dispatch's knowledge lease but no other state of the calling thread: the executor's circular-dependency check does not see the calling dispatch, and a body confined to a UI thread is no longer on it. When the timeout fires the dispatch fails with `com.tnsai.exceptions.TimeoutException` and is recorded as a failure, and the running attempt is interrupted. The worker does not stop with the dispatch: a body that ignores the interrupt runs on to its end, and when the body stops on the interrupt with the `InterruptedException` of a blocking call, which clears the interrupt flag, the remaining retry attempts can still run after the dispatch has failed, and an attempt that fails with the interrupt still set ends the retries. These late attempts run under the dispatch's single approval, their outcomes are not recorded, and while an attempt runs the worker holds a lease on the dispatch's knowledge scope. The circuit breaker runs on the worker too, so it counts what the worker ends with, not the timeout: a dispatch that timed out is a failure in its record, but if its worker later succeeds within 10 seconds the breaker counts a success.

### SCOP continuations run through the executor

**BREAKING (behavior only):** when an action that `SCOPBridge.executeAction` dispatches returns a SCOP action, the bridge now runs that action's `execute()` through `ActionExecutor`'s plain-object dispatch, as a dispatch of its own named `execute`, whatever the host is, a TnsAI `Role` included. It still runs inside the authority of the action that returned it, after the same re-check of that action's authorization binding. The executor authorizes the continuation by its own declarations, such as an `@ExecutionPolicy` on its class. The host action's declarations do not extend to it. The executor's plain-object pipeline applies to it, and it gets its own liability record, without an outcome on success, after the record of the action that returned it. A continuation refused at the executor's entry authorization does not run and is not recorded, and `executeAction` returns a failed result denied for authorization, with the message `This action is not authorized.`. A continuation that throws, other than with an authorization denial, which stays a failed result denied for authorization, now makes `executeAction` return a failed `ActionExecutionResult` whose error message starts with `Action <name> failed:` and whose exception carries the failure. Before, the bridge returned success with the narration `Action <name> failed: …`. The action that returned the continuation has already run and stays recorded as a success. The post-narration is read inside the continuation's own authorization, as before, so a dispatch it makes cannot exceed what the continuation was authorized for. An undeclared continuation has no authorization of its own, and its post-narration runs under the authority of the action that returned it. A post-narration that cannot be read after a successful continuation falls back to the host's state or `Action <name> executed successfully.`, including one the bridge's module cannot access, which before failed the dispatch. An authorization denial raised while reading it still returns a failed result denied for authorization. The executor's circular-dependency check on the plain-object path now keys on the target instance instead of its class: a dispatch is refused only while the same action on the same instance is already on the thread's call stack, so continuations of one class, such as those scop-core's `Role.newAction` builds, nest across hosts, and so do two instances of one plain host class. A plain-object action without a `@Resilience` timeout or a `@Security` `securityTimeout` that dispatches itself again on a fresh instance of its class, without end, is no longer refused at the second level and now recurses until the thread's stack overflows (`StackOverflowError`). Under a `@Resilience` retry each level can retry that failure. Since the plain-object path applies `@Resilience` and, under `SecurityEnforcer`, a `securityTimeout`, both earlier in this release, an action with a `@Resilience` timeout, or with a `securityTimeout` under `SecurityEnforcer`, runs its body on a worker thread where the circular-dependency check does not see the calling dispatch, so that case is not refused either, where 0.18.2 refused it at the second level. The `Role` path still keys on the role's class.

### The portable role schema is removed

**BREAKING (resource):** `schema/portable-role.schema.json`, a JSON Schema that nothing in TnsAI read, is no longer in the `tnsai-core` jar, so a classpath lookup of it now returns `null`.

## 0.18.2

### TnsAI moves to its own Maven repository

**BREAKING:** TnsAI is published to its own Maven repository, `https://repo.tnsai.dev/maven2`, instead of Maven Central. 0.18.1 and every earlier version stay on Central and keep resolving from there, but nothing from 0.18.2 on is published to Central, so a build resolves 0.18.2 or later only once it adds the repository, as shown below. The coordinates stay `dev.tnsai`, and every file is signed and carries MD5, SHA-1, SHA-256 and SHA-512 checksums. A library that is itself published to Maven Central cannot depend on TnsAI 0.18.2 or later, because Central's publishing rules require a published artifact's dependencies to resolve from Central.

Add the repository to every build that resolves TnsAI 0.18.2 or later. Maven, in the `pom.xml` or in a profile of `settings.xml`:

```xml
<repositories>
    <repository>
        <id>tnsai</id>
        <url>https://repo.tnsai.dev/maven2</url>
        <releases><enabled>true</enabled></releases>
        <snapshots><enabled>false</enabled></snapshots>
    </repository>
</repositories>
```

Gradle (Kotlin DSL), in `settings.gradle.kts` under `dependencyResolutionManagement` or in `build.gradle.kts`:

```kotlin
repositories {
    mavenCentral()
    maven {
        url = uri("https://repo.tnsai.dev/maven2")
        content { includeGroup("dev.tnsai") }
    }
}
```

Keep Maven Central in the list: TnsAI's own dependencies still resolve from there. The Gradle `includeGroup` keeps every other artifact from being looked up on repo.tnsai.dev. A Maven mirror declared with `<mirrorOf>*</mirrorOf>`, such as a company proxy, must proxy `https://repo.tnsai.dev/maven2` too, or be narrowed to `<mirrorOf>*,!tnsai</mirrorOf>`.

Artifacts are GPG-signed with the same key as before, long key ID `D60CFDA975403254` (fingerprint on [Installation](../start/install.md)).

[Installation](../start/install.md#add-the-tnsai-repository) carries the same blocks for a new project.

### Run file Agents entries take the Config shape

**BREAKING:** an entry of the SCOP run file's `Agents` section takes the shape of an agent's `@Config` file, `{"llm": {...}, "knowledge": {...}, "roles": {...}}`, each field optional. The flat entry, `"QuestionerAgent": {"temperature": 0.3}`, now fails naming the agent type and the setting. Move its LLM fields under `llm`, as shown below. `AgentSettings.fromJson` decodes only `{"llm": {...}}`: a flat LLM setting fails, and so do `knowledge` and `roles`, since they configure the agent type. The top-level `LLM` section is unchanged. `AgentConfiguration.parse` checks an `Agents` entry and reads its `llm`, and a host passes the whole section to the new `ClassConfig.useBaseDirectory(ClassConfig.RunFile)`, which checks the section as an agent's file is checked and applies its `knowledge` and `roles` to the agent type it names.

In a SCOP run file, move each `Agents` entry's LLM fields under `llm`:

```json
{"Agents": {"QuestionerAgent": {"temperature": 0.3}}}
```

becomes

```json
{"Agents": {"QuestionerAgent": {"llm": {"temperature": 0.3}}}}
```

An entry passed to `AgentSettings.fromJson` moves its LLM fields under `llm` the same way: `{"temperature": 0.2}` becomes `{"llm": {"temperature": 0.2}}`.

### The run file wins over Config files

**BREAKING:** the run file's agent section, once a host passes it to `ClassConfig.useBaseDirectory(ClassConfig.RunFile)`, wins over the class's `@Config` files. For the LLM, lowest first, the chain is: the annotations, the shared `LLM` section, the agent's file, its `roles` section, the role's own file, the run file's agent section, that section's `roles`. The run file's type `llm` applies once, at the run level: a host registers `AgentConfiguration.shared()` with `SCOPBridge.llmConfigurationSource`, which sits below the class files, not `resolve(...).llm()`. Until 0.18.1 a class `@Config` file won over the run file's LLM fields, and a host that overlaid its own instance, file or explicit run layers on `resolve(...).llm()` had them win over the type's entry. A source registered with `llmConfigurationSource` sits below the class files and the run file's type section, so such layers no longer win over the type's `llm`, and a setting that must win goes in the run file's section. The section is read once per run, when the host starts it, so an edit applies at the next run. A role class takes one run file `roles` section per run, as it takes one `roles` section from the agent files.

A value that a class `@Config` file used to override now overrides that file. Drop it from the run file to keep the class's value.

### A host that reads the run file

A host that reads the run file:

1. Register the shared section on the bridge, `bridge.llmConfigurationSource(name -> Optional.of(configuration.shared()))` with `bridge` the `SCOPBridge` instance, not `resolve(...).llm()`.
2. Start each run with `ClassConfig.useBaseDirectory(ClassConfig.RunFile.of(base, runFile, agents).agentTypes(registeredTypes))`, where `agents` is the `Agents` section's text, `sections.has("Agents") ? sections.getJSONObject("Agents").toString() : null`, and `RunFile.of` takes `null` for a run file without the section.
3. Before saving edited sections, call `AgentConfiguration.validate(sections, registeredTypes)` with them, and refuse the edit when it throws.

### useBaseDirectory(null) needs a cast

**BREAKING (source only):** `ClassConfig.useBaseDirectory(null)` no longer compiles, since `useBaseDirectory` now takes a `Path` or a `ClassConfig.RunFile`, so write `ClassConfig.useBaseDirectory((Path) null)`. Compiled code is unaffected.

## 0.18.1

Nothing fails to compile: 0.18.1 is additive, and 0.18.0 code and configuration keep working unchanged. Its patched dependencies reach your build only if it lets them.

- **Jackson:** the TnsAI BOM now imports the patched `jackson-bom` lines (2.22.3 and 3.2.3). Maven applies the first import that manages an artifact, so you get them through the BOM only if you import `tnsai-bom` before any `jackson-bom` or Spring Boot BOM. Otherwise, pin Jackson yourself. The one exception is `tnsai-payments`, which now declares Jackson 3 `jackson-databind` directly, so it resolves 3.2.3 even without the BOM.
- **PostgreSQL driver:** the BOM manages `org.postgresql:postgresql` 42.7.14. TnsAI uses it only in tests, but the same import-order rule decides whether your application resolves 42.7.14.
- **Logback:** `logback-classic` moves to 1.6.5, which fixes a path traversal through an MDC value in a `SiftingAppender`. TnsAI configures no `SiftingAppender`. If your application does, make sure 1.6.5 or later is what resolves.
- **New, opt-in:** `@RoleSpec(knowledgePolicy = @KnowledgePolicy(source = "x", presence = KnowledgePolicy.Presence.WHEN_CONFIGURED))` (or the same member on `@AgentSpec`) lets a `FILE` source load as no content until a configuration points it at a file. `SCOPBridge.requireReady(...)` checks before a run that every configured source of that kind is ready. Nothing changes unless you declare them.

## 0.18.0

**Highlights:** Knowledge is declared with one annotation, `@Knowledge`, inside `@AgentSpec`, `@RoleSpec` or `@ActionSpec`. Each entry says how its documents reach the model: `use = SEARCH` (the default), `DIRECT` (every document whole, inside a token budget) or `SEQUENTIAL` (one unit per call). An action sees only the knowledge it names.

### Run the upgrade recipe first

Run `dev.tnsai.rewrite.UpgradeTnsAI_0_18_0` from `tnsai-rewrite` before you bump the version. It marks (`/*~~>*/`) every removed knowledge annotation, every use of the four moved enums and every removed config method, and rewrites nothing. Its description says what each site needs. Remove each marker by hand once the site is migrated.

Your project is still on 0.17.x at that point, so name the 0.18.0 recipe artifact explicitly:

```bash
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=dev.tnsai:tnsai-rewrite:0.18.0 \
  -Drewrite.activeRecipes=dev.tnsai.rewrite.UpgradeTnsAI_0_18_0
```

Rename the four moved enums by hand, with the same constants: `KnowledgeSource.KnowledgeType` becomes `Knowledge.Type`, and `Retrieval.Strategy`, `Retrieval.QueryExpansion` and `Retrieval.FallbackAction` become `Search.Strategy`, `Search.QueryExpansion` and `Search.FallbackAction`. A recipe would import `Type` by its simple name, which collides with `java.lang.reflect.Type`.

### One knowledge annotation

**BREAKING:** `@KnowledgeSource`, `@KnowledgeSources`, `@Retrieval`, `@Sequential` and `@ChatKnowledge` are deleted, with `AgentSpec.retrieval()`, `Retrieval.directContextTokens()` and `AgentSpecExtractor.getChatKnowledge()`. `AgentSpec.knowledge()` now takes `@Knowledge[]`, `RoleSpec.knowledge()` and `ActionSpec.knowledge()` are new, and the four nested enums move to `Knowledge.Type` and `Search.*`. A name declared twice on one class, a name an action names twice, and action knowledge on an action that is not LLM or LOCAL all fail. `KnowledgeSourceConfig` gains a 13th component, `select`, after `chunkContextModel`: the 12-argument constructor remains and loads whole documents, but a record pattern over the 12 components no longer compiles, and `equals`, `hashCode` and `toString` include `select`. `UpgradeTnsAI_0_18_0` does not mark record patterns.

Move each `@KnowledgeSource` into `@RoleSpec(knowledge = @Knowledge(...))` on a role or `@AgentSpec(knowledge = @Knowledge(...))` on an agent, and the attributes keep their names. `@Retrieval(sources = {"a", "b"}, ...)` on an action becomes `@ActionSpec(knowledge = {@Knowledge(name = "a", search = @Search(...)), @Knowledge(name = "b")})`, and the `@Search` attributes keep the `@Retrieval` names. `directContextTokens = n` becomes `use = DIRECT, maxTokens = n` on that knowledge. `@Sequential(source = "x")` becomes `@ActionSpec(knowledge = @Knowledge(name = "x", use = Knowledge.Use.SEQUENTIAL))`. `@ChatKnowledge(source = "x", topK = n)` becomes `@AgentSpec(knowledge = @Knowledge(name = "x", search = @Search(topK = n)))`.

| 0.17.x | 0.18.0 |
|---|---|
| `@KnowledgeSource(...)` on a role or agent | `@RoleSpec(knowledge = @Knowledge(...))` or `@AgentSpec(knowledge = @Knowledge(...))`, and the attributes keep their names |
| `@Retrieval(sources = {"a", "b"}, ...)` on an action | `@ActionSpec(knowledge = {@Knowledge(name = "a", search = @Search(...)), @Knowledge(name = "b")})`, and the `@Search` attributes keep the `@Retrieval` names |
| class-level `@Retrieval(...)` or `@AgentSpec(retrieval = ...)` | `search = @Search(...)` on the declared knowledge, named by each action that used it |
| `@KnowledgeSource` on an action method | declare it on the role or agent, and name it from the action's `@ActionSpec(knowledge = ...)` |
| `directContextTokens = n` | `use = Knowledge.Use.DIRECT, maxTokens = n` on that knowledge |
| `@Sequential(source = "x")` | `@ActionSpec(knowledge = @Knowledge(name = "x", use = Knowledge.Use.SEQUENTIAL))` |
| `@ChatKnowledge(source = "x", topK = n)` | `@AgentSpec(knowledge = @Knowledge(name = "x", search = @Search(topK = n)))` |

`KnowledgeSourceConfig.from(KnowledgeSource)` becomes `KnowledgeSourceConfig.from(Knowledge)`. A record pattern over `KnowledgeSourceConfig` takes a 13th component, `select`. The recipe does not mark record patterns, so find them by hand. `SequentialConfig.from(Sequential)` becomes `new SequentialConfig(name)`. `RetrievalConfig.from(Retrieval)` has no replacement, so build the config with `RetrievalConfig.builder()`. `RetrievalConfig.directContextTokens()`, its builder method and the constructor that took it have no replacement: a builder `RetrievalConfig` only searches, and DIRECT is declared on `@Knowledge`.

### An action sees only the knowledge it names

**BREAKING:** an action sees only the knowledge it names in `@ActionSpec(knowledge = ...)`, resolved against the declaring role or agent. Class-level `@Retrieval` and `@AgentSpec(retrieval = ...)` used to reach every LLM action of the class. Nothing replaces that, so an action that names no knowledge retrieves nothing.

Each LLM action that used class-level `@Retrieval` or `@AgentSpec(retrieval = ...)` must now name its knowledge in `@ActionSpec(knowledge = ...)`, or, on a builder agent, in `actionKnowledge(action, names...)`.

### Chat grounds without an opt-in

On `Agent`, chat knowledge is on by default, with no opt-in: a chat turn sees every enabled `@AgentSpec` knowledge entry with `use = SEARCH` or `DIRECT`, and `enabled = false` is the only opt-out. On `SCOPBridge`, SEARCH chat stays host opt-in: it reaches a chat turn only through a binding the host attaches.

**BREAKING:** a chat turn sees the enabled `@AgentSpec` knowledge with `use = SEARCH` or `DIRECT`, with no opt-in. SEQUENTIAL entries are ignored by chat, GRAPH entries stay out of it, and `@RoleSpec` knowledge reaches only the actions that name it, except on a class that also carries `@AgentSpec`, whose chat sees it too. One validator checks every runtime `ChatKnowledgeBinding`, on `Agent` and on `SCOPBridge` alike: the binding must belong to the agent, name one of its enabled SEARCH entries and carry the same `topK` as that entry's merged `@Search`. Each refusal names the mismatched field and both values. `SCOPBridge` has no agent startup, so a bridge chat call without a binding gives the conversation owner's DIRECT entries only, and SEARCH reaches a bridge chat through a binding the host attaches. An agent whose enabled `@AgentSpec` knowledge includes `use = SEARCH` builds its chat retrieval engine at startup, unless a runtime binding was attached first, and fails there without an installed `RetrievalEngineProvider` such as `tnsai-rag`. Before, only an agent that opted in with `@ChatKnowledge` did.

### DIRECT fails instead of falling back

**BREAKING:** DIRECT knowledge is loaded at agent startup and checked against its budget there and on every call. Over the budget, or not loadable as whole documents, it fails. It never falls back to search, as `directContextTokens` did. The engine-side automatic DIRECT selection is removed with it.

### Run file knowledge sources are removed

**BREAKING:** an `Agents.<Type>` run-file entry with `knowledgeSources` fails with `knowledgeSources moved to the class's knowledge declaration`. `AgentConfiguration.parseSourceOverrides(JSONObject)`, `AgentConfiguration.SourceOverride`, `AgentConfiguration.KnowledgeSourceSelection` and `AgentSettings.knowledgeSources()` are removed, as are `KnowledgeSourceOverrides`, the context keys `RetrievalSpi.RESOLVED_SOURCE_CONFIGS_KEY` and `RetrievalSpi.SOURCE_OVERRIDE_ROOTS_KEY`, `SCOPBridge.sourceOverrideRoots(Collection)` and the 6-argument `SCOPBridge.executeAction(Object, String, Map, Map, List, InvocationContext)`, and `executeAction(Object, String, Map, Map<String, Path>, InvocationContext)` replaces it and still rejects a `null` invocation. Resolved runtime paths still locate declared sources.

Move each `Agents.<Type>.knowledgeSources` row of a run file onto the class's knowledge declaration. A 5-argument `SCOPBridge.executeAction` call that passed `null` as the removed source-override list now binds to the `InvocationContext` overload and returns an authorization error result instead of executing. Drop the `null` argument to reach the 4-argument overload.

### DeclarativeAgentBuilder is removed

**BREAKING:** `DeclarativeAgentBuilder` is removed. Its constructor was package-private, so code outside `com.tnsai.agents` reached it only through `fromYaml(String)` and `fromSpec(Map<String, Object>)`, and neither could return an agent: neither set a model, so `build()` threw a `NullPointerException`. `AgentBuilder` builds agents.

Replace each `DeclarativeAgentBuilder.fromYaml(...)` or `fromSpec(...)` call with `AgentBuilder`. `UpgradeTnsAI_0_18_0` marks every use of the class.

### A FILE entry with no path fails

**BREAKING:** a FILE knowledge entry with no path, which nothing locates, fails as `CONFIGURATION` whatever its `onFailure`. On `Agent` this happens at startup, for every action that names the entry, whether it uses SEARCH, SEQUENTIAL or DIRECT. On `SCOPBridge` it happens at dispatch, when the run's path map gives the entry no path, and the action does not run. Before, a SEARCH action read such an entry as an empty source and succeeded with no context, a SEQUENTIAL action ran with no unit, and a DIRECT action failed as `SOURCE_LOADING`. A FILE path that is set but missing passes startup and is a source that does not load, as the entry above describes.

Give each enabled FILE knowledge entry a path, in `@Knowledge`, the `@Config` file or the run's path map, or set `enabled = false` on it.

### A denied source fails as a RetrievalException

**BREAKING:** a knowledge source whose load a `SecurityException` of either type denies reaches the caller as a `RetrievalException` with reason `CONFIGURATION`, the text `Knowledge source access was denied` and the `SecurityException` in its cause chain. On 0.17.5 the `SecurityException` itself was thrown, so a `catch (SecurityException e)` no longer catches it. A loader that wrapped the denial in its own `RetrievalException` kept that exception's reason on 0.17.5, so a `SOURCE_LOADING` one was skipped by a SEARCH action with `onFailure = CONTINUE`. It is now `CONFIGURATION` and fails every action that names the source, whatever its `onFailure`. On `SCOPBridge` the failed action's message is `Action <name> failed: Knowledge source access was denied`, with no source, role or cause detail.

Where code caught the `SecurityException` of a denied knowledge source, catch `RetrievalException` and look for the `SecurityException` in its cause chain. The reason alone does not tell a denial apart, since invalid configuration is `CONFIGURATION` too. Where a loader wraps a denial in its own `RetrievalException`, its reason is no longer kept: an action that skipped such a `SOURCE_LOADING` failure under `onFailure = CONTINUE` now fails, so grant the access or stop naming the source.

### MEMORY is chat knowledge the application supplies

**BREAKING:** `Knowledge.Type.MEMORY` is chat knowledge the application supplies at runtime through a `ChatKnowledgeBinding`. Before, it was a loader extension point that no bundled loader served. Initialization loads nothing for it and needs no `RetrievalEngineProvider` on its account: the agent starts ungrounded, and `setChatKnowledgeBinding` grounds it under the binding rule of the chat-scope entry above, with the chat default `topK` of 5 when the entry sets none. A MEMORY entry is declared on `@AgentSpec` only and uses SEARCH. It takes no `maxTokens`, no location or loader member and no `@Search` member but `topK`. A chat scope holds at most one MEMORY entry and no other SEARCH entry beside it, counting the `@RoleSpec` knowledge of a class that carries both annotations, while DIRECT entries may sit beside it, and an action that names a MEMORY entry fails. Each rule fails at agent init, naming the entry and, for `@Search`, the member. `SCOPBridge` has no agent init: there the rules fire from a class's first action dispatch when a `RetrievalSpi` such as `tnsai-rag` is installed, and at every `sendToLLM` call that carries a `ChatRetrievalContext`, while a `sendToLLM` call with a `null` context checks only the binding's owner. A `SourceLoader` whose `handles()` is MEMORY or GRAPH fails discovery, and `KnowledgePreflight.requireLoaded` refuses a MEMORY name. `AgentBuilder.liveChatKnowledge(String)`, public since 0.16.0, is removed: a builder agent gives `knowledge(KnowledgeEntry.search(KnowledgeSourceConfig.memory(name)))`, and the rules above run on its layered knowledge, builder entries included. In `tnsai-server`, `RagService.attachChatTo(Agent, int)` becomes `attachChatTo(Agent)`, which binds `RagService.CHAT_TOP_K`, 5, and every agent `AgentFactory` builds declares `RagService.CHAT_SOURCE` as its MEMORY chat knowledge.

Replace `AgentBuilder.liveChatKnowledge(name)` with `knowledge(KnowledgeEntry.search(KnowledgeSourceConfig.memory(name)))`, and the runtime binding must then carry `topK` 5, the chat default. Replace `RagService.attachChatTo(agent, topK)` with `attachChatTo(agent)`, which binds `RagService.CHAT_TOP_K`, 5. Move a MEMORY `@Knowledge` off `@RoleSpec` and out of `@ActionSpec(knowledge = ...)` onto the agent's `@AgentSpec`, keep at most one per agent with no other SEARCH entry beside it, drop its location and loader members, `maxTokens` and every `@Search` member but `topK`, and remove a `SourceLoader` that handles MEMORY. `UpgradeTnsAI_0_18_0` marks the two removed methods. Find the declarations and loaders by hand.

### Builder knowledge is given as entries

**BREAKING:** builder knowledge is given as the same `KnowledgeEntry` model the annotations produce. `AgentBuilder.addKnowledgeSource(KnowledgeSourceConfig)`, `knowledgeSources(List)`, `retrieval(RetrievalConfig)` and `chatKnowledge(String)` are removed, with the `Agent` hooks `getKnowledgeSources()`, `getRetrieval()` and `getKnowledgeSourcesTemplate()` and with `AgentToolExecutor.AgentContext.getAgentRetrievalConfig()`. Before, a builder agent's sources and its one `RetrievalConfig` sat beside the declared knowledge: chat grounded on the one source `chatKnowledge` named, and an action that named no knowledge, or only SEARCH knowledge, searched every builder source with that config. Now `AgentBuilder.knowledge(KnowledgeEntry...)` layers entries over the `@AgentSpec` knowledge by name: an entry with a declared name replaces that entry in place, so one replacing a `@RoleSpec` entry stays a role entry, and an entry with a new name joins the agent's chat scope. The knowledge rules run on the layered list, so a chat scope still holds one MEMORY entry and no other SEARCH entry beside it. A chat turn searches every enabled SEARCH entry of the chat scope, and an action uses only the knowledge it names, and `AgentBuilder.actionKnowledge(action, names...)` replaces the list an action's `@ActionSpec` names, and an action the agent does not have fails at init with the list of its actions. A subclass gives the same through the new `protected` hooks `getKnowledge()` and `getActionKnowledge()`. `Agent.getRetrievalConfig()` stays and returns the chat scope's merged search configuration.

Give builder knowledge as entries. `addKnowledgeSource(source)` and `knowledgeSources(List.of(source))` become `knowledge(KnowledgeEntry.search(source))`, or a `new KnowledgeEntry(source, use, maxTokens, search)` for DIRECT or for a source with its own search settings. `retrieval(RetrievalConfig.builder().topK(5).build())` becomes `SearchSettings.builder().topK(5).build()` on the SEARCH entries it applied to. The settings of a scope's SEARCH entries merge member by member, as `@Search` members do. Drop `chatKnowledge(name)`: a chat turn searches every enabled SEARCH entry of the chat scope. An action that searched the builder's sources without naming them now names them with `actionKnowledge(action, names...)`. A subclass override of `getKnowledgeSources()` or `getRetrieval()` becomes an override of `getKnowledge()`, and one of `getActionKnowledge()` for its actions. An `AgentToolExecutor.AgentContext` implementation drops `getAgentRetrievalConfig()`. `UpgradeTnsAI_0_18_0` marks each removed call and each override of a removed hook.

## Before 0.18

### Bridge retries follow the error type (0.17.5)

**BREAKING:** this change is silent: nothing fails to compile, but retries
behave differently. Before 0.17.5, `BridgeLLMClient.chat()` reported every failure as
`ErrorType.UNKNOWN`, which is retryable. It now reports the classified type,
so a `RetryPolicy` keyed on error types behaves differently on the HTTP
fallback path:

- A policy keyed on `SERVER_ERROR` or `MODEL_OVERLOADED` now retries 5xx
  and 429 failures.
- 401/403 and other 4xx failures are no longer retryable.
- An empty 2xx body, or a 2xx body carrying an `error` object, stays
  `UNKNOWN` and retryable, as before.

The bridge itself still sends each request once, and any retry comes from the
caller's `RetryPolicy`. If you relied on 4xx failures being retried, review
your policy before upgrading. See the
[0.17.5 changelog entry](../help/changelog.md#0-17-5-bridge-retry-types).
