Graph state schema

MainAgentGraphState is the LangGraph state that flows through every node. Fields, reducers, the loadedPlugins addition, and what plugins may read.

Overview

MainAgentGraphState is a LangGraph annotation-based state. Every node in the graph reads it and may return a partial update; the runtime merges updates via per-field reducers and checkpoints the result to per-user SQLite (backed by Matrix).

import { Annotation, MessagesAnnotation } from '@langchain/langgraph';

export const MainAgentGraphState = Annotation.Root({
  messages: MessagesAnnotation.spec.messages,
  config: Annotation<{ wsId?: string; did: string }>({ /* ... */ }),
  client: Annotation<'portal' | 'matrix' | 'slack'>({ /* ... */ }),
  editorRoomId: Annotation<string | undefined>({ /* ... */ }),
  spaceId: Annotation<string | undefined>({ /* ... */ }),
  currentEntityDid: Annotation<string | undefined>({ /* ... */ }),
  browserTools: Annotation<BrowserToolCall[] | undefined>({ /* default: () => [] */ }),
  agActions: Annotation<AgAction[] | undefined>({ /* default: () => [] */ }),
  userContext: Annotation<UserContextData>({ /* ... */ }),
  userPreferences: Annotation<UserPreferences | undefined>({ /* ... */ }),
  loadedPlugins: Annotation<string[]>({
    reducer: (current, update) =>
      Array.from(new Set([...(current ?? []), ...(update ?? [])])),
    default: () => [],
  }),
});

Plugins read state via rtCtx.history.state (typed as ReadonlyState) or via specific helpers like rtCtx.history.messages / rtCtx.history.userContext.

Fields

messages
  • Type: LangChain BaseMessage[]
  • Reducer: MessagesAnnotation default (append, dedupe by id).
  • Owner: runtime + agent loop.

The thread's full message history. Plugins read it via rtCtx.history.messages (readonly) or rtCtx.history.recent(n).

config
  • Type: { wsId?: string; did: string }
  • Owner: runtime.

Per-request runtime config — primarily the user's DID and the WebSocket connection ID.

client
  • Type: 'portal' | 'matrix' | 'slack'
  • Owner: runtime.

Which client surface this turn arrived through. Same value plugins read via rtCtx.session.client.

editorRoomId
  • Type: string | undefined
  • Owner: Editor plugin.

The active BlockNote room for editor sub-agent work.

spaceId
  • Type: string | undefined
  • Owner: runtime / plugins.

The active workspace/space ID, when relevant.

currentEntityDid
  • Type: string | undefined
  • Owner: Domain Indexer plugin.

The IXO entity DID currently in focus, when set by a domain lookup.

browserTools
  • Type: BrowserToolCall[] | undefined
  • Default: [] (empty array).
  • Owner: Portal client.

Browser tools declared by the Portal frontend for this turn. Each entry is { name, description, schema }. The Portal plugin's sub-agent only builds when this array is non-empty.

agActions
  • Type: AgAction[] | undefined
  • Default: [] (empty array).
  • Owner: Portal client (AG-UI).

AG-UI actions declared by the frontend. Each entry is { name, description, schema, hasRender? }. The AG-UI plugin's sub-agent only builds when this is non-empty.

userContext
  • Type: UserContextData (Record<string, unknown>)
  • Owner: Memory plugin (writes via enrichment middleware).

Memory-enriched user profile. Other plugins read via rtCtx.shared.userProfile (registered by Memory's getSharedState).

userPreferences
  • Type: UserPreferences | undefined
  • Owner: user-preferences plugin.

Behavioural preferences (tone, format, length) injected into the prompt.

loadedPlugins
  • Type: string[]
  • Reducer: union via Set (deduplicating).
  • Default: [].
  • Owner: runtime — written by the load_capability meta-tool.

The names of on-demand plugins the agent has loaded for this thread. Monotonically growing across turns. Cleared on new thread.

This is the single new field the plugin runtime added to the state — every other field above pre-dates the plugin rewrite.

ReadonlyState

Plugins access the state via rtCtx.history.state, which is typed as ReadonlyState:

export interface ReadonlyState {
  readonly messages: readonly BaseMessage[];
  readonly userContext?: UserContextData;
  readonly loadedPlugins?: ReadonlySet<string>;
  readonly [key: string]: unknown;
}

The full annotation state is open-ended ([key: string]: unknown), so plugins can read field names they know exist but the type doesn't enforce it. For fully-typed reads, declare the field on SharedAccessors via shared state, or check existence at runtime.

Reducers

Each field has a reducer that merges partial updates from agent nodes:

  • messages — append + dedupe by ID (LangGraph default for the messages channel).
  • loadedPlugins — union via Set; never removes.
  • Most other fields use last-write-wins or "merge if present" semantics; consult the source for the exact reducer when authoring middleware that mutate state.

Checkpointing

State is checkpointed per thread to per-user SQLite via UserMatrixSqliteSyncService + SqliteSaver. The DB lives under SQLITE_DATABASE_PATH, synced to Matrix in the background.

The runtime exposes hooks.checkpointerForUser(userDid) so hosts can override the default per-user checkpointer with an alternate implementation — pass via createOracleApp({ hooks }).