Skip to main content
The @xtraceai/memory package is the primary supported client. It’s a hand-written wrapper over the HTTP API with idiomatic TypeScript types, an exponential-backoff polling helper, async-iterator pagination, and a typed error hierarchy.
npm install @xtraceai/memory
Zero runtime dependencies. Node 18+ (native fetch). Works in the browser too.

MemoryClient

The entry point. One client serves the whole org.
import { MemoryClient } from '@xtraceai/memory';

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

Constructor options

OptionTypeRequiredDefaultNotes
apiKeystringxtk_... API key
orgIdstringOrganization id, sent as X-Org-Id header
baseUrlstringhttps://api.production.xtrace.aiOverride for staging or self-hosted: e.g. https://api.staging.xtrace.ai
fetchtypeof fetchglobalThis.fetchInject a custom fetch (tests, polyfills, instrumentation)
maxRetriesnumber2Retries on 5xx (idempotent methods) and 429 (any method); honors Retry-After
requestIdFactory() => stringreq_<uuid>Override the X-Request-Id generator

client.memories

All memory operations live here. Returns a Memories instance.

ingest(body, options?)

Submit conversation messages for extraction. Returns an IngestJob.
const job = await client.memories.ingest({
  messages: [
    { role: 'user', content: 'My favorite food is pad see ew.' },
    { role: 'assistant', content: 'Noted.' },
  ],
  user_id: 'alice',
  conv_id: 'conv_2026_05_16',
});
Required body fields: messages, user_id, conv_id. Optional: agent_id, app_id, group_ids (tag the extracted memories to groups), timestamp_format, extract_artifacts (defaults to true — pass false to skip the artifact-extraction stage). Options:
  • wait?: boolean — if true, the server holds the connection up to 30s and returns a terminal job inline. Falls back to async if extraction is still running at 30s.
  • signal?: AbortSignal
  • requestId?: string

list(query?)AsyncIterable<Memory>

Auto-paginating async iterator over memories matching the query.
for await (const memory of client.memories.list({ user_id: 'alice' })) {
  console.log(memory.text);
}
Filter keys (all optional): user_id, agent_id, conv_id, app_id, type, limit, order, include.

listPage(query?)Promise<ListEnvelope<Memory>>

Single-page version of list. Use when you need cursor-level control.
const page = await client.memories.listPage({ user_id: 'alice', limit: 50, cursor });
// page.data, page.has_more, page.next_cursor

get(id)Promise<Memory>

Fetch a single memory by id. Returns the full row, including details.full_content for artifacts.
const memory = await client.memories.get('5b0d0f7d-d502-...');

delete(id)Promise<void>

Hard delete. Removes the point outright — afterwards get 404s, it’s gone from list/search, and a second delete 404s (idempotent by absence). There is no update method: corrections flow through ingest (re-ingesting the corrected statement supersedes the old one).
await client.memories.delete('5b0d0f7d-d502-...');

search(body)Promise<SearchListEnvelope>

Vector search, scoped by what you pass (user_id / group_ids / agent_id / app_id — all AND-narrow; at least one required). mode defaults to compose.
const results = await client.memories.search({
  query: 'what does the user like to eat?',
  user_id: 'alice',
  limit: 10,
});
// results.data — ranked rows;  results.context — assembled prompt when mode==='compose'
See Searching memories for scoping, modes, and groups.

retrieve(body)Promise<SearchListEnvelope>

Sugar over search that forces mode: 'compose' — the response’s context carries the LLM-assembled, ready-to-inject prompt.

recall(params, options?)Promise<RecallResult>

Personal + shared (group) read in one call. Fans out a search per scope, dedupes by id, and renders a single prompt sectioned by Personal + group name (shared lines attributed to their author). This is the combined read that AND-scoping can’t express in a single search.
const { prompt, memories, scopes } = await client.memories.recall({
  query: 'what should we plan for dinner on the trip?',
  pools: [
    { user_id: 'alice' },             // personal scope
    { group_ids: ['grp_tokyo2026'] }, // shared scope (any-of)
  ],
});
// inject `prompt`; `memories` is the deduped, score-ranked union; `scopes` is per-pool counts
Axes AND within a pool; pools OR. So [{ user_id }, { app_id: 'product-kb' }] reads “alice’s memories OR the product KB,” while { user_id, app_id } in one pool would AND them. Params: query (required) and pools (≥1 pool — each a ScopePool of { user_id?, group_ids?, agent_id?, app_id? }); optional mode (default compose), limit (default 10). Options: template? (override the prompt format — see PromptTemplate), render? (supply your own renderer), signal?, requestId?. renderMemoriesPrompt(memories, opts?) is also exported standalone if you want to format a memory list yourself.

client.memories.jobs

get(jobId)Promise<IngestJob>

Poll a single ingest job. Use pollUntilDone instead for normal flows.

pollUntilDone(jobId, options?)Promise<IngestJob>

Polls a job until it reaches succeeded or failed. Exponential backoff starting at 500ms, capped at 5s, default 60s timeout.
const done = await client.memories.jobs.pollUntilDone(job.id);
if (done.status === 'failed') throw new Error(done.error?.message);
console.log(done.result?.memories_created);
Options:
  • timeoutMs?: number — default 60_000
  • initialIntervalMs?: number — default 500
  • maxIntervalMs?: number — default 5_000
  • backoffFactor?: number — default 1.5
  • signal?: AbortSignal

client.groups

Register and manage groups — shared tagging targets. Register a group, then pass its id in ingest({ group_ids }) and read it back with search/recall. See the Groups guide.
const trip = await client.groups.create({
  name: 'Tokyo trip 2026',
  prompt: 'Facts about the Tokyo trip: hotels, restaurants, dates, dietary needs.',
});
// trip.id === "grp_…"
MethodReturnsNotes
create({ name, prompt })Groupprompt tells the ingest classifier what belongs to this group
list()Group[]all groups (active + archived)
get(id)Group
update(id, { name?, prompt?, status? })Groupedit / re-prompt; status: 'archived' archives
archive(id)Groupsoft-archive — drops the group from future ingest tagging

Error classes

Every API failure is a subclass of MemoryError. Match on the class for HTTP-status handling and on .code for stable machine-readable error codes from the server.
import { MemoryNotFound, RateLimited, MemoryError } from '@xtraceai/memory';

try {
  await client.memories.get('does-not-exist');
} catch (err) {
  if (err instanceof MemoryNotFound) {
    // 404
  } else if (err instanceof RateLimited) {
    console.log('retry after', err.retryAfter, 'seconds');
  } else if (err instanceof MemoryError) {
    console.log(err.status, err.code, err.message);
  }
}
ClassHTTP status
BadRequest400
Unauthorized401
Forbidden403
MemoryNotFound404
Conflict409
Unprocessable422
RateLimited429 (adds .retryAfter: number)
ServerError5xx
Common fields on every error:
  • status: number — HTTP status code
  • code: string — stable machine-readable error code (e.g. memory_not_found, org_mismatch)
  • errorType: string — category (e.g. invalid_request_error)
  • requestId: string | undefined — propagate to support / logs
  • details: Record<string, unknown> | undefined — error-specific extras

Type exports

The full set of exported types:
import type {
  // Resources
  Memory,
  FactMemory,
  ArtifactMemory,
  EpisodeMemory,
  FactDetails,
  ArtifactDetails,
  EpisodeDetails,
  MemoryRef,
  MemoryType,
  MemoryStatus,

  // Ingest
  Message,
  Role,
  IngestRequest,
  IngestJob,
  IngestJobResult,
  JobStatus,

  // Read
  ListQuery,
  ListEnvelope,
  SearchRequest,
  SearchListEnvelope,
  SearchMode,

  // Recall + prompt rendering
  RecallParams,
  RecallResult,
  RecallScopeStat,
  ScopePool,
  PromptTemplate,

  // Groups
  Group,
  GroupStatus,
  GroupCreateRequest,
  GroupUpdateRequest,
  GroupListEnvelope,

  // Errors
  ApiErrorBody,
} from '@xtraceai/memory';
renderMemoriesPrompt and DEFAULT_PROMPT_TEMPLATE are exported as values (not types):
import { renderMemoriesPrompt, DEFAULT_PROMPT_TEMPLATE } from '@xtraceai/memory';
Memory is a discriminated union — m.type === 'fact' narrows m.details to FactDetails, etc.

Retries and timeouts

WhatDefaultOverride
Max retries2new MemoryClient({ maxRetries: 5 })
Retry-eligible methods on 5xxGET, HEAD onlyn/a
Retry-eligible methods on 429anyn/a — always retried, honoring Retry-After
Backoff250ms · 500ms · 1s · … capped at 5s, with jittern/a
Network errors on idempotent methods also retry.

Request ids

Every request carries an X-Request-Id header (auto-generated req_<uuid> by default). The server echoes it; the SDK surfaces it on errors via err.requestId. Use it when filing support tickets — it pins down the exact request in our logs.

See also