Class SCOPBridge
This is a facade that delegates to focused helpers:
SystemPromptBuilder- annotation-to-prompt conversionLLMDispatcher- provider-specific HTTP transport (fallback)BridgeLLMClient- LLMClient adapter over HTTP fallbackLLMConfiguration- resolved LLM config (provider, model, endpoint, keys)ActionExecutionResult- action execution result wrapperLLMResponse- LLM response wrapper
When Core's LLMClientProvider SPI is available on the classpath
(e.g., tnsai-llm module), this bridge uses real provider implementations
(OllamaClient, OpenAIClient, etc.) with full streaming and retry support.
Otherwise, it falls back to BridgeLLMClient + LLMDispatcher.
executeAction(Object, String, Map) requires explicit
AgentPrincipal and AuthorityScope wiring through the fluent
setters. A LiabilitySink is optional and is never selected by default.
Missing identity/authority wiring fails before retrieval or the
action body so a SEQUENTIAL knowledge cursor cannot advance and then
still return an error.
-
Method Summary
Modifier and TypeMethodDescriptionauthorityScope(com.tnsai.accountability.AuthorityScope authorityScope) Records this authority scope on every dispatch.com.tnsai.security.AccessRequestRechecks current execution binding without hooks, retrieval, approval consumption or invocation.Builds a system prompt from TnsAI annotations on the target.voidconfigureExecution(com.tnsai.security.AccessSubject agent) Configure the existing action gate once at trusted host startup.voidconfigureExecution(com.tnsai.security.AccessSubject agent, Function<Object, String> targetSystem) Optional trusted lookup for existing AuthorityScope target-system bounds.Executes a TnsAI@Actionmethod on the target object.executeAction(Object target, String actionName, Map<String, Object> params, Map<String, Path> resolvedPaths) Executes a TnsAI@Actionand locates named knowledge sources for this dispatch only.executeAction(Object target, String actionName, Map<String, Object> params, Map<String, Path> resolvedPaths, com.tnsai.security.InvocationContext invocation) Trusted ingress overload.Optional<com.tnsai.metadata.ActionMetadata> getActionSpec(Object target, String actionName) Gets the ActionMetadata for a specific action on the target.static SCOPBridgeReturns a new SCOPBridge with the default read timeout (300s).static SCOPBridgegetInstance(long readTimeoutSeconds) Returns a new SCOPBridge with a custom read timeout.static SCOPBridgegetInstance(com.tnsai.identity.AgentPrincipal principal, com.tnsai.accountability.LiabilitySink liabilitySink, com.tnsai.accountability.AuthorityScope authorityScope) Returns a new SCOPBridge whose action executor is already accountability-wired.booleanhasTnsAIActions(Object target) Checks if a target object has TnsAI @Action annotations.knowledgeScope(com.tnsai.rag.KnowledgeScope scope) Uses an explicit knowledge-source lifetime for subsequent action dispatches.liabilitySink(com.tnsai.accountability.LiabilitySink liabilitySink) Publishes per-dispatch liability records to this sink.llmConfigurationSource(LLMConfigurationSource llmConfigurationSource) Registers the external layer consulted when LLM configuration resolves.org.json.JSONArrayprepareConversationForDispatch(org.json.JSONArray conversation, Object... conversationOwners) Creates an outbound request copy with a fresh dynamic state snapshot.principal(com.tnsai.identity.AgentPrincipal principal) requireLoaded(Object target, Collection<String> names) requireLoaded(Object, Collection, Map)with no per-call locations, so the bridge-level ones fromresolvedSourcePaths(Map)apply, as they do forexecuteAction(Object, String, Map).requireLoaded(Object target, Collection<String> names, Map<String, Path> resolvedPaths) Loads the named SEARCH sources oftargetin the binding thatexecuteAction(Object, String, Map, Map)with the same target andresolvedPathsbinds, and returns the number of documents each holds.requireReady(Object target, com.tnsai.rag.KnowledgeScope scope) requireReady(Object, com.tnsai.rag.KnowledgeScope, Map)with no per-call locations, so the bridge-level ones fromresolvedSourcePaths(Map)apply, as they do forexecuteAction(Object, String, Map).requireReady(Object target, com.tnsai.rag.KnowledgeScope scope, Map<String, Path> resolvedPaths) Checks, before a run, that every configuredWHEN_CONFIGUREDsource oftargetis ready: its file exists and loads at least one unit, in the binding a dispatch oftargetwith the same scope andresolvedPathsreads.resolvedSourcePaths(Map<String, Path> resolvedSourcePaths) Supplies run-time locations for@Knowledgenames whosepathis empty or a placeholder.Optional<com.tnsai.llm.LLMClient> resolveLLMClient(Object target) Resolves an LLMClient for the given target.resolveLLMSpec(Object role, Object agent, Object playground) Resolves LLM configuration, deriving the external source's lookup key fromagent— seeresolveLLMSpec(Object, Object, Object, String).resolveLLMSpec(Object role, Object agent, Object playground, String agentName) Resolves LLM configuration following the hierarchy: run filerolessection > run file agent type section > role's own@Config> agent@Configrolessection > agent@Config> external source > Role > Agent > Playground.sendToLLM(LLMConfiguration config, org.json.JSONArray conversation, org.json.JSONArray tools) Sends a message to the LLM and returns the response.sendToLLM(LLMConfiguration config, org.json.JSONArray conversation, org.json.JSONArray tools, Object conversationOwner, com.tnsai.rag.ChatKnowledgeBinding binding, com.tnsai.rag.ChatRetrievalContext context) Sends a SCOP conversation through owner-scoped chat retrieval before dispatching it to the configured provider.
-
Method Details
-
getInstance
Returns a new SCOPBridge with the default read timeout (300s).Every call returns a new instance: this is a factory, not an accessor for a shared bridge. Configuration such as
configureExecution(com.tnsai.security.AccessSubject),principal(AgentPrincipal),authorityScope(AuthorityScope),liabilitySink(LiabilitySink),resolvedSourcePaths(Map)andllmConfigurationSource(LLMConfigurationSource)belongs to the instance it is called on, so apply it to the instance that executes the actions: configuring the result of a separategetInstance()call leaves every other instance unchanged, and some of these settings are then missing silently rather than with an error. -
getInstance
Returns a new SCOPBridge with a custom read timeout. LikegetInstance(), every call returns a new instance whose configuration is its own. -
getInstance
public static SCOPBridge getInstance(com.tnsai.identity.AgentPrincipal principal, com.tnsai.accountability.LiabilitySink liabilitySink, com.tnsai.accountability.AuthorityScope authorityScope) Returns a new SCOPBridge whose action executor is already accountability-wired. LikegetInstance(), every call returns a new instance whose configuration is its own.- Parameters:
principal- identity recorded on every dispatchliabilitySink- sink that receives per-dispatch recordsauthorityScope- authority recorded on every dispatch- Returns:
- wired bridge
- Throws:
NullPointerException- if any argument isnull- Since:
- 0.15.0
-
principal
Attributes subsequentexecuteAction(java.lang.Object, java.lang.String, java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.nio.file.Path>, com.tnsai.security.InvocationContext)calls to this principal.- Parameters:
principal- identity recorded on every dispatch- Returns:
- this bridge
- Throws:
NullPointerException- ifprincipalisnull- Since:
- 0.15.0
-
liabilitySink
Publishes per-dispatch liability records to this sink.There is no silent no-op default. Callers choose the sink explicitly, including test recording sinks.
- Parameters:
liabilitySink- sink that receives per-dispatch records- Returns:
- this bridge
- Throws:
NullPointerException- ifliabilitySinkisnull- Since:
- 0.15.0
-
authorityScope
Records this authority scope on every dispatch.Use
AuthorityScope.unrestricted(java.time.Duration)when the caller deliberately chose not to narrow — that is still an explicit decision, not a hidden default.- Parameters:
authorityScope- authority recorded on every dispatch- Returns:
- this bridge
- Throws:
NullPointerException- ifauthorityScopeisnull- Since:
- 0.15.0
-
resolvedSourcePaths
Supplies run-time locations for@Knowledgenames whosepathis empty or a placeholder.Subsequent
executeAction(Object, String, Map)calls put this map on the dispatch context so action knowledge (SEARCH, DIRECT and SEQUENTIAL) binds through the same locate-and-override rules asRoleRagBinding.forRole(Class, Map). A per-call map onexecuteAction(Object, String, Map, Map)wins for that dispatch only.- Parameters:
resolvedSourcePaths- source name to its location; copied- Returns:
- this bridge
- Throws:
NullPointerException- ifresolvedSourcePathsor a value isnull- Since:
- 0.16.1
-
knowledgeScope
Uses an explicit knowledge-source lifetime for subsequent action dispatches. The caller owns and closes the scope when its lifetime ends; active leased conversations keep resources until they finish. This bridge never closes it.- Parameters:
scope- knowledge-source lifetime, including one closing after this thread's turn has already acquired a lease- Returns:
- this bridge
- Since:
- 0.17.4
-
buildSystemPromptFromAnnotations
Builds a system prompt from TnsAI annotations on the target. Delegates toSystemPromptBuilder. -
hasTnsAIActions
Checks if a target object has TnsAI @Action annotations. -
getActionSpec
Gets the ActionMetadata for a specific action on the target. -
configureExecution
public void configureExecution(com.tnsai.security.AccessSubject agent) Configure the existing action gate once at trusted host startup. -
configureExecution
public void configureExecution(com.tnsai.security.AccessSubject agent, Function<Object, String> targetSystem) Optional trusted lookup for existing AuthorityScope target-system bounds. -
authorizeAction
public com.tnsai.security.AccessRequest authorizeAction(Object target, String actionName, Map<String, Object> params) Rechecks current execution binding without hooks, retrieval, approval consumption or invocation. -
executeAction
public ActionExecutionResult executeAction(Object target, String actionName, Map<String, Object> params, Map<String, Path> resolvedPaths, com.tnsai.security.InvocationContext invocation) Trusted ingress overload. Invocation identity is never read from params.- Since:
- 0.18.0
-
executeAction
public ActionExecutionResult executeAction(Object target, String actionName, Map<String, Object> params) Executes a TnsAI@Actionmethod on the target object. Routes through Core'sActionExecutorpipeline.principal(AgentPrincipal)andauthorityScope(AuthorityScope)are required before dispatch.liabilitySink(LiabilitySink)is an optional recording destination. Missing wiring fails before retrieval or the action body so a SEQUENTIAL knowledge cursor cannot advance and then still return an error. Role and plain-Object hosts share this gate.- Parameters:
target- action host (Role or plain Object)actionName- declared action method nameparams- action parameters;nullbecomes empty- Returns:
- success, or a configuration/execution error that did not run retrieval or the action body when wiring is missing
-
executeAction
public ActionExecutionResult executeAction(Object target, String actionName, Map<String, Object> params, Map<String, Path> resolvedPaths) Executes a TnsAI@Actionand locates named knowledge sources for this dispatch only.resolvedPathsis copied onto the dispatch context underRetrievalSpi.RESOLVED_SOURCE_PATHS_KEY. Action knowledge keeps reading the annotations; the map only replacespathfor names the Role already declared.nullfalls back toresolvedSourcePaths(Map).- Parameters:
target- action host (Role or plain Object)actionName- declared action method nameparams- action parameters;nullbecomes emptyresolvedPaths- source name to its location for this call, ornullto use the fluent setter- Returns:
- success, or a configuration/execution error
- Since:
- 0.16.1
-
requireLoaded
requireLoaded(Object, Collection, Map)with no per-call locations, so the bridge-level ones fromresolvedSourcePaths(Map)apply, as they do forexecuteAction(Object, String, Map).- Parameters:
target- action host (Role or plain Object)names- declared, enabled sources read withuseSEARCH- Returns:
- each name's document count, in sorted name order
- Since:
- 0.18.0
-
requireLoaded
public Map<String,Integer> requireLoaded(Object target, Collection<String> names, Map<String, Path> resolvedPaths) Loads the named SEARCH sources oftargetin the binding thatexecuteAction(Object, String, Map, Map)with the same target andresolvedPathsbinds, and returns the number of documents each holds. Nothing is retrieved and no action runs.The check goes through the code the dispatch runs: it binds the role to its owner's configuration, takes the per-call locations or, for
null, the bridge-level ones, and leases this bridge'sknowledge scope, so the binding checked is the one the dispatch leases. Use this rather than the retrieval runtime's own preflight for a target this bridge dispatches; that one sees neither the bridge-level locations nor the owner's configuration.- Parameters:
target- action host (Role or plain Object)names- declared, enabled sources read withuseSEARCHresolvedPaths- source name to its location, as given toexecuteAction, ornullto use the fluent setter- Returns:
- each name's document count, in sorted name order
- Throws:
IllegalArgumentException- when a name or a location names a source the target does not declare, or a name is not read withuseSEARCHcom.tnsai.rag.RetrievalException- when a named source did not loadIllegalStateException- when no retrieval runtime is on the classpath, the knowledge scope is closed, the target's class has taken a differentrolessection, or the owner'srolessection names a role the owner does not play yet; the role is bound as a dispatch binds it, so check it once the owner plays all its configured rolesUnsupportedOperationException- when the retrieval runtime has no preflight- Since:
- 0.18.0
-
requireReady
requireReady(Object, com.tnsai.rag.KnowledgeScope, Map)with no per-call locations, so the bridge-level ones fromresolvedSourcePaths(Map)apply, as they do forexecuteAction(Object, String, Map).- Parameters:
target- action host (Role or plain Object)scope- the knowledge scope the host later sets withknowledgeScope(com.tnsai.rag.KnowledgeScope), ornullfor the process-wide one- Returns:
- each checked source's unit count, in sorted name order
- Since:
- 0.18.1
-
requireReady
public Map<String,Integer> requireReady(Object target, com.tnsai.rag.KnowledgeScope scope, Map<String, Path> resolvedPaths) Checks, before a run, that every configuredWHEN_CONFIGUREDsource oftargetis ready: its file exists and loads at least one unit, in the binding a dispatch oftargetwith the same scope andresolvedPathsreads. An unconfigured WHEN_CONFIGURED source is left out, and so is every MUST_LOAD source.The role is bound to its owner's configuration first, as a dispatch binds it. A target that then declares no knowledge, or a bridge with no retrieval runtime on the classpath, has nothing to check: the call returns an empty map without leasing the scope.
Pass the scope this bridge's dispatch will use, the one later given to
knowledgeScope(com.tnsai.rag.KnowledgeScope); another scope loads a generation the dispatch does not read. Unlike that setter, this method acceptsnull, meaning the process-wide cache, as a dispatch with no scope set uses.- Parameters:
target- action host (Role or plain Object)scope- the knowledge scope, ornullfor the process-wide oneresolvedPaths- source name to its location, as given toexecuteAction, ornullto use the fluent setter- Returns:
- each checked source's unit count, in sorted name order
- Throws:
com.tnsai.exceptions.KnowledgeNotReadyException- when a checked source's file is missing, loads no unit, or fails to load or count, whatever the reasoncom.tnsai.rag.RetrievalException- unchanged, when it comes from outside a source's load and count:CONFIGURATIONwhen an action of the target reads a WHEN_CONFIGURED source DIRECT, or the binding's own lease or buildIllegalArgumentException- for a source the knowledge generation cannot hold or readIllegalStateException- when the knowledge scope is closed, the target's class has taken a differentrolessection, or the owner'srolessection names a role the owner does not play yetUnsupportedOperationException- when the retrieval runtime has no readiness check- Since:
- 0.18.1
-
llmConfigurationSource
Registers the external layer consulted when LLM configuration resolves.Whatever the source names wins over
@RoleSpec.llm()and@AgentSpec.llm(); whatever it leaves out keeps its annotation value. The class configuration levels and the run file'sAgentssection sit above it, so this is the place for settings shared by every agent, such as a run'sLLMsection; an agent type's own settings reach the bridge through the run file, asresolveLLMSpec(Object, Object, Object, String)describes. The source is read on every resolution and never cached here, so an edit is visible to the next resolution in the same process.A source is trusted infrastructure. It can name an
apiKeyEnv, which reads any environment variable or System property the process can see, and anendpoint, which decides where that value is sent — read the trust paragraph onLLMConfigurationSourcebefore registering one.- Parameters:
llmConfigurationSource- the external source, e.g.LLMConfigurationSource.folder(Path)orLLMConfigurationSource.environment();LLMConfigurationSource.noOp()to disable the layer- Returns:
- this bridge
- Throws:
NullPointerException- ifllmConfigurationSourceisnull- Since:
- 0.16.4
-
resolveLLMSpec
Resolves LLM configuration, deriving the external source's lookup key fromagent— seeresolveLLMSpec(Object, Object, Object, String).- Throws:
IllegalStateException- if a@RoleSpecrole's class has taken a differentrolessection in this process, from another agent class or from a dispatch with no owner
-
resolveLLMSpec
public Optional<LLMConfiguration> resolveLLMSpec(Object role, Object agent, Object playground, String agentName) Resolves LLM configuration following the hierarchy: run filerolessection > run file agent type section > role's own@Config> agent@Configrolessection > agent@Config> external source > Role > Agent > Playground.The annotation tiers are combined in two groups, because not every setting is safe to mix across authors:
- Routing —
provider,model,endpoint,apiKeyEnv— is taken whole from the highest-precedence tier that declares amodel, which is exactly the tier that used to win outright. A tier naming other routing fields without a model contributes nothing, as before, but now says so. These four only mean anything as a set: a model belongs to a provider, an endpoint speaks one provider's wire format, a key is issued by one provider. Letting them cross tier boundaries independently would send a role author's local model to an agent author's hosted endpoint under that host's key — a configuration neither author wrote. - Tuning —
temperatureandmaxTokens— merges field by field across every tier. These carry no routing meaning, so an agent that declares a temperature but no model now contributes that temperature to whichever tier supplies the routing. That merge is the change TAN-6201 asked for; before it, the whole tier was skipped becausehasModel()was false.
A setting no layer declares keeps the default
LLMSpecdeclares for it.A tier is read through
LLMOverrides.fromAnnotation(LLMSpec), which treats a member as declared when it differs fromLLMSpec's own default — necessarily, since@AgentSpec.llm()and@RoleSpec.llm()both default to@LLMSpec()and therefore carry a full set of member values whether or not their author wrote any of them.The one all-or-nothing rule that remains is the terminal one: if no layer at all supplies a model, the resolution yields
Optional.empty()rather than a configuration that names no model.A tier that declares
modelwithoutprovidertherefore gets theLLMSpecdefault provider, not a lower tier's — which is exactly what that annotation means on its own, sinceprovider()defaults toOLLAMA.The external source is the one layer that does overlay every field individually, including routing. That is deliberate and is what TAN-6201 requires: the tiers are different authors, whereas a source is one operator configuring a deployment, and requiring them to restate provider and temperature in every file is the duplication this layer exists to remove. A source naming
modelwithoutproviderkeeps the annotation's provider, so the two must agree.Above the external source sit the
llmlevels of the class configurations (Config): the agent's file, then therolessection bound for the role, then the role's own file, then the agent type's section of the run file a host passed toClassConfig.useBaseDirectory(ClassConfig.RunFile)and therolessection that gives the role. The bound sections are the ones the role's knowledge reads; with no agent they are empty, so only the role's own file applies, and an agent class that gives the role a section fails. Each overlays field by field, like the external source, so a file namingmodelalone keeps the provider resolved below it. The class configuration is read once per class and run.- Parameters:
agentName- lookup key for the external source. The no-key overload derives it from the agent's owngetName()— whichcom.tnsai.agents.Agentdeclares — and falls back to the simple class name; pass it explicitly to override that derivation. Anullor blank key skips the source.- Throws:
LLMConfigurationException- if the external source holds settings for the agent but they cannot be readIllegalStateException- if a@RoleSpecrole's class has taken a differentrolessection in this process, from another agent class or from a dispatch with no owner; if a class configuration file or the run file section breaks its rules (ClassConfig.of(java.lang.Class<?>),ClassConfig.useBaseDirectory(ClassConfig.RunFile)), such as a simple-name key two classes share; or if the run file section givesrolesthat name no role the agent plays, or that cannot be checked because the agent does not list its roles- Since:
- 0.16.4
- Routing —
-
resolveLLMClient
Resolves an LLMClient for the given target. Prefers Core's LLMClientProvider SPI when available; falls back to BridgeLLMClient.- Throws:
IllegalStateException- if the target's class, a@RoleSpecrole, has taken a differentrolessection in this process, from another agent class or from a dispatch with no owner
-
sendToLLM
public LLMResponse sendToLLM(LLMConfiguration config, org.json.JSONArray conversation, org.json.JSONArray tools) Sends a message to the LLM and returns the response. Delegates toLLMDispatcher. -
prepareConversationForDispatch
public org.json.JSONArray prepareConversationForDispatch(org.json.JSONArray conversation, Object... conversationOwners) Creates an outbound request copy with a fresh dynamic state snapshot.The caller-owned conversation is never mutated. Only fields explicitly annotated with
Stateare read. Static persona and instruction content stays in place while any prior bridge-markedCurrent Statesection is replaced by one versioned request-local section. Calls with no annotated owners return a deep copy after removing any prior bridge-marked state.When multiple owners contribute state, owners are rendered in caller encounter order and each owner's fields are sorted lexically by their effective
state name. Repeated references to the same owner object are rendered once. This transport-neutral preparation step must be used immediately before both buffered and streaming LLM dispatches so action/tool-loop mutations are visible to the follow-up request without growing stored history.Diagnostics log only owner and field counts; state values are never written to logs by this bridge.
Prompts persisted from bridge versions that predate versioned dynamic markers must be rebuilt once. This method deliberately does not infer section boundaries from untrusted Markdown headings.
- Parameters:
conversation- caller-owned conversation historyconversationOwners- exact owner or role objects whose annotated state contributes to this request; null entries and annotation-free objects are ignored- Returns:
- a request-local deep copy, never the input instance
- Throws:
NullPointerException- ifconversationisnullIllegalArgumentException- if annotated state must be added to a system message whose content is neither text nor a structured content array- Since:
- 0.16.0
-
sendToLLM
public LLMResponse sendToLLM(LLMConfiguration config, org.json.JSONArray conversation, org.json.JSONArray tools, Object conversationOwner, com.tnsai.rag.ChatKnowledgeBinding binding, com.tnsai.rag.ChatRetrievalContext context) Sends a SCOP conversation through owner-scoped chat retrieval before dispatching it to the configured provider.The exact
conversationOwnersupplies the@AgentSpec(knowledge = ...)declarations; its enabled SEARCH entries are the ones the binding may search. The caller supplies the matching owner-scoped binding and request-local context; the bridge does not discover owners from global state or keep retrieval lifecycle state of its own. Run, episode, reset, and end transitions remain explicit throughChatKnowledgeBinding.invalidate()andChatKnowledgeBinding.close().The owner's chat knowledge is the one an
Agentchat turn sees (KnowledgeDeclarations.chatScope(List)): enabled SEARCH and DIRECT entries, SEQUENTIAL ignored, GRAPH left out. DIRECT entries are given whole through the retrieval runtime on every call, within their budget against the input window ofconfig's model, and render around the SEARCH evidence in declaration order. The bridge keeps no state for them, so each call loads and measures them again.Knowledge is added only to a deep copy of the outbound conversation. A call without a retrieval context, owners without chat knowledge, empty results, and recoverable retrieval failures preserve the existing direct-dispatch behavior. A binding that belongs to another owner fails the call even without a retrieval context.
- Parameters:
config- provider-neutral LLM configurationconversation- caller-owned conversation historytools- optional provider-neutral tool declarationsconversationOwner- exact object whose chat annotations apply; chat-enabled bindings must be created with the same object through an exact-ownerChatKnowledgeBindingfactorybinding- exact-owner-scoped live retrieval binding, ornullwhen the owner has no SEARCH chat knowledge or the optional retrieval runtime is unavailablecontext- immutable request-local scope and lifecycle context, ornullwhen the optional retrieval runtime is unavailable- Returns:
- provider response
- Throws:
IllegalArgumentException- when the owner's knowledge and the supplied binding disagreeIllegalStateException- when the owner's DIRECT knowledge has no retrieval runtime to give it or exceeds its budget, or its knowledge breaks a declaration rule- Since:
- 0.14.0
-