# RAG

Retrieval-augmented generation (RAG) grounds chat turns or action calls in
knowledge the application owns, before the LLM runs. You declare each source
once, by name, and choose per source how it reaches the model. Corpus loading,
retrieval policy and invocation wiring stay separate, so the entry point and
the strategy can be chosen independently.

| To | Read |
|---|---|
| Declare knowledge and decide who sees it | This page |
| Build, fill and query a `KnowledgeBase` in code | [KnowledgeBase API](knowledge-base.md) |
| Control how files, PDFs and Office documents are read | [File formats](file-formats.md) |
| Pick a retrieval algorithm | [Retrieval strategies](retrieval-strategies.md) |
| Install embedding, reranking and other providers | [Install retrieval providers](providers.md) |
| Configure embeddings and their prefixes | [Embeddings](embedding-prefixes.md) |
| Keep vectors in Qdrant or pgvector | [Remote vector stores](remote-vector-stores.md) |
| Retrieve from a knowledge graph | [Graph stores](graph-stores.md) |
| Hand an action a source one unit at a time | [Ordered consumption](sequential-reading.md) |
| Search the files an agent writes during a run | [Agent run files](run-files.md) |
| Retrieve without vectors, by descending a tree | [Vectorless reasoning](vectorless.md) |
| Split papers by their headings | [Smart segmentation](segmentation.md) |
| Re-embed only the chunks that changed | [Selective re-embed](selective-reembed.md) |
| Index and search through `tnsai-server` | [Pipeline](pipeline.md) |
| See the configuration RAG resolved | [Diagnostics](diagnostics.md) |

RAG spans three modules:

| Layer | Module | Provides |
|---|---|---|
| Primitives | `tnsai-core` (`com.tnsai.knowledge`, `com.tnsai.memory.advanced`, `com.tnsai.rag`) | `KnowledgeBase`, `Document`, `EmbeddingFunction`, `BM25Index`, `VectorMemoryStore`, `HybridMemoryRetriever`, `KnowledgeEntry`, `KnowledgeSourceConfig` |
| Strategies | `tnsai-rag` (`com.tnsai.intelligence.rag`) | `RAGPipeline`, `RAGStrategy`, `RetrievedDocument`, `RAGContext` |
| Service and HTTP | `tnsai-server` (`com.tnsai.server.rag`) | `RagService`, `FileIndexer`, `CodeChunker`, `HybridRetriever`, retrieval streams |

Embedded agents typically use the core primitives plus the strategies.
`tnsai-intelligence` does not depend on `tnsai-rag`, so the two are
independent additions. `tnsai-server` adds HTTP endpoints, indexing and
stream-based retrieval.

## Declarative (annotations)

Knowledge is declared in one place and used by name. A `@Knowledge` entry
in the `knowledge` member of `@AgentSpec` or `@RoleSpec` declares the
source: where it comes from, how it is sliced, and how it reaches the model.
Who sees it follows from where it is declared:

- **Chat** sees every enabled `SEARCH` or `DIRECT` `@AgentSpec` entry.
  Builder entries that the annotation does not declare join the chat scope
  too. On a class that carries both `@RoleSpec` and `@AgentSpec`, its
  `@RoleSpec` entries join the chat scope as well. A builder entry with the
  same name replaces one in place.
  A `SEQUENTIAL` entry or a `GRAPH`-type entry reaches only the actions that
  name it.
- **An action** sees only the entries its `@ActionSpec(knowledge = ...)`
  names. The action's entry carries the name and may set only `use`,
  `maxTokens` and `search`. Setting a source member such as `path` there
  fails at startup.

Action knowledge is valid on `LLM` actions, where it is spliced into the
prompt, and on `LOCAL` actions, where it is pulled before the body runs.
Any other action type that names knowledge fails at startup.

Names are unique within one list: two entries with the same name in one
`@AgentSpec` or one `@RoleSpec` fail at startup, and so does a class that
carries both annotations and declares the same name in each. An `@AgentSpec`
entry whose name matches an entry of one of the agent's roles replaces that
entry for the role's actions.

Each entry reaches the model by its own `use`:

| `use` | What the model receives |
|---|---|
| `SEARCH` (the default) | Ranked results per request, configured by `@Search`. |
| `DIRECT` | The whole source, within a shared token budget (`maxTokens`, or a quarter of the model's input window). A source that does not fit fails and never falls back to `SEARCH`. |
| `SEQUENTIAL` | The source's next unit on each call ([Ordered consumption](sequential-reading.md)). |

A non-`Role` SCOP object that carries `@RoleSpec(knowledge = ...)` runs the
same action path through [`SCOPBridge.executeAction`](../connect/scop.md).
SCOP **chat** grounding is
[`SCOPBridge.sendToLLM(ChatTurn)`](../connect/scop.md#owner-aware-chat-grounding)
with owners and a retrieval context. It reads each owner's `@AgentSpec`
knowledge, or its `@RoleSpec` knowledge when the class has no `@AgentSpec`.
A turn without owners or without a context stays ungrounded.
Do not use `sendToLLM` as a stand-in for action knowledge.

Named `queryParam` selection requires consumer classes compiled with Java's
`-parameters` flag. In Maven, set `<maven.compiler.parameters>true</maven.compiler.parameters>`.
With `javac`, pass `-parameters`. A `queryParam` declared on a Role or Agent
source applies to every action that names the source, so each of those
actions must expose a parameter of that name. If that contract differs by
action, set `search = @Search(queryParam = ...)` on the action's own
`@Knowledge` entry instead. A missing name is a configuration error before
`onFailure` applies.

<!-- java-contract: src/main/java/com/example/tnsai/docs/RagTopicAnnotationsExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.actions.ActionExecutor;
import com.tnsai.agents.Agent;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.AgentSpec;
import com.tnsai.annotations.Knowledge;
import com.tnsai.annotations.RoleSpec;
import com.tnsai.annotations.Search;
import com.tnsai.enums.ActionType;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;

import java.util.List;

public final class RagTopicAnnotationsExample {
    private RagTopicAnnotationsExample() {}

    @AgentSpec(knowledge = @Knowledge(name = "faq", path = "knowledge/faq"))
    public static final class SupportAgent extends Agent {
        public SupportAgent(AgentPrincipal principal, AuthorityScope scope) {
            super("support-agent", null, null, wired(principal, scope));
        }

        private static ActionExecutor wired(AgentPrincipal principal, AuthorityScope scope) {
            ActionExecutor executor = new ActionExecutor();
            executor.setPrincipal(principal);
            executor.setDefaultScope(scope);
            return executor;
        }

        @Override
        protected LLMClient getLLM() {
            return new AnthropicClient("claude-sonnet-4-20250514");
        }

        @Override
        protected List<Role> getRoles() {
            return List.of(Role.create(SupportRole.class));
        }
    }

    @RoleSpec(knowledge = @Knowledge(
        name = "product-docs",
        path = "knowledge/products",
        search = @Search(
            strategy = Search.Strategy.HYBRID,
            queryParam = "question",
            topK = 5)))
    public static final class SupportRole extends Role {
        @Override
        public RoleIdentity getIdentity() {
            return new RoleIdentity("support", "Product support", "support");
        }

        @ActionSpec(
            type = ActionType.LLM,
            description = "Answer from product documentation",
            knowledge = @Knowledge(name = "product-docs"))
        public String answer(String question) {
            return question;
        }
    }
}
```

### Configuration files and the run file

An agent or role class can name a configuration file with
`@AgentSpec(config = @Config(path = ...))` or `@RoleSpec(config = ...)`. The
file's `knowledge` object is keyed by knowledge name and layers over the
annotation entries:

- A name a lower layer declares is overridden member by member, `search`
  included. What the file leaves out keeps the value of the layer below. An
  entry that changes `use` keeps a `maxTokens` or `search` from below only
  when the new `use` applies to it.
- A name no lower layer declares is a full declaration. It follows the
  entries below, in name order.
- Every entry is validated as an annotation entry is.

A SCOP run file's `Agents` section is one more layer, above the
class files. A host passes it with
`ClassConfig.useBaseDirectory(ClassConfig.RunFile)`. Each `Agents.<Type>`
entry, keyed by the agent type's simple or binary name, takes the shape of an
agent's file: `llm`, `knowledge` and `roles`. Below, the agent declares `faq`
and its role `AnswererRole` declares `manual`. The run file moves `faq` and
gives `manual` a direct budget:

```json
{
  "Agents": {
    "AnswererAgent": {
      "knowledge": {"faq": {"path": "knowledge/faq-v2"}},
      "roles": {
        "AnswererRole": {
          "knowledge": {"manual": {"use": "DIRECT", "maxTokens": 4000}}
        }
      }
    }
  }
}
```

The layers, lowest first:

| Knowledge of | Layers |
|---|---|
| An agent | `@AgentSpec`, the agent's own file, `Agents.<Type>` |
| A role | `@RoleSpec`, the `roles` section of its agent's file, the role's own file, `Agents.<Type>.roles.<name>` |

The run file's section is read once, when the host starts the run, so an
edit applies at the next run. A relative FILE `path` it gives resolves
against the run file's base directory. Knowledge given to a type with no
`@AgentSpec`, neither its own nor one it inherits, fails, and so does a key
that gives a role class's binary name: a role takes its run settings under
`Agents.<Type>.roles.<name>`. A section keyed by a superclass configures
only that class, not its subclasses.

### Where a relative FILE path resolves

Every relative FILE path resolves against the base directory
`ClassConfig.useBaseDirectory` set, when one is set, and against the working
directory otherwise: one written in an annotation, one a `@Config` entry
keeps without giving `path`, one built with `KnowledgeSourceConfig.file`,
and a location passed to `RoleRagBinding.forRole`. Absolute paths and URLs
are kept as written. The path resolves when the source is read, so a base
directory set later applies from the next run. The loader, the readiness
check, the startup presence check and `tnsai-server`'s `FileIndexer` all
resolve it the same way, though `FileIndexer.index(Path, …)` still resolves its
`Path` against the working directory.

`KnowledgePaths.resolve(path)` resolves a path as those readers do, and
`KnowledgePaths.resolve(path, base)` does it against a given base (the
working directory when `base` is null). Both return an absolute, normalized
path. `ClassConfig.baseDirectory()` returns the base directory in force, or
empty when there is none. Under a base directory, a `@Config` entry that
restates the annotation's relative path, or names its absolute path under
the base, names the same source, so a `WHEN_CONFIGURED` source named that
way is not configured and is skipped with a WARN naming the resolved path.

### Inherited `@AgentSpec`

`@AgentSpec` is `@Inherited`, as `@RoleSpec` is. An
agent class without its own `@AgentSpec` takes its nearest superclass's
whole, knowledge included, and each concrete agent binds and indexes that
knowledge on its own. Fields are not merged: a class that declares its own
`@AgentSpec` replaces the superclass's entirely, and one on an interface is
not inherited. An unannotated subclass's `getKnowledge()` entries layer by
name over the inherited knowledge. See [Agent fundamentals](../agents/create.md#inherited-agentspec)
for everything else a subclass takes.

### Refresh a corpus

A Role's binding (its documents, indexes and retrieval-result cache) is built once per Role class per JVM and kept for the life of the process. Nothing re-reads your sources on its own, and `@Search.cacheTTL` doesn't help: an expired cache entry is recomputed against the same index.

When any of the Role's sources may have changed, invalidate the whole binding with `com.tnsai.intelligence.rag.binding.RoleRagBinding`. The next dispatch re-ingests every source, but reuses the vectors of chunks whose content and embedding provider are unchanged:

```java
RoleRagBinding.invalidate(SupportRole.class);
```

When one enabled source changed, pass its exact, case-sensitive name:

```java
RoleRagBinding.invalidate(SupportRole.class, "product-docs");
```

That rebuilds the bindings that contain `product-docs` and resets only that source's sequential cursor; other bindings and cursors are kept. A null, blank, disabled or unknown name fails. A retrieval already running finishes on the snapshot it started with.

## Programmatic (builder)

The builder takes the same entries as `KnowledgeEntry` values. The
application composition root also supplies the required principal and
authority scope, and a liability sink if you want records:

<!-- java-contract: src/main/java/com/example/tnsai/docs/RagTopicBuilderExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.annotations.Knowledge;
import com.tnsai.annotations.Search;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.rag.KnowledgeEntry;
import com.tnsai.rag.KnowledgeSourceConfig;
import com.tnsai.rag.SearchSettings;
import com.tnsai.roles.Role;

import java.util.Optional;
import java.util.OptionalInt;

public final class RagTopicBuilderExample {
    private RagTopicBuilderExample() {}

    public static Agent buildActionAgent(
        LLMClient llmClient,
        Role supportRole,
        AgentPrincipal principal,
        LiabilitySink liabilitySink,
        AuthorityScope authorityScope
    ) {
        return AgentBuilder.create()
            .llm(llmClient)
            .role(supportRole)
            .knowledge(new KnowledgeEntry(
                KnowledgeSourceConfig.file(
                    "product-docs", "knowledge/products"),
                Knowledge.Use.SEARCH,
                OptionalInt.empty(),
                Optional.of(SearchSettings.builder()
                    .strategy(Search.Strategy.HYBRID)
                    .queryParam("question")
                    .topK(5)
                    .build())))
            .actionKnowledge("answer", "product-docs")
            .principal(principal)
            .liabilitySink(liabilitySink)
            .authorityScope(authorityScope)
            .build();
    }

    public static Agent buildCanonicalChatAgent(
        LLMClient llmClient,
        Role supportRole,
        AgentPrincipal principal,
        LiabilitySink liabilitySink,
        AuthorityScope authorityScope
    ) {
        return AgentBuilder.create()
            .llm(llmClient)
            .role(supportRole)
            .knowledge(KnowledgeEntry.search(
                KnowledgeSourceConfig.file("faq", "knowledge/faq")))
            .principal(principal)
            .liabilitySink(liabilitySink)
            .authorityScope(authorityScope)
            .build();
    }
}
```

`knowledge(KnowledgeEntry...)` layers entries over the agent's
`@AgentSpec` knowledge by name. An entry whose name the annotation declares
replaces that entry, and any other entry joins the agent's chat scope. Repeated
calls add to the entries already given. `KnowledgeEntry.search(config)`
builds a `SEARCH` entry with the default settings, and the record constructor
takes the use, the `DIRECT` budget and the `SearchSettings` explicitly.

`actionKnowledge(action, names...)` names the knowledge one action uses and
replaces the list its `@ActionSpec` names. The action and the names are
checked when the agent first initializes, not by `build()`: an action the
agent does not have fails with the list of its actions.

`knowledgeUsageListener(...)` receives one `KnowledgeUsage` row per source
for every chat turn (scope `"chat"`) and every action call that runs
knowledge (scope = the action name). `knowledgeScope(...)` shares
action-level source snapshots and ordered cursors with other agents and
bridges that use the same host-owned scope.

## Three ways to ground chat

What separates them is **what describes the source**, and when it is resolved:

| path | source described by | resolved |
| --- | --- | --- |
| `@AgentSpec(knowledge = ...)` | annotations | at init. A `SEARCH` entry needs an installed `RetrievalEngineProvider` |
| `AgentBuilder.knowledge(...)` | a `KnowledgeEntry` over a `KnowledgeSourceConfig` | at init, on the same terms |
| a `MEMORY` entry + `setChatKnowledgeBinding(...)` | nothing (the application supplies the binding) | never. Chat grounds when the binding arrives |

`KnowledgeSourceConfig` is a **declarative descriptor**: path, format,
connection, query. It cannot wrap a live in-memory object, which is the
whole reason the third row exists.

Declaring a runtime corpus as a file or URL source does **not** leave
chat ungrounded. It makes startup fail. Agent chat knowledge creates its
retrieval engine at startup, and a missing `RetrievalEngineProvider` or an
unloadable source fails there. The answer for a corpus the application
owns is row three, not a workaround for rows one and two.

### Application-supplied live sources

A `MEMORY` entry declares that chat grounds on a source the application
supplies at runtime. Nothing is loaded or indexed for it and initialization
builds no retrieval engine on its account, so no `RetrievalEngineProvider`
is required for it. The agent starts ungrounded and stays that way until a
binding arrives. A `MEMORY` entry is chat-only: declare it on `@AgentSpec`
or through `AgentBuilder.knowledge(...)`, never on a role or an action. A
chat scope holds at most one `MEMORY` entry, and `DIRECT` entries may sit beside
it.

Other `SEARCH` entries may sit beside it too. Each chat turn then
searches both kinds, the other `SEARCH` knowledge first, then `MEMORY`, and
each kind has its own binding:

- `setChatKnowledgeBinding` replaces only the binding of the kind its
  sources name, and `setMemoryChatKnowledgeBinding` sets the `MEMORY` one.
  A binding that searches `MEMORY` together with other knowledge is refused.
- Each binding's `topK` is checked against the `@Search` of its own kind,
  and each renders its evidence within its own context window, so a turn
  that searches both can carry up to twice the evidence of one.
- `getChatKnowledgeBindings()` returns the bindings a turn searches, in the
  order their evidence renders. `getChatKnowledgeBinding()` returns the other
  `SEARCH` binding and falls back to the `MEMORY` one.
- `invalidateChatKnowledge()` advances the revision of every binding and
  returns the `MEMORY` binding's, and `clearConversationHistory()` advances
  both. To advance one binding alone, call that binding's own `invalidate()`.
- `getRetrievalConfig()`, `AgentSpecExtractor.getRetrieval()` and
  `RagDiagnostics`' type-level declaration give the settings of the other
  `SEARCH` entries, not of the `MEMORY` entry.
  `KnowledgeDeclarations.withoutMemoryBesideSearch(entries)` gives the
  entries those settings merge over, and
  `ChatKnowledgeBindingValidator.searchesMemory(binding, entries)` tells
  whether a binding searches the `MEMORY` knowledge.

A SCOP chat turn can also search an application source that no `@Knowledge`
declares, through `ChatKnowledgeBinding.undeclared(...)`. See
[Exact owner and `isOwnedBy`](../connect/scop.md#exact-owner-and-isownedby).

`ChatKnowledgeBinding.live(ownerId, sourceName, topK, knowledgeBase)`
adapts an application-owned corpus. It **holds** the corpus rather than
copying it, so documents added after binding are visible to retrieval,
and closing the binding does not close the corpus. That stays the
application's to manage, which matters when the same corpus is rebound
on every message. Retrieval is keyword-strategy, fail-open and uncached,
matching what `KnowledgeBase.search` actually does.

**The name is load-bearing, and both halves must agree.** The binding
must be owned by that agent, search only the declared entry, and carry that
entry's `topK`: the chat default of `5` unless the entry's `@Search` sets
one. Write the two halves together:

<!-- java-contract: src/main/java/com/example/tnsai/docs/LiveChatKnowledgeExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.knowledge.KnowledgeBase;
import com.tnsai.llm.LLMClient;
import com.tnsai.rag.ChatKnowledgeBinding;
import com.tnsai.rag.KnowledgeEntry;
import com.tnsai.rag.KnowledgeSourceConfig;
import com.tnsai.roles.Role;

public final class LiveChatKnowledgeExample {
    private LiveChatKnowledgeExample() {}

    // Both halves must name the same source, and the binding must carry the
    // entry's topK (5 unless the entry sets one), or the agent refuses it.
    private static final String SOURCE = "session-corpus";

    public static Agent buildAgent(
        LLMClient llmClient,
        Role supportRole,
        AgentPrincipal principal,
        LiabilitySink liabilitySink,
        AuthorityScope authorityScope
    ) {
        return AgentBuilder.create()
            .llm(llmClient)
            .role(supportRole)
            .knowledge(KnowledgeEntry.search(KnowledgeSourceConfig.memory(SOURCE)))
            .principal(principal)
            .liabilitySink(liabilitySink)
            .authorityScope(authorityScope)
            .build();
    }

    public static void groundChat(
        Agent agent,
        KnowledgeBase applicationOwnedCorpus
    ) {
        agent.setChatKnowledgeBinding(
            ChatKnowledgeBinding.live(
                agent.getId(), SOURCE, 5, applicationOwnedCorpus));
    }
}
```

A refused binding is not quiet. `setChatKnowledgeBinding` throws
`IllegalArgumentException` when the owner, the source name or the `topK`
disagrees with the declaration, and `IllegalStateException` when the agent
has no enabled `SEARCH` chat entry at all or has already stopped or
failed. On a stopped or failed agent, a binding that passes its check is
closed before the exception is thrown. A binding set before the agent
initializes is held as a `MEMORY` or other `SEARCH` binding by the knowledge
declared when it is given, and checked again at start. If the knowledge at
start makes its source the other kind, the start fails with
`IllegalStateException` and the binding is closed. A binding given after
start is routed by the knowledge declared at that moment.

A start that fails closes the bindings given before it, and `stop()` closes
every chat binding even when closing one throws, leaving the agent
`STOPPED`.

## Which one to use

Use chat knowledge when every conversational turn should be grounded in the
same sources. It retrieves before the LLM call, fences retrieved content,
and adds the matching disclosure to the system prompt.

Use action knowledge when retrieval belongs to specialized actions. It runs
during action dispatch, and only for the actions that name the source.

Explicit builder values win over annotations. A builder entry replaces an
annotation entry with the same name. Other named entries remain available,
and `actionKnowledge(...)` replaces the list an `@ActionSpec` names.

For a corpus the application owns and keeps mutating, declare a `MEMORY`
entry and bind a `KnowledgeBase` to it with `ChatKnowledgeBinding.live(...)`.
[Upgrade](../upgrade/index.md) maps the knowledge APIs that earlier releases
removed.

## Field reference

`@Knowledge` and `@Search` are never placed on their own: `@Knowledge` goes in the `knowledge` member of `@AgentSpec`,
`@RoleSpec` or `@ActionSpec`, and `@Search` is the `search` member of a
`@Knowledge` entry.

### Knowledge fields

| `@Knowledge` field | Effect |
|---|---|
| `name` | Required. Unique within the declaring Agent or Role. Actions, the class config file and run-level overrides address the source by it. |
| `type` | Selects the loader. Default `FILE`. TnsAI bundles loaders for `FILE` and HTTP/HTTPS `URL`. `DATABASE` needs an application-supplied `SourceLoader`. `MEMORY` (application-supplied chat corpus) and `GRAPH` (a graph-store handle for actions) load nothing. |
| `format` | Selects AUTO detection or strict per-file parsing for FILE and URL sources. |
| `path` | Supplies the file, directory or URL. |
| `connection` | Supplies the database connection for a `DATABASE` source. |
| `query` | Supplies the ingestion query template for a `DATABASE` source. |
| `enabled` | Includes or skips the source during binding. A disabled source cannot be named by an action. |
| `select` | Keeps only the blocks at and below a structural path, such as `$.products` for JSON. Strict per document. |
| `include` | Includes matching FILE paths before ingestion. |
| `exclude` | Excludes matching FILE paths before ingestion. |
| `unit` | Selects `AUTO`, `DOCUMENT`, `LINE`, `HEADING`, or `CHUNK` slicing at ingest. |
| `chunkContext` | Selects `NONE`, `STRUCTURAL`, or `LLM` context prepended for indexing. `LINE` rejects non-`NONE`. |
| `chunkContextModel` | Selects the generator model for `chunkContext = LLM`. |
| `use` | `SEARCH` (default), `DIRECT` or `SEQUENTIAL`. Settable on an action's entry. |
| `maxTokens` | Budget for a `DIRECT` source, only valid with `use = DIRECT`. Settable on an action's entry. |
| `search` | Ranking settings for a `SEARCH` source. Settable on an action's entry, where it replaces the declared `@Search` as a whole, so repeat every member to keep. |

`use`, `maxTokens` and `search` are arrays whose empty default means "not
given". Write one value as usual. More than one value fails at startup.

### Search fields

Every `@Search` member is an array whose empty default means "not given".
The default column is the value an unset member takes on an action. On a chat
turn an unset `minScore` is `0.0`, and the other defaults are the same. Several `SEARCH`
sources in one scope run through one retrieval engine, so their `@Search`
members merge field by field, and two sources setting the same member to
different values fail at startup. A scope is one class's whole declaration
(the `SEARCH` entries of its `@RoleSpec` and `@AgentSpec` together) and, again,
the entries one action names. A disabled `SEARCH` entry still takes part in
the merge, so it can cause a conflict. `GRAPH` entries merge only in a scope
whose `SEARCH` entries are all `GRAPH`. Tune a source differently per action on the
action's entry, not on the declaration.

| `@Search` field | Effect | Default |
|---|---|---|
| `strategy` | Selects semantic, keyword, hybrid, graph, multi-query, hierarchical, temporal, or reasoning retrieval. | `SEMANTIC` |
| `queryParam` | Selects a named action parameter when compiled with `-parameters`. Unset keeps positional inference. | positional |
| `rerank` | Enables provider-backed reranking. | `false` |
| `rerankerModel` | Selects the installed reranker provider and model. | `""` |
| `contextWindow` | Caps the rendered retrieval context in tokens. | `4000` |
| `topK` | Caps results per source from the selected strategy. | `5` |
| `topN` | Caps results after reranking. | `3` |
| `minScore` | Drops results below the relevance threshold. | `0.5` |
| `deduplicate` | Enables post-fusion duplicate removal. | `true` |
| `dedupeThreshold` | Sets the duplicate similarity threshold. | `0.9` |
| `queryExpansion` | Selects the query-expansion mode. | `NONE` |
| `queryExpansionModel` | Selects the installed query-expansion provider and model. | `""` |
| `navigatorModel` | Selects the installed `TreeNavigator` when `strategy` is `REASONING`. Required on that path. Blank or unmatched identifiers fail before `onFailure`. | `""` |
| `expandedQueries` | Caps generated query variants. | `3` |
| `includeSpec` | Controls source/score metadata in rendered context. | `true` |
| `contextFormat` | Defines the validated context template. | `[Source: ${source}]\n${content}\n` |
| `cache` | Enables the bounded per-Role retrieval cache. | `true` |
| `cacheTTL` | Sets cache lifetime in seconds. | `300` |
| `maxStaleness` | Caps stale-cache age for `USE_CACHE`, in seconds past expiry. `-1` removes the ceiling. | `3600` |
| `onFailure` | Selects the runtime-failure policy: `CONTINUE`, `FAIL`, `USE_CACHE` or `RETRY_SIMPLE`. | `CONTINUE` |

## Related

- [Retrieval strategies](retrieval-strategies.md): what each `@Search` strategy does.
- [Install retrieval providers](providers.md): the providers a strategy needs.
- [Grounded action tutorial](../tutorials/grounded-action.md): action knowledge end to end.
- [SCOP Bridge](../connect/scop.md): knowledge on SCOP objects and chat turns.
- [Upgrade](../upgrade/index.md): mapping from the removed knowledge annotations.
