Skip to main content

Overview

RuntimeContext is the runtime-side bag a plugin’s code sees on every request. Built fresh per LangGraph invocation by buildRuntimeContext(runConfig, ambient, state).
import type { RuntimeContext } from '@ixo/oracle-runtime';

Full shape

export interface RuntimeContext<TConfig = MergedConfig> {
  user: {
    did: string;
    matrixUserId: string;
    ucanDelegation: UcanDelegation;
    timezone?: string;
    currentTime?: string;
  };
  session: {
    id: string;
    client: 'portal' | 'matrix' | 'slack';
    wsId?: string;
    requestId: string;
    roomId?: string;
  };
  history: {
    messages: readonly BaseMessage[];
    recent: (n: number) => BaseMessage[];
    userContext: UserContextData;
    state: ReadonlyState;
  };
  config: TConfig;
  availablePlugins: ReadonlySet<string>;
  loadedPlugins: ReadonlySet<string>;
  secrets: {
    getIndex: () => Promise<SecretIndex>;
    getValues: (keys: string[]) => Promise<Record<string, string>>;
  };
  blobStore: {
    put: (params: { userDid: string; name: string; value: string; ttlSeconds?: number }) => Promise<string>;
    get: (params: { userDid: string; blobId: string }) => Promise<{ name: string; value: string } | null>;
    isValidBlobId: (value: unknown) => value is string;
  };
  matrix: {
    postToRoom: (roomId: string, content: unknown) => Promise<string>;
    getRoomState: (roomId: string) => Promise<RoomStateSnapshot>;
    getEventById: (roomId: string, eventId: string) => Promise<MatrixEvent>;
  };
  ucan: {
    requireCapability: (resource: string, action: string) => void;
    hasCapability: (resource: string, action: string) => boolean;
    mintInvocation: (target: { did: string; capability: string }, opts?: { skipCache?: boolean }) => Promise<string>;
    resolveServiceDid: (serviceUrl: string) => Promise<string | null>;
    hasSigningKey: () => boolean;
    createInvocationFromDelegation: (
      delegationCar: string,
      serviceUrl: string,
      capability: { can: string; with: string },
      options?: { maxTtlSeconds?: number },
    ) => Promise<{ invocation: string } | { error: string }>;
  };
  llm: {
    get: (role: ModelRole, params?: ChatOpenAIFields) => BaseChatModel;
  };
  emit: {
    toolCall: (payload: ToolCallEventPayload) => void;
    actionCall: (payload: ActionCallEventPayload) => void;
    renderComponent: (payload: RenderComponentEventPayload) => void;
    reasoning: (payload: ReasoningEventPayload) => void;
    browserToolCall: (payload: BrowserToolCallEventPayload) => void;
    router: (payload: RouterEventPayload) => void;
    messageCacheInvalidation: (payload: MessageCacheInvalidationPayload) => void;
  };
  logger: Logger;
  abortSignal: AbortSignal;
  shared: SharedAccessors;
  toolCallId?: string;
}

Fields

The authenticated user. Validated by AuthHeaderMiddleware before the request reaches any plugin code.
  • did — IXO DID (did:ixo:ixo1...).
  • matrixUserId — e.g. @did-ixo-ixo1abc:ixo.world.
  • ucanDelegation — UCAN envelope from x-ucan-delegation header.
  • timezone — optional, from x-timezone header.
  • currentTime — optional, ISO timestamp.
  • id — the thread ID (Matrix root eventId).
  • client'portal' | 'matrix' | 'slack'.
  • wsId — optional WebSocket connection ID.
  • requestId — correlation ID.
  • roomId — optional Matrix room ID for the active conversation.
  • messages — readonly array of BaseMessage (LangChain). The full thread history loaded by the checkpointer.
  • recent(n) — convenience method returning the most recent n messages.
  • userContext — enrichment object from state.userContext (typically populated by the Memory plugin).
  • stateReadonlyState, a typed view over the LangGraph annotation state. See State schema.
Same merged + validated env as PluginContext.config. Typed by your plugin’s own schema:
const units = configSchema.parse(rtCtx.config).WEATHER_DEFAULT_UNITS;
The names of every plugin that survived boot resolution. Fixed.
The names of on-demand plugins the agent has loaded for this thread via load_capability. Plus implicitly all always plugins. Per-thread, monotonically growing across turns.
Per-room secrets, JWE-encrypted, 24h cache.
  • getIndex() — returns the SecretIndex (metadata only, no values).
  • getValues(keys) — returns plaintext for the requested keys.
Backed by today’s SecretsService. Returns nothing if the encryption key isn’t provisioned.
Short-TTL, user-namespaced store for content the LLM must never relay verbatim — UCAN invocation CARs, JWTs, signed envelopes. A producing tool stores the value and returns a short opaque ID; a consuming tool looks it up server-side and forwards it on. The model only ever sees the ID.
  • put({ userDid, name, value, ttlSeconds? }) — store a value, returns a fresh blob_<16 hex> ID. TTL defaults to 1h and is clamped to the service max (24h). Pass userDid from a trusted source (e.g. rtCtx.user.did) — never from LLM-supplied tool args.
  • get({ userDid, blobId }) — retrieve a blob scoped to the requesting user. Returns null if it doesn’t exist, has expired, or belongs to a different user (cross-user reads always miss).
  • isValidBlobId(value) — cheap format check (blob_<16 hex>); use it in a tool’s input handler to reject malformed IDs before paying for a lookup.
const blobId = await rtCtx.blobStore.put({
  userDid: rtCtx.user.did,
  name: 'signed-invocation',
  value: signedCar,
});
// hand `blobId` back to the model; resolve it server-side in the next tool
const blob = await rtCtx.blobStore.get({ userDid: rtCtx.user.did, blobId });
Scoped Matrix operations.
  • postToRoom(roomId, content) — post a message; returns the event ID.
  • getRoomState(roomId) — snapshot of state events.
  • getEventById(roomId, eventId) — fetch a specific event.
The runtime does not expose the raw Matrix client — only these three scoped methods.
UCAN authorisation helpers.
  • requireCapability(resource, action) — throws if the user’s delegation doesn’t include this capability.
  • hasCapability(resource, action) — boolean check.
  • mintInvocation({ did, capability }, opts?) — mint a downstream invocation signed by the oracle’s signing mnemonic.
  • resolveServiceDid(serviceUrl) — look up a downstream service’s DID document; returns id or null.
  • hasSigningKey()true once the oracle has loaded its Ed25519 signing mnemonic. Gate registration of mint-capable tools on this: without a key, minting is a no-op, so the tool should surface an error rather than pretend it worked.
  • createInvocationFromDelegation(delegationCar, serviceUrl, capability, options?) — mint an invocation from a directly-supplied delegation CAR (rather than the user’s cached one), targeted at a specific service route. Returns { invocation } on success or { error } with a surfaced-verbatim reason (missing signing key, audience mismatch, did:web unreachable, …).
if (!rtCtx.ucan.hasSigningKey()) {
  return { error: 'This oracle is not configured to mint invocations.' };
}
const result = await rtCtx.ucan.createInvocationFromDelegation(
  delegationCar,
  'https://service.example',
  { can: 'submit', with: 'service:claims' },
);
if ('error' in result) return { error: result.error };
// use result.invocation
  • get(role, params?) — returns a BaseChatModel for the given role.
role is one of 'main' | 'subagent' | 'utility' or any custom string mapped in your provider config. The framework’s provider config maps roles to specific OpenRouter / Nebius / OpenAI models.Plugins should use this rather than instantiating LangChain models directly — the provider config handles auth headers, base URLs, and per-role model selection.
Typed event emitter. Bundled clients (Portal, Slack) consume these events; render them into UI. See the API endpoints reference for the WebSocket event protocol.Available events: toolCall, actionCall, renderComponent, reasoning, browserToolCall, router, messageCacheInvalidation. Payload types are currently Record<string, unknown> and may be tightened in future versions.
Same as PluginContext.logger. Plugin-scoped, auto-prefixed with the plugin name.
Propagates from the incoming HTTP request / graph invocation. Pass it to fetch calls so client disconnects abort upstream work.
const response = await fetch(url, { signal: rtCtx.abortSignal });
Read accessors for state owned by other plugins (registered via getSharedState()). Typed via declaration merging on the SharedAccessors interface — see Plugin shared state guide.
The identifier of the inbound tool call that triggered this handler, when available. undefined for direct / test invocations.Used by tools that return a LangGraph Command and need to append a matching ToolMessage to the state update.

Lifetime

Built fresh per graph invocation by buildRuntimeContext(runConfig, ambient, state). Lives for the duration of that single turn. Don’t store references to rtCtx for use across turns — its inner services (Matrix client, secrets cache) may be invalid by the next call.