> ## 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.

# 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).

```ts theme={"system"}
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

<AccordionGroup>
  <Accordion title="messages" icon="comments">
    * **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)`.
  </Accordion>

  <Accordion title="config" icon="cog">
    * **Type:** `{ wsId?: string; did: string }`
    * **Owner:** runtime.

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

  <Accordion title="client" icon="display">
    * **Type:** `'portal' | 'matrix' | 'slack'`
    * **Owner:** runtime.

    Which client surface this turn arrived through. Same value plugins read via `rtCtx.session.client`.
  </Accordion>

  <Accordion title="editorRoomId" icon="pen-to-square">
    * **Type:** `string | undefined`
    * **Owner:** Editor plugin.

    The active BlockNote room for editor sub-agent work.
  </Accordion>

  <Accordion title="spaceId" icon="layer-group">
    * **Type:** `string | undefined`
    * **Owner:** runtime / plugins.

    The active workspace/space ID, when relevant.
  </Accordion>

  <Accordion title="currentEntityDid" icon="id-badge">
    * **Type:** `string | undefined`
    * **Owner:** Domain Indexer plugin.

    The IXO entity DID currently in focus, when set by a domain lookup.
  </Accordion>

  <Accordion title="browserTools" icon="browser">
    * **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.
  </Accordion>

  <Accordion title="agActions" icon="table-cells">
    * **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.
  </Accordion>

  <Accordion title="userContext" icon="user">
    * **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`).
  </Accordion>

  <Accordion title="userPreferences" icon="sliders">
    * **Type:** `UserPreferences | undefined`
    * **Owner:** user-preferences plugin.

    Behavioural preferences (tone, format, length) injected into the prompt.
  </Accordion>

  <Accordion title="loadedPlugins" icon="boxes-stacked">
    * **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.
  </Accordion>
</AccordionGroup>

## ReadonlyState

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

```ts theme={"system"}
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.

<Warning>
  Plugin middleware hooks must **not** return a partial state object (`{ messages: ... }`, etc.). A middleware hook returns only `undefined` (pass through) or `{ jumpTo: 'end' as const }` (flow control). Returning a state channel from `beforeAgent` / `wrapModelCall` / `afterModel` breaks LangGraph checkpointer thread continuity (the symptom is "a new thread per message"). Message rewrites belong in the transport/messages layer, not in a middleware.
</Warning>

## 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 })`.

## Related references

* [RuntimeContext](/build-an-oracle/reference/runtime-context) — `rtCtx.history` field.
* [Meta-tools concept](/build-an-oracle/understand/meta-tools-and-loading) — how `loadedPlugins` is populated.
