> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xtrace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agentic memory search

> Agentic search over the memory pool.

Pipeline: per-corpus vector retrieval → (``mode=compose`` only)
LLM context selection. ``data: list[Memory]`` is always returned;
``mode=compose`` additionally assembles a markdown block into
``context: str``, while ``mode=retrieve`` stops after retrieval
and leaves ``context`` null. Scope is enforced server-side from the
request's scope axes (``user_id`` / ``group_ids`` / ``agent_id`` /
``app_id``); no caller-supplied filter DSL.



## OpenAPI

````yaml https://api.staging.xtrace.ai/openapi.public.json post /v1/memories/search
openapi: 3.1.0
info:
  title: XTrace Vec DB
  description: XTrace API
  version: 1.0.0
servers:
  - url: https://api.production.xtrace.ai
    description: Production
security:
  - ApiKeyHeader: []
  - BearerToken: []
paths:
  /v1/memories/search:
    post:
      tags:
        - memories
      summary: Agentic memory search
      description: |-
        Agentic search over the memory pool.

        Pipeline: per-corpus vector retrieval → (``mode=compose`` only)
        LLM context selection. ``data: list[Memory]`` is always returned;
        ``mode=compose`` additionally assembles a markdown block into
        ``context: str``, while ``mode=retrieve`` stops after retrieval
        and leaves ``context`` null. Scope is enforced server-side from the
        request's scope axes (``user_id`` / ``group_ids`` / ``agent_id`` /
        ``app_id``); no caller-supplied filter DSL.
      operationId: search_memory_v1_memories_search_post
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '401':
          description: >-
            Authentication failed. Missing / invalid API key. Body carries
            `detail.code = "unauthorized"`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: Validation error — invalid query, user_id, mode, or include.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: >-
            Rate-limit or daily-cap exceeded. `Retry-After` and `RateLimit-*`
            response headers indicate when to retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: >-
            `detail.code = "search_failed"` — store init, retrieval, or
            row-build failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
      x-codeSamples:
        - lang: typescript
          label: TypeScript SDK
          source: |
            import { MemoryClient } from '@xtraceai/memory';

            const client = new MemoryClient({
              apiKey: process.env.XTRACE_API_KEY!,
              orgId:  process.env.XTRACE_ORG_ID!,
            });

            const results = await client.memories.search({
              query: 'what does the user like to eat?',
              filters: { user_id: 'alice' },
              limit: 10,
            });

            for (const m of results.data) {
              console.log(m.score?.toFixed(2), '·', m.text);
            }
components:
  schemas:
    SearchRequest:
      properties:
        query:
          type: string
          maxLength: 4000
          minLength: 1
          title: Query
          description: >-
            Natural-language query text. Embedded server-side; the search
            pipeline ranks and selects.
          examples:
            - who likes thai food?
        user_id:
          anyOf:
            - type: string
            - type: 'null'
          title: User Id
          description: >-
            Scope key. When supplied, baked into the per-request store as an AND
            pin so reads are tenant-isolated at the vector-store filter layer.
            **Optional** on search (unlike ingest): omit it and pass
            ``group_ids`` to read a shared group across users. At least one of
            ``user_id`` / ``agent_id`` / ``app_id`` / ``group_ids`` is required.
            The compat shim also accepts it inside legacy ``filters.user_id``
            and lifts it before field validation runs, so old SDKs that send the
            pre-#68 wire shape don't 422.
          examples:
            - user-123
        mode:
          type: string
          enum:
            - retrieve
            - compose
          title: Mode
          description: >-
            Pipeline depth selector.


            - `compose` (default) — vector retrieval **plus** an LLM
            context-selection step that picks the most relevant subset, then
            assembles it into a markdown block. `data` carries the selected
            rows; `context` carries the assembled markdown. One LLM call.

            - `retrieve` — vector retrieval only. No LLM, no agent, cheaper and
            faster. `data` carries the raw ranked candidates (the unfiltered
            set); `context` is null.


            `data` is populated in both modes; under `compose` it's the
            LLM-selected subset, under `retrieve` it's the raw candidate set.
          default: compose
        include:
          items:
            type: string
            enum:
              - fact
              - artifact
              - episode
          type: array
          uniqueItems: true
          title: Include
          description: >-
            Which corpora to search. Subset to restrict — e.g. `["fact"]` for
            facts-only. Default is all three.
        group_ids:
          items:
            type: string
          type: array
          title: Group Ids
          description: >-
            Optional group tags — another AND scope axis. When non-empty,
            candidate rows must be tagged to at least one of the requested
            group(s):

              `org [ AND user_id ] AND ( group_ids ∩ <group_ids> ) [ AND agent_id ] [ AND app_id ]`

            Group membership (the `∩`) is OR / any-of across the list — pass
            `[trip_tokyo, trip_paris]` to span both trips.


            How it composes with `user_id` ("scope by what you pass"):


            - **omit `user_id`, pass `group_ids`** — the cross-user whole-group
            read: every user's rows tagged to those group(s). A traveler's AI
            sees the whole trip's shared facts, from any traveler.

            - **pass both `user_id` and `group_ids`** — the intersection: only
            the caller's own rows that are also tagged to those group(s) (the
            caller's slice of the group).


            (`agent_id` / `app_id` AND on top when set — they narrow further.)
            Group ids are server-generated unguessable handles (see `POST
            /v1/groups`), so knowing the id is the access boundary; other users'
            *untagged* memories never surface.
          examples:
            - - grp_a1b2c3d4e5f6071829304a5b6c7d8e9f
        agent_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Agent Id
          description: >-
            Optional agent scope. When set, ANDs onto the active primary scope
            (whether that's `user_id` or `group_ids`): candidate rows must also
            carry this exact `agent_id`. Use it to narrow a search to one
            agent's contributions. Indexed payload key, same axis ingest stamps.
          examples:
            - bot-7
        app_id:
          anyOf:
            - type: string
            - type: 'null'
          title: App Id
          description: >-
            Optional app scope. When set, ANDs onto the active primary scope
            (like `agent_id`): candidate rows must also carry this exact
            `app_id`. Indexed payload key, same axis ingest stamps.
          examples:
            - app-3
      type: object
      required:
        - query
      title: SearchRequest
      description: >-
        POST /v1/memories/search — agentic memory search.


        The pipeline has two phases:


        1. **Retrieve** — embed the query, fetch per-corpus vector
           candidates, optionally rerank. Output: a ranked set of
           :class:`Memory` rows.
        2. **Compose** *(only when ``mode='compose'``)* — an LLM
           context-selection step picks the most relevant subset of the
           candidates and weaves them into a markdown block ready to drop
           into an LLM prompt.

        ``mode`` toggles whether phase 2 runs:


        - ``"compose"`` (default) → ``data`` populated with rows **and**
          ``context`` populated with the assembled markdown. The dominant
          use case (memory search → LLM prompt) gets the prompt-ready
          blob without an opt-in.
        - ``"retrieve"`` → ``data`` populated, ``context`` null. Skips
          the LLM compose step (cheaper, faster); for callers building
          their own UI or doing custom downstream processing.

        Scope is enforced server-side by the per-request store and

        follows "scope by what you

        pass": every scope axis you supply ANDs; an axis you omit is left

        unconstrained. ``org_id`` is derived server-side from your API key. At
        least one of

        ``user_id`` / ``agent_id`` / ``app_id`` / ``group_ids`` must be

        supplied — an unscoped org-wide search is rejected. ``user_id`` is

        optional here (unlike ingest, where it is required): omit it and

        pass ``group_ids`` to read a shared group across users. No filter

        DSL — the four scope axes are the only narrowing.


        **Legacy compatibility** (undocumented in the public spec, kept

        so existing SDK installations don't break):


        - ``filters: {user_id: "X", ...}`` — accepted; ``user_id`` is
          lifted out of ``filters`` if absent at the body root. Other
          filter keys are silently dropped (the new shape doesn't support
          a general filter DSL).
        - ``mode: "rows"`` → translated to ``"retrieve"``.

        - ``mode: "context"`` → translated to ``"compose"``.

        - ``include: ["context_prompt"]`` → sets ``mode="compose"`` and
          drops the value. ``"full_content"`` is dropped silently.

        These translations are intentionally invisible on the wire — the

        public spec only advertises the new shape — and will be removed

        once consumers have migrated.
    SearchResponse:
      properties:
        object:
          type: string
          const: search
          title: Object
          description: Constant discriminator for the resource type.
          default: search
        mode:
          type: string
          enum:
            - retrieve
            - compose
          title: Mode
          description: >-
            Echoes the request's mode. `compose` ⇒ both `data` and `context`
            populated; `retrieve` ⇒ `data` populated and `context` null.
        data:
          items:
            $ref: '#/components/schemas/Memory'
          type: array
          title: Data
          description: >-
            Ranked Memory rows from retrieval. Populated for both modes.
            Semantic rows (`fact`/`artifact`/`episode`) are ordered by `score`
            desc; when `include` requests `lesson`/`procedure`, those directive
            rows are precision-gated (not vector-scored, `score` is null) and
            lead the list ahead of the semantic rows. For `mode=compose`, the
            semantic rows are the candidates considered by the context-selection
            step — a (possibly larger) superset of what ended up in `context`.
        context:
          anyOf:
            - type: string
            - type: 'null'
          title: Context
          description: >-
            Assembled markdown context block ready for prompt insertion.
            Populated only when `mode=compose`; null otherwise.
        stage_timings:
          additionalProperties:
            type: number
          type: object
          title: Stage Timings
          description: >-
            Per-stage retrieval-pipeline latencies (seconds). Populated in both
            modes.
        context_selection_applied:
          type: boolean
          title: Context Selection Applied
          description: >-
            True when the LLM context-selection step ran. False indicates the
            pipeline fell through (e.g. an empty candidate pool, or context
            selection disabled by config).
          default: false
      type: object
      required:
        - mode
      title: SearchResponse
      description: |-
        POST /v1/memories/search response.

        ``data`` is **always** populated — both modes return the ranked
        Memory rows from retrieval. ``context`` is populated **only when
        ``mode='compose'``** with the assembled markdown block from the
        LLM context-selection step. ``stage_timings`` is always present
        for per-stage latency attribution.

        Note that under ``mode='compose'`` the rows in ``data`` are the
        **retrieval candidates** — a possibly larger set than the subset
        the LLM selected and wove into ``context``. Useful for showing
        "everything we found" alongside "what we sent to the model."
    ErrorEnvelope:
      properties:
        detail:
          $ref: '#/components/schemas/ErrorDetail'
      type: object
      required:
        - detail
      title: ErrorEnvelope
      description: |-
        Standard error body for non-2xx responses raised by the memory
        API. Pydantic field-validation failures (422) use the FastAPI-
        default ``HTTPValidationError`` shape instead, where ``detail`` is
        an array of per-field error entries — switch on the response
        status code to pick the right shape.
    Memory:
      properties:
        id:
          type: string
          title: Id
          description: Stable UUID for this memory row.
        object:
          type: string
          const: memory
          title: Object
          description: Constant discriminator for the resource type.
          default: memory
        type:
          type: string
          enum:
            - fact
            - artifact
            - episode
            - lesson
            - procedure
          title: Type
          description: >-
            Memory subtype. `fact` = a single semantic claim extracted from a
            turn; `artifact` = a structured object (code, doc, image) referenced
            by the conversation; `episode` = a session-scoped summary of a
            stretch of turns; `lesson` / `procedure` = a situated directive
            recalled by the symbol tripwire (see `DirectiveDetails`).
        text:
          type: string
          title: Text
          description: >-
            Short readable preview. For facts: the claim statement. For
            artifacts: a summary or title (full body lives in
            `details.full_content`, opt-in via `include=full_content`). For
            episodes: a summary of the session.
        user_id:
          anyOf:
            - type: string
            - type: 'null'
          title: User Id
          description: User scope this row belongs to.
        agent_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Agent Id
          description: Agent scope, if any.
        conv_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Conv Id
          description: Conversation anchor.
        app_id:
          anyOf:
            - type: string
            - type: 'null'
          title: App Id
          description: App scope, if any.
        group_ids:
          items:
            type: string
          type: array
          title: Group Ids
          description: >-
            Group ids associated with this row — the sharing axis. Stamped at
            ingest time and editable afterward via `PATCH /v1/memories/{id}`
            (`add_group_ids` / `remove_group_ids`). Reachable as a filter axis
            via `filters: {group_ids: <id>}` or `{group_ids: {"$in": [<id>,
            ...]}}`.
        categories:
          items:
            type: string
          type: array
          title: Categories
          description: Optional category labels from the extraction pipeline.
        score:
          anyOf:
            - type: number
            - type: 'null'
          title: Score
          description: >-
            Vector-similarity score. Present **only** on search responses; null
            on list / get-by-id / patch / ingest result rows.
        created_at:
          anyOf:
            - type: string
            - type: 'null'
          format: date-time
          title: Created At
          description: ISO-8601 timestamp of original ingest.
        updated_at:
          anyOf:
            - type: string
            - type: 'null'
          format: date-time
          title: Updated At
          description: ISO-8601 timestamp of the last supersede / consolidation.
        details:
          oneOf:
            - $ref: '#/components/schemas/FactDetails'
            - $ref: '#/components/schemas/ArtifactDetails'
            - $ref: '#/components/schemas/EpisodeDetails'
            - 0dc16cfe-7f7d-4424-b72b-94b908c810ef
          type: object
          title: Details
          description: >-
            Type-specific extension. Shape depends on `type` — see `FactDetails`
            / `ArtifactDetails` / `EpisodeDetails` schemas. Always an object;
            never null.
      type: object
      required:
        - id
        - type
        - text
      title: Memory
      description: |-
        Unified memory resource — facts, artifacts, episodes marshal to
        this shape. ``type`` is the discriminator; ``details`` is the
        type-specific extension.

        Invariant: ``text`` is always a short readable preview, regardless
        of type (fact statement / artifact summary / episode summary).
    ErrorDetail:
      properties:
        code:
          type: string
          title: Code
          description: >-
            Stable error identifier. Switch on this rather than parsing the
            message. Common values: `invalid_request`, `invalid_messages`,
            `memory_not_found`, `job_not_found`, `immutable_field`,
            `reserved_field`, `empty_text_field`, `missing_user_id`,
            `missing_conv_id`, `unsupported_include_option`, `cursor_mismatch`,
            `unauthorized`, `forbidden`, `rate_limited`, `delete_failed`,
            `search_failed`, `ingest_failed`, `server_error`.
          examples:
            - memory_not_found
        message:
          type: string
          title: Message
          description: Human-readable summary; safe to log but not safe to switch on.
          examples:
            - Memory 0fa1c0e6-... not found
      type: object
      required:
        - code
        - message
      title: ErrorDetail
      description: |-
        Inner ``detail`` block on every non-2xx response raised by a
        memory-API route handler. ``code`` is the stable identifier
        clients should switch on; ``message`` is a sanitized human-
        readable summary.
    FactDetails:
      description: |-
        Per-row fact details — sits under ``Memory.details`` when
        ``Memory.type == "fact"``.
      properties:
        fact_type:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Fact Type
        status:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Status
        supersedes:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Supersedes
        source_role:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Source Role
        episode_id:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Episode Id
        artifact_id:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Artifact Id
        artifact_ids:
          items:
            type: string
          title: Artifact Ids
          type: array
        source_event_ids:
          items:
            type: string
          title: Source Event Ids
          type: array
      title: FactDetails
      type: object
    ArtifactDetails:
      description: |-
        Per-row artifact details under ``Memory.details`` when
        ``Memory.type == "artifact"``.

        ``full_content`` is opt-in — omitted on list/search by default,
        included on ``GET /v1/memories/{id}`` and when
        ``include=["full_content"]`` is set on list/search.
      properties:
        title:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Title
        rationale:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Rationale
        version:
          anyOf:
            - type: integer
            - type: 'null'
          default: null
          title: Version
        root_id:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Root Id
        source_fact_ids:
          items:
            type: string
          title: Source Fact Ids
          type: array
        episode_ids:
          items:
            type: string
          title: Episode Ids
          type: array
        full_content:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Full Content
      title: ArtifactDetails
      type: object
    EpisodeDetails:
      description: |-
        Per-row episode details under ``Memory.details`` when
        ``Memory.type == "episode"``.
      properties:
        title:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Title
        started_at:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Started At
        ended_at:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Ended At
        fact_ids:
          items:
            type: string
          title: Fact Ids
          type: array
        artifact_ids:
          items:
            type: string
          title: Artifact Ids
          type: array
      title: EpisodeDetails
      type: object
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: x-api-key
      description: 'Long-lived org API key. Alternative: `Authorization: Bearer <key>`.'
    BearerToken:
      type: http
      scheme: bearer
      bearerFormat: Token
      description: 'Long-lived API key sent as `Authorization: Bearer <api-key>`.'

````