symfonic.core.plugins.base¶
base ¶
BaseDomainPlugin -- protocol for domain-specific agent behavior plugins.
Plugins inject domain knowledge, guardrails, and tools into the agent without modifying the core library. Multiple plugins can be loaded and their contributions are chained in registration order.
BaseDomainPlugin ¶
Bases: Protocol
Protocol for domain-specific agent behavior plugins.
Plugins provide three REQUIRED hooks:
inject_system_prompt: adds domain instructions to the system prompt (single string contribution).validate_state_transition: guardrails that can block agent actions.get_domain_tools: MUST return[]. Domain tools are passed to the constructor viaSymfonicAgent(tools=[...])— theToolRegistryfreezes when the graph compiles in__init__, andload_plugin()runs after that, so a plugin cannot add tools. A non-empty return raisesSymfonicAgentError(seeget_domain_toolsbelow). The hook is vestigial for tools and kept only for Protocol compatibility.
Plugins MAY ALSO define ONE OPTIONAL hook (added in 7.4):
inject_contributions: returnslist[PluginContribution]so a plugin can emit content at multiple cache positions in one call ("cached"prefix vs."volatile"suffix). If defined, this method REPLACESinject_system_promptfor prompt rendering — the legacy method is NOT called for plugins that implement the new API. Plugin authors migrating frominject_system_promptcan leave the legacy method asreturn ""or delete it (the Protocol still considers the class compliant as long as the name exists; an empty stub satisfies that).
Signature::
async def inject_contributions(
self,
state: PromptState,
) -> list[PluginContribution]: ...
See symfonic.core.plugins.PluginContribution for the
contribution shape and the design note at
.claude/docs/2026-05-26-plugin-api-redesign-design.md.
Cache benefit on position="volatile" requires
FrameworkConfig.prompt_layer_mode="stratigraphic". On the JIT
path, volatile contributions are appended to the flat prompt body
with no separate L2 region; the engine emits a one-time WARNING
log per agent instance the first time this is observed.
inject_contributions is NOT part of the runtime-checkable
Protocol surface — that would break
isinstance(plugin, BaseDomainPlugin) for every legacy plugin.
Dispatch happens via hasattr at the engine call site.
All hooks are called by the agent at appropriate lifecycle points. Implement this protocol to inject business-specific behavior without modifying the core library.
get_domain_tools ¶
Soft-deprecated (v8.6.6). MUST return [].
Domain tools are NOT registered through plugins. Pass them to the constructor instead::
SymfonicAgent(tools=[my_tool, ...])
The ToolRegistry freezes when the agent graph compiles inside
SymfonicAgent.__init__. load_plugin() runs only after that
point, so any tools returned here can no longer be registered:
load_plugin() raises SymfonicAgentError on a non-empty return.
There is no pre-compile plugin path that collects these tools, so a
non-empty return is always an error.
This hook is retained only for plugin-Protocol compatibility (like the
inject_system_prompt 7.4 soft-deprecation); it is not removed.
Returns:
| Type | Description |
|---|---|
list[Any]
|
Always |
Source code in src/symfonic/core/plugins/base.py
inject_system_prompt
async
¶
Return domain-specific system prompt content.
DEPRECATION NOTICE (7.4, soft): plugins SHOULD migrate to
inject_contributions (see the class docstring) for
multi-position support and cache hygiene. The legacy method
remains supported indefinitely; no DeprecationWarning is
emitted. Hard removal is deferred until adoption telemetry
exists.
Called during HMS prompt composition. The returned text is appended as a section to the system prompt.
The state mapping is guaranteed to contain at least these
stable keys on every render, across both the JIT and stratified
prompt-assembly paths (v7.3.x+):
TENANT_ID str -- active tenant for this turn
NAMESPACE str -- tenant namespace (or sub-tenant id)
DOMAIN_NAME str -- FrameworkConfig domain identifier
QUERY str -- user query for this turn; empty string
when no query is available (never None,
never missing)
Additional context keys MAY be present and vary between the JIT
and stratified strategies -- for example TOOL_MANIFEST,
MEMORY_CONTEXT, SOUL_SCHEMA, CORE_PREFERENCES,
ACTIVE_TOOLS, HYDRATED_CONTEXT, REQUIRED_LABELS,
ONBOARDING_CHECKLIST, CURRENT_TIMESTAMP,
PROSPECTIVE_TASKS, DOMAIN_DESCRIPTION. Plugins MUST use
state.get(key, default) for any non-stable key.
QUERY was added to the contract in v7.3.x to unblock
dynamic-retrieval plugins (vector search, RAG, query-conditioned
HMS scoping); previously plugins had no race-free path to the
user query (lifecycle hooks fire past prompt assembly, and
plugin instances are shared across concurrent runs).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
PromptState
|
Current prompt state. See the stable-key list above. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Domain instructions string (empty string to skip). |
Source code in src/symfonic/core/plugins/base.py
validate_state_transition
async
¶
Validate whether an agent action should be allowed.
Called before tool execution or state changes. Return False to block the action.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
str
|
Name of the action/tool being attempted. |
required |
context
|
dict[str, Any]
|
Current execution context (tenant, params, etc.) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True to allow, False to block. |