Overview
createOracleApp(opts: CreateOracleAppOptions): Promise<OracleApp> resolves the plugin set, validates env, populates registries, builds the dynamic RuntimeAppModule, bootstraps NestJS, and returns an OracleApp whose listen() runs beforeListen callbacks then starts the HTTP server.
Matrix init runs in the background — the returned promise resolves as soon as Nest is built. Status flips via onPluginStatusChange.
CreateOracleAppOptions
config
config
- Type:
OracleConfig - Required: yes
name, optional org, optional description, optional prompt. The runtime fills in entityDid from the ORACLE_ENTITY_DID env var, so don’t put it here.openingUsed verbatim as the identity preamble — the very first section of the assembled system prompt. Write it as a complete paragraph describing who the oracle is and what it does.If opening is absent, the runtime generates a fallback from the other top-level fields:name+org+descriptionpresent →"You are {name}, an AI agent operated by {org}. {description}."orgmissing →"You are {name}. {description}."- Both
organddescriptionmissing →"You are {name}."
opening gives you full control over tone, persona, and framing — use it whenever the generated fallback is too generic.communicationStyleInjected inside the hardcoded ”## Operating principles” section of the prompt. If set, it appears as a paragraph within that section immediately after the 7 standard operating-principle bullets. If empty or absent, the field is omitted entirely — the operating-principles section still appears, just without the custom style block.Use this to steer the agent’s tone: formal, concise, empathetic, domain-specific jargon preferences, and so on.capabilitiesRendered above the Tier-1 plugin capability block (the auto-generated list of visibility='always' plugins). No header is added by the runtime around this text — write your own heading if you want one. If empty or absent, the field is omitted entirely.Use this to describe high-level skills the oracle has that aren’t obvious from the plugin manifest alone — things like domain expertise, supported workflows, or what the agent should proactively offer.customInstructionsFree-form standing guidance injected verbatim into a dedicated ## Custom Instructions section of the system prompt. Use it for house style, domain rules, compliance guardrails, or any directive that doesn’t fit communicationStyle (tone) or capabilities (the elevator pitch). The runtime renders the section only when there is content.Operating guides contributed by on-demand capabilities the agent loads mid-thread (e.g. the Flow Builder guide, which appears once flows is in loadedPlugins) are appended after your text in the same section — so they cost no tokens on turns where the capability is never loaded.entityDidDo not set this in code. The runtime reads it from the ORACLE_ENTITY_DID environment variable and populates it automatically during boot. Setting it inline has no effect.Full examplefeatures
features
- Type:
Partial<Record<BundledFeatureName | (string & {}), FeatureToggle>>whereFeatureToggle = boolean | 'auto' - Required: no
name field of each plugin (e.g. 'domain-indexer', 'user-preferences'), not a camelCase alias. Values: true (force on), false (force off), 'auto' (run autoDetect(env)). Omitted keys are treated as 'auto'.manifestOverrides
manifestOverrides
- Type:
Partial<Record<BundledFeatureName | (string & {}), PluginManifestOverride>>wherePluginManifestOverride = Partial<PluginManifest> - Required: no
- Added in:
@ixo/oracle-runtime1.2.0
visibility — without forking the plugin source. Typical uses: flip a noisy always plugin to on-demand, hide one behind silent, or relabel its summary / tags / whenToUse.Keys are plugin names (the same kebab-case identifiers used by features, e.g. 'domain-indexer'). Values are a Partial<PluginManifest> — set only the fields you want to change.- Keys you set win; keys you omit keep the plugin default.
undefinedvalues are ignored — a sparse override never blanks out a field.- Arrays and nested objects are replaced wholesale (not deep-merged). For example, supplying
whenToUse: [...]replaces the plugin’s full list rather than appending to it.
list_capabilities / load_capability meta-tools, and the agent’s visibility index — so an override is the single source of truth from boot onwards.ValidationBecause the merged manifest is run through the same validateManifest rules as an authored one (see Manifest schema), an override is held to the same constraints. For example, an override that empties whenToUse on a non-silent plugin fails boot with a clear Plugin manifest validation failed error.Unknown keysOverride keys that don’t match a loaded plugin (e.g. the plugin is excluded by features or simply misspelt) are logged and ignored — they do not fail boot:The same option is available on
createTestRuntime with identical shape and semantics, so a test can exercise a plugin under a retuned visibility (e.g. forcing silent) without a bespoke fixture.plugins
plugins
- Type:
OraclePlugin[] - Required: no
new EditorPlugin({ matrixClient }), new CreditsPlugin({ redis, network })). The plugin loader dedupes by name, so an explicit instance overrides the bundled default of the same name.nestModules
nestModules
- Type:
Array<Type | DynamicModule> - Required: no
RuntimeAppModule.imports. They get full DI access to the Tier-0 services (Sessions, Messages, Secrets, UCAN, …) and can declare controllers, providers, OnModuleInit, OnModuleDestroy.AuthHeaderMiddleware unless you list them in authExcludedRoutes.authExcludedRoutes
authExcludedRoutes
- Type:
AuthExcludedRoute[] - Required: no
AuthHeaderMiddleware. Use for routes contributed by nestModules (webhooks, OAuth callbacks, public probes). Symmetric with each plugin’s getAuthExcludedRoutes() hook — both lists merge onto the runtime’s built-in exclusions (/health, /docs).bundledPlugins
bundledPlugins
- Type:
OraclePlugin[] - Required: no
createOracleApp without dragging in the full bundled catalog. Production callers should leave this unset.env
env
- Type:
NodeJS.ProcessEnv - Required: no
process.env. Tests use this to inject a clean, controlled env so the boot doesn’t depend on the machine’s environment. Production code should leave it unset.skipMatrixInit
skipMatrixInit
- Type:
boolean - Required: no
skipGracefulShutdown
skipGracefulShutdown
- Type:
boolean - Required: no
logger
logger
- Type:
Logger - Required: no
Logger interface (log, error, warn, optional debug/verbose/child). Falls back to NestJS Logger.hooks
hooks
- Type:
MainAgentHooks - Required: no
resolveModel), the per-user checkpointer (checkpointerForUser), and the prompt/middleware toggles. Merged on top of the runtime’s defaults, and host hooks win.This is where you change the AI model. See MainAgentHooks below for all ten fields, the resolveModel example, and the 'main'-only nuance.MainAgentHooks
hooks lets you override how the runtime builds the main agent on every request, without forking the runtime. The runtime merges your hooks over its own defaults — your hooks win ({ ...defaultHooks, ...opts.hooks }). The only default the runtime sets is checkpointerForUser (a per-user SQLite store); everything else is unset unless you provide it.
Change the AI model
This is the answer to “how do I change the model?”.resolveModel is the model resolver. Return any LangChain BaseChatModel for the given role:
Where model IDs come from. Each role (
main, subagent, vision, guard, …) maps to a hardcoded model ID per provider. The provider is chosen by env: LLM_PROVIDER = openrouter (default) or nebius, with the matching key (OPEN_ROUTER_API_KEY / NEBIUS_API_KEY). There is no env var to change the main model ID — overriding it is exactly what resolveModel is for. Building the model with getProviderChatModel (rather than new ChatOpenAI(...)) keeps the OpenRouter fallback-models list and latency sort the 'main' role ships with.All ten fields
| Field | Type | What it does |
|---|---|---|
resolveModel | (role: ModelRole, params?: ChatOpenAIFields) => BaseChatModel | Resolves the chat model. Called once, with 'main'. Default: ambient.llm.get('main'). |
checkpointerForUser | (userDid: string) => Promise<BaseCheckpointSaver> | Per-user conversation store. Default: per-user SQLite synced to Matrix — override to swap the backend. |
getRoomTitle | (roomId: string) => Promise<string | undefined> | Page-title lookup. Providing it adds the page-context middleware to the stack; omitting it leaves that middleware out. |
safetyModel | BaseChatModel | Cheap classifier model. Providing it adds the safety-guardrail middleware; omitting it leaves it out. |
validationSkipToolNames | string[] | Tool names whose dangling ToolMessage outputs are stripped before the next model call — typically sub-agent tools whose output the caller summarises, not the model. |
operationalMode | string | Replaces the default operational-mode prompt block. (The editor plugin’s per-page mode still overrides this when a page is open.) |
editorSection | string | Editor prompt block — normally populated by the editor plugin; set it only if you compose the section yourself. |
composioContext | string | Composio guidance prompt block — normally populated by the composio plugin. |
userSecretsContext | string | Per-key secret bullet list rendered into the prompt (e.g. - _USER_SECRET_FOO). |
degradedServicesBlock | string | A “some services are degraded” notice appended to the system prompt body. |
ModelRole is 'main' | 'subagent' | 'utility' | (string & {}). ChatOpenAIFields is Record<string, unknown> — the optional fields forwarded to the underlying chat model.
Swap the checkpointer — supply your own LangGraph saver per user:
OracleApp
The resolvedOracleApp exposes:
getNestApp()
getNestApp()
Returns the underlying
INestApplication. Use it to add Express middleware, register global filters, or do anything else NestJS exposes. Mainly an escape hatch — nestModules and beforeListen cover most cases.ambient
ambient
The production
AmbientServices bag — used by MessagesController to build per-request RuntimeContexts before invoking createMainAgent. Forks normally don’t touch this directly.plugins.status()
plugins.status()
Returns a snapshot of loader/exclusion/soft-dep state.
beforeListen(fn)
beforeListen(fn)
Register a callback that runs after Nest is built but before HTTP starts accepting. Multiple registrations run in order, awaiting each.
onError(handler)
onError(handler)
Subscribe to background errors. The runtime currently surfaces Matrix init failures here (with
source: 'matrix-init'). If no handler is registered, errors log to the bootstrap logger.onPluginStatusChange(handler)
onPluginStatusChange(handler)
Subscribe to plugin lifecycle changes. The current emitter is the Matrix init flow:Multiple handlers may register; each is invoked for every event.
listen(port?)
listen(port?)
Start the HTTP server. Honours
beforeListen callbacks first. Port resolution: explicit port arg → PORT env (validated, defaults to 3000). Throws if called twice.Behaviour
The function executes these steps in order:- Validates
config(namerequired). - Resolves plugin set: bundled + your plugins, deduped by
name, withfeaturestoggles andautoDetectpredicates applied. Excluded plugins surface inplugins.status().excludedwith their reason. - Topologically sorts by
dependsOn. Cycles or missing hard deps fail boot. - Shallow-merges any
manifestOverridesonto each loaded plugin’s manifest, then validates the merged manifest. Hard violations fail boot. Override keys that don’t match a loaded plugin are logged (boot.plugin.manifest_override_unknown) and ignored. - Composes env schema (base + all loaded plugins’
configSchema) and validatesprocess.env. Missing required vars fail boot with[boot-error] Plugin '<name>' env validation failed for '<field>'. - Cross-field check: the API key for the selected
LLM_PROVIDERis present. (Schema-merge can’t express this.) - Builds the internal
OracleIdentityfromconfig+ validated env. - Populates the six registries (tools, sub-agents, middleware, manifests, configSchema, sharedState).
- Collects
getNestModulesfrom every loaded plugin. - Builds the dynamic
RuntimeAppModuleand bootstraps NestJS (NestFactory.create). Installs a globalValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), enables CORS with the framework’s allowed headers (authorization,x-auth-type,x-ucan-delegation, …), and mounts Swagger UI at/docs.
The global
ValidationPipe applies to every controller — including ones you add via nestModules or a plugin’s getNestModules. Annotate request DTOs with class-validator decorators: unknown body fields are rejected (forbidNonWhitelisted), unannotated props are stripped (whitelist), and nested DTOs are instantiated (transform). A controller that silently 400s on an extra field is hitting this pipe.- Builds
AmbientServices. - Warms the boot caches (each registry runs its boot-time hooks once).
- Wires the default checkpointer (
UserMatrixSqliteSyncService+SqliteSaver), then merges hosthooksover it. - Populates the
OracleRuntimeBundleHoldersoMessagesControllercan read it on each request. - Schedules background Matrix init (unless
skipMatrixInit: true). Registers a graceful-shutdown handler (unlessskipGracefulShutdown: true). - Returns the
OracleApp.
listen() blocks on beforeListen callbacks before starting HTTP.
Throws
Error('createOracleApp: \'config\' is required.')—configmissing.Error('createOracleApp: \'config.name\' is required.')—config.nameempty.- Plugin resolution errors (cycle, missing hard dep, manifest invalid, env invalid). Each error is reported via
logger.errorwith[boot-error] …prefix and a remediation hint. Error('createOracleApp: ORACLE_ENTITY_DID env is required and was empty after validation.')— when the validated env’sORACLE_ENTITY_DIDis empty.Error('OracleApp.listen called twice.')— whenlisten()is called more than once.
Related guides
- Create your oracle — hands-on walkthrough.
- Using bundled plugins — the
featuresfield in detail. - Plugin API reference — what plugin instances must implement.