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; can?: string }) => 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
user
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 fromx-ucan-delegationheader.timezone— optional, fromx-timezoneheader.currentTime— optional, ISO timestamp.
session
id— the thread ID (Matrix rooteventId).client—'portal' | 'matrix' | 'slack'.wsId— optional WebSocket connection ID.requestId— correlation ID.roomId— optional Matrix room ID for the active conversation.
history
messages— readonly array ofBaseMessage(LangChain). The full thread history loaded by the checkpointer.recent(n)— convenience method returning the most recentnmessages.userContext— enrichment object fromstate.userContext(typically populated by the Memory plugin).state—ReadonlyState, a typed view over the LangGraph annotation state. See State schema.
config
Same merged + validated env as PluginContext.config. Typed by your plugin's own schema:
const units = configSchema.parse(rtCtx.config).WEATHER_DEFAULT_UNITS;availablePlugins
The names of every plugin that survived boot resolution. Fixed.
loadedPlugins
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.
secrets
Per-room secrets, JWE-encrypted, 24h cache.
getIndex()— returns theSecretIndex(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.
blobStore
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 freshblob_<16 hex>ID. TTL defaults to 1h and is clamped to the service max (24h). PassuserDidfrom 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. Returnsnullif 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 });matrix
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
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.opts.canis the ability the invocation claims (default'*');opts.skipCachebypasses the invocation cache, required for services that enforce single-use replay protection per invocation CID.
Claim the ability the user's delegation actually grants. A claim resolves against a delegation only when the granted ability is '*', equals the claim, or is a prefix/* covering it. So the default '*' claim is satisfiable only by a '*' grant — if the user granted memory/*, a '*' claim is an over-claim and the service refuses it:
// ✅ delegation grants { can: 'memory/*', with: 'ixo:memory' }
await rtCtx.ucan.mintInvocation(
{ did: memoryDid, capability: 'ixo:memory' },
{ can: 'memory/*' },
);The service must also register that ability: it matches an invocation's can by strict equality, so one that only defines '*' rejects a memory/* invocation as an unknown capability before authorization is considered. Roll out the service side first.
resolveServiceDid(serviceUrl)— look up a downstream service's DID document; returnsidornull.hasSigningKey()—trueonce 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.invocationllm
get(role, params?)— returns aBaseChatModelfor 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.
emit
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.
logger
Same as PluginContext.logger. Plugin-scoped, auto-prefixed with the plugin name.
abortSignal
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 });toolCallId
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.