Orchestrator load, plugins, and settings
How chat reaches an orchestrator from the pool, how OrchestratorFactory wires adapters and SDKPluginManager, plugin contract resolution, settings injection, and stateless bundle caching.
Intended audience: Solution architects, Developers
Runtime loading path in `engine/` and `infra/plugins/`. Pair with the upload pipeline guide for ZIP validation vs pool-time loading.
Learning outcomes by role
Solution architects
- Map hot vs demand pool, S3-backed cache, and shared bundle cache to scaling and isolation concerns.
Developers
- Trace `OrchestratorPool.get` through `factory.create`, `load_plugins`, and bundle creation in source order.
- Explain registry vs filesystem resolution, `ensure_local`, and `PluginSettingsResolver.resolve` for a given `PluginRef`.
This guide walks through how an orchestrator instance is obtained at runtime, how plugins are resolved and turned into SDKPluginBundle objects, and how per-instance settings are merged and decrypted. For uploading ZIPs to the catalog, see Plugin upload and verification. For product context, see Hot reload AI App pool, AI Agent system, and Chat and engine.
Big picture
Section titled “Big picture”On a chat request, the engine resolves an orchestrator instance id, then OrchestratorPool.get(instance_id) returns a BaseOrchestrator. If the instance is not in the hot or demand pool, the pool loads configuration from the database and OrchestratorFactory.create builds: adapter → SDKPluginManager → load_plugins → streaming wrapper → orchestrator → initialize().
flowchart TB
subgraph poolLayer [OrchestratorPool]
get[get instance_id]
hot[hot_pool dict]
dem[demand_pool TTL]
cold[_load_from_db]
end
subgraph factoryLayer [OrchestratorFactory.create]
reg[registry lookup]
pm[SDKPluginManager.load_plugins]
sw[streaming wrapper]
orch[orchestrator.initialize]
end
get --> hot
get --> dem
get --> cold
cold --> reg
reg --> pm
pm --> sw
sw --> orch
Pool — getting an orchestrator
Section titled “Pool — getting an orchestrator”Why: Orchestrators are expensive; hot instances stay resident; demand instances are TTL-evicted and capped.
What: OrchestratorPool.get in src/cadence/engine/pool/pool.py.
In the code
Section titled “In the code”- If
instance_id in self.hot_pool→ return immediately. - Else
self.demand_pool.get(instance_id)— on hit, TTL is extended (DemandPool.getupdates_last_accessed). - Else acquire
self.locks[instance_id], double-check hot and demand, thenawait self._load_from_db(instance_id, source="on_demand").
DemandPool.peek returns an instance without extending TTL (used when you must inspect without treating access as “use” — e.g. reload paths).
async def get(self, instance_id: str) -> BaseOrchestrator: if instance_id in self.hot_pool: return self.hot_pool[instance_id] orch = self.demand_pool.get(instance_id) if orch is not None: return orch self._ensure_lock(instance_id) async with self.locks[instance_id]: if instance_id in self.hot_pool: return self.hot_pool[instance_id] orch = self.demand_pool.get(instance_id) if orch is not None: return orch return await self._load_from_db(instance_id, source="on_demand")Cold load — DB row to factory
Section titled “Cold load — DB row to factory”Why: Demand-tier misses must hydrate framework_type, mode, config, plugin_settings, and whoami from persistence before construction.
What: _load_from_db loads the instance row, then build_resolved_instance_config (src/cadence/engine/config_builder.py), then await self.factory.create(...), then self.demand_pool.set(instance_id, orchestrator).
Config shapes
Section titled “Config shapes”instance_config—configmerged withplugin_settingsfrom the row (org overrides for each plugin key).resolved_config—instance_configplusorg_idandwhoami, passed into the orchestrator stack.
instance_config = { **config, "plugin_settings": instance.get("plugin_settings", {}),}resolved_config = { **instance_config, "org_id": instance["org_id"], "whoami": instance.get("whoami") or "",}Factory — wiring
Section titled “Factory — wiring”What: OrchestratorFactory.create in src/cadence/engine/factory.py.
Order:
registry[(framework_type, mode)]→BackendRegistryEntry(adapter, orchestrator, streaming wrapper classes).adapter = adapter_class()SDKPluginManager(...)withllm_factory,org_id, tenant/system plugin roots,plugin_repo,bundle_cacheawait plugin_manager.load_plugins(plugin_specs, instance_config)whereplugin_specs = instance_config.get("active_plugins", [])streaming_wrapper = streaming_wrapper_class()orchestrator = orchestrator_class(plugin_manager=..., llm_factory=..., resolved_config=..., adapter=..., streaming_wrapper=...)await orchestrator.initialize()
sequenceDiagram participant Pool as OrchestratorPool participant Fact as OrchestratorFactory participant PM as SDKPluginManager participant Orch as BaseOrchestrator Pool->>Fact: create(framework_type, mode, org_id, instance_config, resolved_config) Fact->>PM: load_plugins(active_plugins, instance_config) Fact->>Orch: orchestrator_class(...) Fact->>Orch: initialize()
Plugin loading loop
Section titled “Plugin loading loop”What: SDKPluginManager.load_plugins in src/cadence/infra/plugins/plugin_manager.py.
For each string in active_plugins (e.g. org:com.example.plugin@1.0.0):
PluginRef.from_ref(plugin_spec)contract = await self._resolve_contract(plugin_ref, registry)- Deduplicate by
(source, pid, contract.version) self._validate_plugin(contract)(SDK structure validation)bundle = await self._create_bundle_with_cache(contract, settings_resolver, plugin_ref)- Store in
self._bundles
PluginSettingsResolver(instance_config) is constructed once per load and passed into bundle creation.
flowchart TD
loop[For each active_plugins string]
ref[PluginRef.from_ref]
contract[_resolve_contract]
dedupe{bundle key seen?}
validate[_validate_plugin]
bundle[_create_bundle_with_cache]
store[Store in _bundles]
skip[Skip duplicate version]
loop --> ref
ref --> contract
contract --> dedupe
dedupe -->|yes| skip
dedupe -->|no| validate
validate --> bundle
bundle --> store
Contract resolution — registry, filesystem, S3 sync
Section titled “Contract resolution — registry, filesystem, S3 sync”What: PluginLoaderMixin._resolve_contract in src/cadence/infra/plugins/plugin_loader.py.
registry.get_plugin_by_version(pid, version)— in-process SDK registry (e.g. already loaded in this worker).- If missing →
await _load_versioned_plugin_from_filesystem(plugin_ref)which uses_resolve_local_plugin_directory→_find_plugin_file→_ensure_plugin_dependencies→_load_plugin_module(runsscan_plugin_astbeforeexec_module) →_extract_plugin_class→PluginContract+ register in registry. - If still missing →
PluginNotFoundError. - If
plugin_repoexists and S3 is enabled →ensure_local(pid, version, org_id)to align local cache with object storage; onResourceNotFoundError, logs a warning and continues with local-only content.
Local directory resolution
Section titled “Local directory resolution”_resolve_local_plugin_directory tries, in order:
plugin_repo.ensure_local(...)if a store is configured (may download and extract from S3).- Else tenant path
tenant_plugins_root / org_id / pid / versionor system pathsystem_plugins_dir / pid / versionif directories exist and are non-empty.
Cache miss — ensure_local
Section titled “Cache miss — ensure_local”What: PluginStoreRepository.ensure_local in src/cadence/data/plugins/store.py.
- If local dir exists with content → cache hit.
- If S3 disabled and missing locally →
PluginNotFoundError(when that path is used as the only source). - If S3 enabled → download
s3_key,_extract_zipinto the local cache path.
Plugin not found — decision outline
Section titled “Plugin not found — decision outline”flowchart TD
start[_resolve_contract]
reg{registry hit?}
fs[_load_versioned_plugin_from_filesystem]
local[_resolve_local_plugin_directory]
hit{local content?}
s3{S3 enabled?}
load[load module register contract]
fail[PluginNotFoundError]
start --> reg
reg -->|yes| done[return contract]
reg -->|no| fs
fs --> local
local --> hit
hit -->|yes| load
hit -->|no| s3
s3 -->|try download| load
s3 -->|cannot| fail
load --> done
Settings injection
Section titled “Settings injection”What: PluginSettingsResolver.resolve in src/cadence/infra/plugins/plugin_settings_resolver.py.
Order:
- Schema from
get_plugin_settings_schema(plugin_class)→ defaults. - Overrides from
instance_config["plugin_settings"][plugin_ref.to_ref()]— keys likeorg:pid@version. - Merge
{**defaults, **overrides}. decrypt_sensitive_settings(resolved, settings_schema)._validate_required—PluginValidationErrorif a required key is missing.
Lookup uses PluginRef.to_ref() so instance rows and API payloads stay aligned with active_plugins strings.
sequenceDiagram
participant R as PluginSettingsResolver
participant S as settings schema
participant DB as instance_config plugin_settings
participant D as decrypt_sensitive_settings
participant V as _validate_required
R->>S: defaults from decorator schema
R->>DB: overrides by PluginRef key
R->>R: merge overrides over defaults
R->>D: decrypt sensitive fields
D-->>R: plaintext values
R->>V: required keys present
alt missing required
V-->>R: PluginValidationError
else ok
R-->>R: resolved_settings to bundle
end
Bundle creation and optional shared cache
Section titled “Bundle creation and optional shared cache”What: PluginBundleBuilderMixin in src/cadence/infra/plugins/plugin_bundle_builder.py.
_create_bundle (simplified order in source):
metadata = contract.plugin_class.get_metadata()agent = contract.plugin_class.create_agent()resolved_settings = settings_resolver.resolve(...)agent.initialize(resolved_settings)when supporteduv_tools = agent.get_tools()→orchestrator_toolsvia adapter_create_plugin_modelwhenllm_config_id(etc.) appears in resolved settings- LangGraph:
create_tool_node(uv_tools)whenadapter.framework_type == "langgraph" - Return
SDKPluginBundle
_create_bundle_with_cache: If bundle_cache is set and contract.is_stateless, SharedBundleCache.get_or_create keys bundles by (plugin_pid, version, settings_hash, adapter_type) where settings_hash is the first 16 hex chars of SHA-256 of sorted JSON settings (src/cadence/engine/shared_resources/bundle_cache.py). Otherwise builds a fresh bundle every time.
Stateful plugins skip sharing so agent state does not leak across instances.
Example instance_config slice
Section titled “Example instance_config slice”{ "active_plugins": ["org:com.acme.helpdesk@1.0.0"], "plugin_settings": { "org:com.acme.helpdesk@1.0.0": { "settings": [ {"key": "api_key", "value": "enc:..."}, {"key": "max_results", "value": 25}, ] } }}load_plugins walks active_plugins, resolves each PluginRef, merges settings for that ref, and attaches bundles to the SDKPluginManager used when the orchestrator graph calls into plugin tools.
Behavior summary
Section titled “Behavior summary”| Scenario | What happens |
|---|---|
| Plugin already in SDK registry | _resolve_contract returns without filesystem I/O |
| Not in registry, local tree present | Loaded from disk, registered, ensure_local may still sync S3 |
| Local empty, S3 on | ensure_local downloads zip and extracts |
| Local empty, S3 off | Resolution fails with PluginNotFoundError when no path works |
| S3 object missing but files local | Warning logged; local copy used |
| Stateless + cache + same settings | SharedBundleCache may reuse bundle |
| Stateful plugin | Always fresh bundle path (no cross-instance sharing) |
| Missing required setting | PluginValidationError during resolve |
Key modules
Section titled “Key modules”| Concern | File |
|---|---|
| Hot / demand / DB load | src/cadence/engine/pool/pool.py, src/cadence/engine/pool/demand_pool.py |
| Instance config merge | src/cadence/engine/config_builder.py |
| Factory pipeline | src/cadence/engine/factory.py |
| Plugin manager + bundles | src/cadence/infra/plugins/plugin_manager.py, src/cadence/infra/plugins/plugin_bundle_builder.py |
| Filesystem + registry load | src/cadence/infra/plugins/plugin_loader.py |
| Settings merge + decrypt | src/cadence/infra/plugins/plugin_settings_resolver.py |
| Stateless bundle cache | src/cadence/engine/shared_resources/bundle_cache.py |
| Local / S3 paths | src/cadence/data/plugins/store.py |
Related documentation
Section titled “Related documentation”- Plugin upload and verification — ZIP validation path (subprocess inspector, etc.).
- Developer onboarding — engine and
infralayout. - Hot reload AI App pool — when reload picks up new plugins.
- AI Agent system — product lifecycle.