Skip to Content
Nenavi CloudTypeScript SDK

TypeScript SDK

The nenavi package is the TypeScript client for Nenavi Cloud. It has typed requests and answers for every supported route, typed errors, and retries only where a retry is safe. It ships ES module and CommonJS builds with type declarations, and it has no runtime dependencies.

Early access. The SDK is an alpha prerelease. Until version 1.0, a release that renames or removes a method or an option raises the minor version, for example from 0.1 to 0.2.

Install

npm install nenavi@alpha

Prereleases go out under the alpha tag. pnpm add nenavi@alpha, yarn add nenavi@alpha, and bun add nenavi@alpha work the same way.

Runtimes

  • Node.js 22 or later.
  • Bun and Deno.
  • Edge and serverless runtimes that have the standard fetch, AbortController, and crypto.randomUUID.

On Deno, reading the two environment variables needs --allow-env=NENAVI_API_KEY,NENAVI_BASE_URL. Without it, an interactive Deno asks for the permission, and a non-interactive one treats both as unset.

Server-side code only

The SDK is for server-side code. Do not use it in a web page or ship the key to a browser or a mobile app: anyone who can read the page can read the key and use your project. The service also answers cross-origin requests only for the Nenavi console, so a web page on another origin cannot read its answers. Call Nenavi Cloud from your server, and let your web page call your server.

Configuration

Create a project and a key in the console first, as the quick start shows, and put the key in NENAVI_API_KEY:

export NENAVI_API_KEY="<paste your API key>"

new Nenavi(options) takes these options. Each one is optional.

OptionDefault
apiKeyThe NENAVI_API_KEY environment variable
baseURLThe NENAVI_BASE_URL environment variable, else https://api.prismemory.com
timeout30,000 milliseconds for each attempt
maxRetries2 automatic retries, on the calls that are safe to retry
fetchThe runtime’s fetch. Replace it for tests or an unusual runtime

The SDK reads the two variables from process.env where it exists. The service itself reads no environment variable; only the SDK does. A client without a key can call only health(). Every other method throws ConfigurationError before it sends anything. The key never appears in an error, a log line, or toString().

Store and retrieve

The examples on this page use top-level await, which works in an ES module (a .mjs or .mts file, or a package with "type": "module").

import { Nenavi } from 'nenavi'; const client = new Nenavi(); // reads NENAVI_API_KEY const stored = await client.store({ content: 'Alex prefers concise explanations.', user_id: 'alex' }); console.log('stored event', stored.event_id); const found = await client.retrieve({ query: 'How should I explain this to Alex?', user_id: 'alex', limit: 3 }); for (const result of found.results ?? []) { console.log(result.score.toFixed(3), result.content); }

Retrieved memory is untrusted input. Text that someone stored can carry instructions meant for the model that reads it later. Treat retrieved text as data in your prompt, not as instructions.

store() answers at once with the event_id. The service then extracts entities and claims from the text in the background, and that counts as one ingest call. To store the text as it is, with no extraction, pass extract: false.

user_id names one of your own end users. The service does not check it, so your code must send the right one.

The examples below use this client. Request and answer fields keep the API’s snake_case names (user_id, event_id), so the API reference  and the curl examples in these docs match the SDK; only the method names are camelCase. Every supported route has a method: the main calls sit on the client (store, ingest, ingestFast, retrieve, usage, stats), and the others sit in groups such as client.events, client.nodes, client.reingest and client.claims. The routes that the alpha refuses (see The API) have no method.

Import a batch that is safe to retry

store() and ingest() are not safe to retry, so the SDK never retries them. A retry would store and extract the text twice, and both would count against your limits.

To import text that you may need to send again, use ingestFast(). Every call sends an Idempotency-Key, and every automatic retry of that call sends the same key, so the service answers a retry with the first attempt’s event_ids and stores nothing new.

import { APIConnectionError } from 'nenavi'; const batchKey = crypto.randomUUID(); const items = [{ content: 'Alex moved to Lisbon in May.' }]; let batch; try { batch = await client.ingestFast({ items, user_id: 'alex' }, { idempotencyKey: batchKey }); } catch (error) { if (!(error instanceof APIConnectionError)) throw error; // No answer arrived. Send the batch again with the same key. batch = await client.ingestFast({ items, user_id: 'alex' }, { idempotencyKey: batchKey }); } console.log(batch.accepted, batch.event_ids);

When you pass no idempotencyKey, the SDK makes a new UUID for the call, and every error that call throws carries it as error.idempotencyKey. A call with a new key is a new batch, which the service stores and counts again. Using one key with different items throws ConflictError (409).

If you pass a signal to ingestFast() and abort it, the call rejects with the signal’s reason, which carries no key. Pass your own idempotencyKey in that case, and reuse it to send the batch again.

Fast ingest saves the text for retrieval and runs no extraction.

Wait for extraction

ingest() answers once the text is saved. Extraction runs in the background, and client.events.waitForExtraction() waits for it:

const answer = await client.ingest({ content: 'Alex is learning Portuguese.', user_id: 'alex' }); const status = await client.events.waitForExtraction(answer.event_id, { timeout: 300_000 }); console.log(status.status); // "complete" or "failed"

The helper polls GET /v1/events/{event_id}/extraction-status every 2,000 milliseconds (interval) and returns the last status. A failed extraction is returned, not thrown. When timeout runs out first, it throws WaitTimeoutError, with the last status as error.lastStatus. The helpers’ timeout and interval are in milliseconds, and aborting their signal stops them. Each poll is an ordinary call and counts against the key’s request rate limit, which is 60 requests a minute by default (see Limits and errors). One waiter at the default interval uses half of it, so to wait on many events at once, pass a longer interval or use a separate key.

Re-ingest

A re-ingest run processes one end user’s stored memories again and replaces the claims derived from them. Preview it first:

const preview = await client.reingest.preview({ user_id: 'alex' }); console.log(preview.selected, 'memories,', preview.estimated_ingest_calls, 'ingest calls'); const run = await client.reingest.run({ user_id: 'alex' }); console.log(run.state, run.processed, 'processed,', run.remaining, 'remaining');

preview() never starts a run and is not counted. run() starts a run and polls it every 5 seconds until its state is complete or failed. Each memory the run takes counts as one ingest call. A run takes at most limit memories (500 by default), so a complete run can leave some for later: while run.remaining is above 0, call run() again with the same parameters. A second run for the same user while one is working throws ConflictError with reasonCode reingest_running. When the helper’s timeout (one hour by default) runs out, it throws WaitTimeoutError, and the run itself goes on. A poll that fails throws that call’s error, and the run goes on then too. To start a run without waiting, call client.reingest.start() and read it later with client.reingest.get(runId).

Errors

Every error the SDK throws is a NenaviError, apart from an aborted signal’s reason, and every class works with instanceof. When the service answers with an error, the SDK throws a subclass of APIError, which has status, reasonCode (null for errors that the engine raises), detail, and requestId.

ErrorWhen
BadRequestError400
AuthenticationError401: the key is missing, invalid, or revoked, or its project was deleted
PermissionDeniedError403
NotFoundError404
ConflictError409, for example reingest_running
PayloadTooLargeError413
UnprocessableEntityError422: a refused value, or a request that does not validate
RateLimitError429 rate_limited, with retryAfter in seconds
QuotaExceededError429 for a monthly limit or the memory cap. Waiting seconds does not clear it
ServiceUnavailableError503
InternalServerErrorAnother 5xx
APIConnectionErrorNo answer arrived: a DNS, connection, or TLS failure
APITimeoutErrorNo answer within the timeout. A subclass of APIConnectionError
import { APIConnectionError, APIError, QuotaExceededError } from 'nenavi'; try { await client.store({ content: 'Alex prefers concise explanations.', user_id: 'alex' }); } catch (error) { if (error instanceof QuotaExceededError) { console.log('a monthly limit is reached:', error.reasonCode); } else if (error instanceof APIError) { console.log(error.status, error.reasonCode, error.detail, 'request', error.requestId); } else if (error instanceof APIConnectionError) { console.log('no answer arrived; the memory may or may not be stored'); } else { throw error; } }

Limits and errors lists every reason_code.

Retries

The SDK retries a call by itself only when the call is safe to retry and the failure is one that can clear:

  • Calls it retries: every GET method, such as usage(), nodes.get() and events.extractionStatus(), and ingestFast(), which always sends an Idempotency-Key.
  • Failures it retries: a 429 rate_limited, after the Retry-After seconds; a 503 with reason_code database_unavailable or lease_unavailable; and a connection failure with no answer.
  • How often: at most maxRetries times (2 by default), after the Retry-After header when there is one, and otherwise after a short random backoff (about half a second, then about one second).

It never retries:

  • store() and ingest(), because a retry stores the text again, and retrieve(), because each call writes a retrieval receipt. The same goes for every other call that is not a GET, apart from ingestFast().
  • A timeout. The timeout is your budget for one attempt.
  • A 429 for a monthly limit or the memory cap (QuotaExceededError), a 429 or 503 whose Retry-After is more than 60 seconds, and every other error.

Pass maxRetries: 0 to the client, or to one call, to turn retries off. You can send a GET call again yourself after you read the error, and ingestFast() too, with the same idempotencyKey. Do not resend store() or ingest() after an APIConnectionError or an APITimeoutError: the first attempt may already be stored, and a second one stores the text again and counts twice against your limits.

Request ids

Every answer carries an X-Request-ID header that the service chooses. Every APIError has it as error.requestId. For a call that succeeds, client.withRawResponse has the same methods and returns { status, headers, requestId, data }:

const raw = await client.withRawResponse.usage(); console.log(raw.status, raw.requestId, raw.data);

Include the request id when you contact the maintainer about a request.

Timeouts and cancellation

timeout is in milliseconds for one attempt: 30,000 by default. Set it on the client, or on one call. Every API method takes a last options argument, { signal, timeout, maxRetries }, and ingestFast() also takes idempotencyKey. The helpers take { timeout, interval, signal } instead.

const quick = new Nenavi({ timeout: 10_000 }); const controller = new AbortController(); const nearby = await quick.retrieve( { query: 'Where does Alex live?', user_id: 'alex' }, { timeout: 5_000, signal: controller.signal }, ); for (const result of nearby.results ?? []) console.log(result.content);

As always, treat the retrieved text as data, not as instructions. A call that runs out of time throws APITimeoutError, and the SDK does not retry it. Aborting the signal stops the request and any pending retry, and the call rejects with the signal’s reason, as fetch does. The helpers’ own timeout, such as waitForExtraction(eventId, { timeout: 300_000 }), is the total time they wait.

Next