Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ixo.world/llms.txt

Use this file to discover all available pages before exploring further.

A QiForge oracle is one main.ts that calls createOracleApp. The runtime boots a NestJS app, loads bundled + your plugins, validates env, builds the agent graph, and starts HTTP when you call listen(). Reference implementation: apps/qiforge-example/src/main.ts. Source: packages/oracle-runtime/src/bootstrap/create-oracle-app.ts.

Minimal main.ts

import 'dotenv/config';
import { createOracleApp } from '@ixo/oracle-runtime';

const app = await createOracleApp({
  config: { name: 'My Oracle', org: 'Acme' },
});
await app.listen();
That’s a working oracle — every bundled plugin runs its autoDetect(env) and the ones whose env is present load.

The recipe

Write your config

Put OracleConfig in its own file so tests can import it without booting the runtime.
// src/config.ts
import type { OracleConfig } from '@ixo/oracle-runtime';

export const config: OracleConfig = {
  name: 'My Oracle',                      // required
  org: 'My Org',                          // optional
  description: 'What this oracle is for', // optional — appears in the system prompt
  prompt: {                               // optional — every field optional
    opening: 'You are My Oracle. …',
    communicationStyle: '- Be terse …',
    capabilities: 'I can …',
  },
};
entityDid is sourced from the ORACLE_ENTITY_DID env var — never put it in config. The prompt block is composed into the system prompt; absent fields fall back to runtime defaults. Source: plugin-api/types.ts (search OracleConfig).

Add your plugins

Bundled plugins flow in automatically from BUNDLED_PLUGINS — each runs its own autoDetect(env). Only list the ones you wrote yourself or bundled plugins that need constructor args:
import { createOracleApp, EditorPlugin } from '@ixo/oracle-runtime';
import { WeatherPlugin } from './plugins/weather/weather.plugin.js';

const app = await createOracleApp({
  config,
  plugins: [
    new EditorPlugin({ matrixClient }),   // bundled, needs Matrix client
    new WeatherPlugin(),                  // yours
  ],
});
The loader dedupes by name, so an explicit instance overrides the bundled default of the same name. Source: bootstrap/plugin-loader.ts.

Toggle bundled plugins with features

Override autoDetect:
const app = await createOracleApp({
  config,
  features: {
    composio: false,        // force off
    firecrawl: true,        // force on, even if env is missing (will fail env validation if so)
    domainIndexer: 'auto',  // default — runs autoDetect(env)
  },
});
ValueMeaning
trueForce on. Skip autoDetect.
falseForce off. Skip even if autoDetect would say yes.
'auto'Default. Run autoDetect(env).
Omitted keys are treated as 'auto'. Recipe: Enable bundled plugins.

Mount your own Nest modules

Custom HTTP endpoints, event consumers, admin dashboards — anything that doesn’t fit the plugin model goes in nestModules. Each gets full DI access to runtime services (Sessions, Messages, Secrets, UCAN, Auth, …).
import { Controller, Get, Module } from '@nestjs/common';

@Controller('version')
class VersionController {
  @Get()
  get() { return { name: 'My Oracle', version: '1.0.0' }; }
}

@Module({ controllers: [VersionController] })
class VersionModule {}

const app = await createOracleApp({
  config,
  nestModules: [VersionModule],
});

Exclude routes from auth

Every route defaults to going through AuthHeaderMiddleware (requires x-ucan-delegation). Opt routes out via authExcludedRoutes:
import { RequestMethod } from '@nestjs/common';
import type { AuthExcludedRoute } from '@ixo/oracle-runtime';

const routes: AuthExcludedRoute[] = [
  { path: 'version', method: RequestMethod.GET },
];

const app = await createOracleApp({
  config,
  nestModules: [VersionModule],
  authExcludedRoutes: routes,
});
Symmetric with each plugin’s getAuthExcludedRoutes(). Both merge onto the runtime’s built-ins (/health, /docs). Recipe for the plugin side: Add HTTP endpoints.

Customise the agent graph (optional)

hooks overrides the agent-build defaults. Every field is optional:
import type { MainAgentHooks } from '@ixo/oracle-runtime';

const hooks: MainAgentHooks = {
  checkpointerForUser:    async (did) => myCheckpointer.for(did),
  resolveModel:           (role, params) => myModelFactory.get(role, params),
  getRoomTitle:           async (roomId) => myRoomTitles.get(roomId),
  safetyModel:            mySafetyModel,
  validationSkipToolNames: ['some_streaming_tool'],
  operationalMode:        'You operate in production mode …',
  editorSection:          '## Editor\n\n…',
  composioContext:        '## Composio\n\n…',
  userSecretsContext:     '## User secrets\n\n…',
  degradedServicesBlock:  '## Degraded services\n\n…',
};

const app = await createOracleApp({ config, hooks });
HookDefaultWhat it overrides
checkpointerForUser(did)per-user SQLite synced to MatrixSwap in your own BaseCheckpointSaver
resolveModel(role, params)ambient.llm.get(role)LLM resolver
getRoomTitle(roomId)undefinedPage-context middleware lookup
safetyModelsafety-guardrail defaultCheap classifier for the safety middleware
validationSkipToolNames[]Tool names whose ToolMessage is stripped between turns
operationalModeruntime defaultOperating-mode prompt block
editorSectionpopulated by editor pluginEditor prompt block
composioContextpopulated by composio pluginComposio guidance block
userSecretsContextemptyPer-key secret bullet list
degradedServicesBlockemptyDegraded-services notice appended to system prompt
Source: graph/main-agent-types.ts (search MainAgentHooks).

Run a beforeListen hook

Run setup that must complete before HTTP starts accepting:
const app = await createOracleApp({ config });

app.beforeListen(async (nestApp) => {
  await myWarmupCache(nestApp);
});

await app.listen();
Hooks run sequentially in registration order.

Observe plugin lifecycle

app.onPluginStatusChange((event) => {
  // { plugin, from: 'pending'|'loaded'|'failed', to: ..., reason? }
  logger.log(`[plugin] ${event.plugin} ${event.from}${event.to}`);
});

app.onError((err, source) => {
  logger.error(`[runtime] ${source}: ${err.message}`);
});

const status = app.plugins.status();
logger.log(`[boot] loaded: ${status.loaded.join(', ')}`);
onPluginStatusChange fires when Matrix transitions pending → loaded (or failed). onError catches Matrix init + lifecycle errors. plugins.status() returns the same shape qiforge inspect prints.

Listen

await app.listen();        // honours config.PORT, env.PORT, then 5678
await app.listen(3000);    // explicit port
Calling listen() twice throws. Default port is 5678 — set PORT env or pass a number to override.

All options at a glance

interface CreateOracleAppOptions {
  config: OracleConfig;                                            // required
  features?: Partial<Record<BundledFeatureName | string, FeatureToggle>>;
  plugins?: OraclePlugin[];
  nestModules?: Array<Type | DynamicModule>;
  authExcludedRoutes?: AuthExcludedRoute[];
  logger?: PluginLogger;
  hooks?: MainAgentHooks;

  // Test-only — do not use in production
  bundledPlugins?: OraclePlugin[];
  env?: NodeJS.ProcessEnv;
  skipMatrixInit?: boolean;
  skipGracefulShutdown?: boolean;
}
bundledPlugins, env, skipMatrixInit, and skipGracefulShutdown exist for the test harness. Don’t use them in production code — integration tests must boot the same way prod does.

Full example

import 'dotenv/config';
import { createOracleApp, EditorPlugin } from '@ixo/oracle-runtime';
import { Controller, Get, Logger, Module, RequestMethod } from '@nestjs/common';
import * as sdk from 'matrix-js-sdk';
import { config } from './config.js';
import { WeatherPlugin } from './plugins/weather/weather.plugin.js';

@Controller('version')
class VersionController {
  @Get() get() { return { name: 'My Oracle' }; }
}

@Module({ controllers: [VersionController] })
class VersionModule {}

async function bootstrap(): Promise<void> {
  const matrixClient = sdk.createClient({
    baseUrl: process.env.MATRIX_BASE_URL!,
    userId: process.env.MATRIX_ORACLE_ADMIN_USER_ID!,
    accessToken: process.env.MATRIX_ORACLE_ADMIN_ACCESS_TOKEN!,
  });

  const app = await createOracleApp({
    config,
    logger: Logger,
    plugins: [new EditorPlugin({ matrixClient }), new WeatherPlugin()],
    nestModules: [VersionModule],
    authExcludedRoutes: [{ path: 'version', method: RequestMethod.GET }],
  });

  app.onPluginStatusChange((event) => {
    Logger.log(`[plugin] ${event.plugin} ${event.from}${event.to}`);
  });

  Logger.log(`[boot] loaded: ${app.plugins.status().loaded.join(', ')}`);
  await app.listen();
}

bootstrap().catch((err) => {
  Logger.error('Oracle failed to start:', err);
  process.exit(1);
});

Write a plugin

End-to-end Weather plugin walkthrough.

Enable bundled plugins

The features map in detail.

createOracleApp reference

Flat reference table for every field.

Environment variables

Core + per-plugin env vars.