Interface RetrievalSpi
Implementations resolve the knowledge an action names through
@ActionSpec(knowledge = ...) against the host's declared
@Knowledge, give or search it according to each entry's
use, and inject the formatted context string into the action's
context map under RETRIEVED_CONTEXT_KEY. Downstream LLM
executors read that key to splice the knowledge into the prompt.
use = SEQUENTIAL entries go through the same reserved context keys
so ordered consumption reuses the existing narration path.
Wired into ActionExecutor.executeInternal via find(),
which resolves the cached provider snapshot through SpiLoader. If no
implementation is on the classpath (e.g. tnsai-intelligence
isn't a dependency), the call is a no-op — actions continue to run
with no retrieved context, and SEARCH knowledge silently degrades to a
documentation hint. This matches the
SecurityEnforcerHandle / McpToolExecutor optional-SPI
pattern already used by ActionExecutor.
Issue #172, Phase 1 — earlier turns surfaced that
the memory and knowledge annotations were declarative-only. This SPI is the runtime
binding side; the default implementation lives in
tnsai-intelligence alongside RAGPipeline /
VectorMemoryStore / source loaders.
- Since:
- 0.5.4 (issue #172, Phase 1)
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final AtomicReference<Optional<RetrievalSpi>> Cache forfind()— null until first lookup, then immutable.static final StringPer-dispatch explicit source lifetime.static final StringContext key for aMap<String, java.nio.file.Path>of declared@Knowledgenames to locations known only at run time.static final StringThe key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)writes the formatted retrieved context string into an action'scontextmap.static final StringThe key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)records whether the injected context represents every retrieved document in full (Boolean).static final StringThe key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)writes the count of documents actually included in the assembled context (Integer), after empty-document filtering and context-window enforcement.static final StringThe key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)records what each knowledge source gave the action (List<KnowledgeUsage>), one row per enabled source in declaration order.static final StringThe key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)records whether the injected context came from a degraded fallback rather than a fresh retrieval (Boolean). -
Method Summary
Modifier and TypeMethodDescriptionvoidResolves the knowledge the given action names (it may name none) and, if any, gives or searches it against the host's declared knowledge according to each entry'suse.default voidenforce(ActionMetadata action, Object target, Map<String, Object> params, Map<String, Object> context) Same contract asenforce(ActionMetadata, Role, Map, Map)for a target that is not aRole.static Optional<RetrievalSpi> find()Discovers the registeredRetrievalSpiviaSpiLoader.default ChatKnowledgeContextprepareChatKnowledge(Object agent, List<KnowledgeEntry> entries, LLMClient llm) Prepares the DIRECT knowledge an agent's chat turns give.requireLoaded(Object target, Collection<String> names, Map<String, Object> context) Loads the named SEARCH sources oftargetin the binding a dispatch of its declared knowledge with thiscontextbinds, and returns the number of documents each holds.requireReady(Object target, Map<String, Object> context) Checks that every configuredWHEN_CONFIGUREDsource in the target's reach holds at least one unit, keyed as its actions' dispatch keys it.default voidvalidateConfiguration(Class<?> targetClass) Validates the knowledge declarations on a target before that target can dispatch an action.default voidvalidateConfiguration(Class<?> targetClass, List<KnowledgeEntry> outer, LLMClient llm) Validates a target's knowledge once the knowledge around it is known: every action scope resolved against the target's own declarations plusouter, and every DIRECT source measured against its budget.default voidvalidateConfiguration(Class<?> targetClass, List<KnowledgeEntry> outer, LLMClient llm, boolean pathsPerRun) Validates a target's knowledge asvalidateConfiguration(Class, java.util.List, com.tnsai.llm.LLMClient)does, for a host that locates FILE sources per run.default voidvalidateConfiguration(Class<?> targetClass, List<KnowledgeEntry> outer, LLMClient llm, Map<String, List<String>> actionKnowledge) Validates a target's knowledge asvalidateConfiguration(Class, java.util.List, com.tnsai.llm.LLMClient)does, with the knowledge of some actions given by name: an actionactionKnowledgenames sees the entries given there instead of the ones its@ActionSpecnames.
-
Field Details
-
RETRIEVED_CONTEXT_KEY
The key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)writes the formatted retrieved context string into an action'scontextmap. LLM executors read this key to splice retrieved snippets into the prompt.Underscored so it doesn't collide with consumer-supplied context keys; matches the
_approval_tokenconvention already used byActionExecutor.- See Also:
-
RETRIEVED_DOCUMENT_COUNT_KEY
The key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)writes the count of documents actually included in the assembled context (Integer), after empty-document filtering and context-window enforcement. Useful for telemetry / tests that want to assert "retrieval ran but injected nothing" vs "retrieval was skipped entirely".- See Also:
-
RETRIEVED_STALE_FALLBACK_KEY
The key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)records whether the injected context came from a degraded fallback rather than a fresh retrieval (Boolean).Written by exactly the code path that writes
RETRIEVED_CONTEXT_KEY, so the three states are unambiguous:- absent — no context was injected: retrieval was skipped, the action names no knowledge, or a failure policy declined to ground the action at all
Boolean.FALSE— the injected context is freshBoolean.TRUE— the injected context is a degraded fallback; under@Search(onFailure = USE_CACHE)it is an expired cache entry served after retrieval failed, so the documents may be arbitrarily old
Without this key a stale fallback is indistinguishable from fresh grounding: both leave a populated
RETRIEVED_CONTEXT_KEYand a positiveRETRIEVED_DOCUMENT_COUNT_KEY, which is whatLLMRoleExecutor's splice guard reads as "fully grounded". Executors, interceptors, and evaluators branch on this key to surface, annotate, or refuse degraded grounding.- Since:
- 0.13.0
- See Also:
-
RETRIEVED_CONTEXT_TRUNCATED_KEY
The key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)records whether the injected context represents every retrieved document in full (Boolean).Written by exactly the code path that writes
RETRIEVED_CONTEXT_KEY, on the same all-or-nothing terms asRETRIEVED_STALE_FALLBACK_KEY:- absent — no context was injected at all
Boolean.FALSE— every retrieved document reached the context intactBoolean.TRUE— the context is lossy: at least one document was dropped, or one document's content was shortened, to honourSearch.contextWindow()
This key is what makes an empty context legible. Assembly walks the score-ordered list and stops at the first document that will not fit, so a single oversized top-ranked document yields an empty context and a zero
RETRIEVED_DOCUMENT_COUNT_KEY— a stateLLMRoleExecutor's splice guard would otherwise read as "retrieval found nothing" and let the model answer ungrounded. With this key the two are distinct:- count
0, truncatedBoolean.FALSE— retrieval genuinely matched nothing - count
0, truncatedBoolean.TRUE— retrieval matched documents but the configuredcontextWindowcould not hold any of them; the action is ungrounded because it is misconfigured, not because the corpus is empty
- Since:
- 0.13.0
- See Also:
-
RETRIEVED_KNOWLEDGE_USAGE_KEY
The key under whichenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)records what each knowledge source gave the action (List<KnowledgeUsage>), one row per enabled source in declaration order.Written by the code paths that write
RETRIEVED_DOCUMENT_COUNT_KEY, on the same terms: absent means retrieval never ran; a row with count 0 means it ran and that source gave nothing. Builder retrieval, which declares no entries, reports one row per configured source, or per source the evidence names when none is configured.- Since:
- 0.18.0
- See Also:
-
RESOLVED_SOURCE_PATHS_KEY
Context key for aMap<String, java.nio.file.Path>of declared@Knowledgenames to locations known only at run time.RoleRagBinding.forRole(Class, Map)already locates those declarations. The SCOP Object path never called that factory:executeActionbuilt a context with no builder sources, so the knowledge bound against the annotation's emptypath. Callers put the map here; the retrieval SPI converts it through the same locate-and-override rules and leaves the annotation itself unchanged.Absent or empty means annotation paths only. An unknown name is a configuration error, not a silent new source.
ActionExecutorremoves the entry afterenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)so a reused context map cannot carry a previous episode's folder into the next dispatch.- Since:
- 0.16.1
- See Also:
-
KNOWLEDGE_SCOPE_KEY
Per-dispatch explicit source lifetime. Absent means the existing process-wide Role binding and target-owned sequential cursor behavior. The action executor consumes this key after retrieval enforcement.- Since:
- 0.17.4
- See Also:
-
HOLDER
Cache forfind()— null until first lookup, then immutable.
-
-
Method Details
-
validateConfiguration
Validates the knowledge declarations on a target before that target can dispatch an action.The default is a no-op so
tnsai-coreremains usable without an installed retrieval runtime. Implementations should resolve only declaration-time capabilities here; provider work that depends on a query remains inenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>).- Parameters:
targetClass- Role or object class whose declarations are being wired- Since:
- 0.15.0
-
validateConfiguration
Validates a target's knowledge once the knowledge around it is known: every action scope resolved against the target's own declarations plusouter, and every DIRECT source measured against its budget.Called at agent init and by hosts that bind a target to their own sources. The default is a no-op for the same reason as
validateConfiguration(Class).- Parameters:
targetClass- Role or object class whose knowledge is being wiredouter- knowledge in reach beyond the class's own, such as the agent's; it replaces a class entry of the same namellm- the model the target's actions run on, ornull; its input window sizes the default DIRECT budget- Since:
- 0.18.0
-
validateConfiguration
default void validateConfiguration(Class<?> targetClass, List<KnowledgeEntry> outer, LLMClient llm, Map<String, List<String>> actionKnowledge) Validates a target's knowledge asvalidateConfiguration(Class, java.util.List, com.tnsai.llm.LLMClient)does, with the knowledge of some actions given by name: an actionactionKnowledgenames sees the entries given there instead of the ones its@ActionSpecnames. The default ignoresactionKnowledge.- Parameters:
targetClass- Role or object class whose knowledge is being wiredouter- knowledge in reach beyond the class's ownllm- the model the target's actions run on, ornullactionKnowledge- entry names by action name- Since:
- 0.18.0
-
validateConfiguration
default void validateConfiguration(Class<?> targetClass, List<KnowledgeEntry> outer, LLMClient llm, boolean pathsPerRun) Validates a target's knowledge asvalidateConfiguration(Class, java.util.List, com.tnsai.llm.LLMClient)does, for a host that locates FILE sources per run.A FILE source declared without a path has no file until such a host resolves one for a run, so with
pathsPerRunits DIRECT budget is measured when it loads instead. Without it, as at agent init, the source is measured here and a missing path fails. The default ignorespathsPerRun.- Parameters:
targetClass- Role or object class whose knowledge is being wiredouter- knowledge in reach beyond the class's ownllm- the model the target's actions run on, ornullpathsPerRun-truewhen the host resolves FILE paths per run- Since:
- 0.18.0
-
prepareChatKnowledge
default ChatKnowledgeContext prepareChatKnowledge(Object agent, List<KnowledgeEntry> entries, LLMClient llm) Prepares the DIRECT knowledge an agent's chat turns give.Called once at agent initialization, only when an enabled
@AgentSpecentry in the chat scope (KnowledgeDeclarations.chatScope(java.util.List)) is DIRECT. Implementations check every DIRECT source against its budget here, so an oversized source or an unknown model window fails at initialization rather than on the first turn.The default throws: a runtime that cannot give this knowledge must not let the agent chat without it.
- Parameters:
agent- the agent instanceentries- the agent's chat knowledge, in declaration order, SEARCH entries included so the blocks keep their place around the SEARCH evidencellm- the agent's model, ornull; its input window sizes the default DIRECT budget- Returns:
- the per-turn knowledge
- Throws:
IllegalStateException- if this runtime does not support it- Since:
- 0.18.0
-
enforce
void enforce(ActionMetadata action, Role role, Map<String, Object> params, Map<String, Object> context) Resolves the knowledge the given action names (it may name none) and, if any, gives or searches it against the host's declared knowledge according to each entry'suse. Mutatescontextby adding the formatted context string underRETRIEVED_CONTEXT_KEY, the assembled document count underRETRIEVED_DOCUMENT_COUNT_KEY, the fallback provenance of that context underRETRIEVED_STALE_FALLBACK_KEY, and whether the context lost anything to the configured context window underRETRIEVED_CONTEXT_TRUNCATED_KEY.Implementations must not throw for benign cases: no named knowledge, missing knowledge sources, empty retrieval result. They may throw for genuine configuration errors (unparseable source path, contradictory topK / minScore, etc.) — those propagate up as
ActionExecutionException(VALIDATION)per the dispatcher's convention.- Parameters:
action- the action being dispatched (carries the method handle for annotation lookup)role- the role instance hosting the action (carries the class for class-level annotations)params- the action's working parameters — implementations may use one as the retrieval query (Phase 1: the firstStringparameter, since the knowledge declaration doesn't name which param is the query)context- the action's context map; mutated in place
-
enforce
default void enforce(ActionMetadata action, Object target, Map<String, Object> params, Map<String, Object> context) Same contract asenforce(ActionMetadata, Role, Map, Map)for a target that is not aRole.SCOP Conversation dispatches plain objects through
ActionExecutor.execute(Object). The Role overload cannot accept that target, so without this method knowledge on a SCOP action compiles and is discovered, then never runs (TAN-5876).The default delegates to the Role overload when
targetis aRole, and is a no-op otherwise. Implementations that already resolve knowledge sources fromtarget.getClass()must override this so a non-Role host is not silently skipped.- Parameters:
action- the action being dispatchedtarget- the object hosting the action (Role or a SCOP POJO)params- the action's working parameterscontext- the action's context map; mutated in place- Since:
- 0.14.1
-
requireLoaded
default Map<String,Integer> requireLoaded(Object target, Collection<String> names, Map<String, Object> context) Loads the named SEARCH sources oftargetin the binding a dispatch of its declared knowledge with thiscontextbinds, and returns the number of documents each holds. The context is read asenforce(com.tnsai.metadata.ActionMetadata, com.tnsai.roles.Role, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>)reads it:KNOWLEDGE_SCOPE_KEYfor the scope andRESOLVED_SOURCE_PATHS_KEYfor the run-time locations. Nothing is retrieved.A dispatcher that builds the context offers this beside its dispatch, so a host checks its knowledge against the context its actions get rather than rebuilding it.
- Parameters:
target- the object hosting the knowledgenames- declared, enabled sources read withuseSEARCHcontext- a context built as for the dispatch; not changed- Returns:
- each name's document count, in sorted name order
- Throws:
UnsupportedOperationException- if this provider does not support the preflight- Since:
- 0.18.0
-
requireReady
Checks that every configuredWHEN_CONFIGUREDsource in the target's reach holds at least one unit, keyed as its actions' dispatch keys it.An unconfigured WHEN_CONFIGURED source is skipped, and so is every
MUST_LOADsource, whose load already fails on a missing file.- Parameters:
target- the object hosting the knowledgecontext- a context built as for the dispatch; not changed- Returns:
- each checked source's unit count, in sorted name order; empty when the target declares no knowledge
- Throws:
KnowledgeNotReadyException- if a configured source is missing, empty or unreadableUnsupportedOperationException- if this provider does not support the readiness check- Since:
- 0.18.1
-
find
Discovers the registeredRetrievalSpiviaSpiLoader.The result is cached in an
AtomicReferenceso the lookup happens at most once per JVM.Optional.empty()means no implementation is on the classpath, which is a supported configuration — callers should treat it as a no-op.- Returns:
- the discovered SPI, or empty if no implementation is registered
-