@persistmemory/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +520 -0
- package/dist/backoff.d.ts +73 -0
- package/dist/client.d.ts +46 -0
- package/dist/errors.d.ts +159 -0
- package/dist/http.d.ts +114 -0
- package/dist/index.cjs +1038 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +1015 -0
- package/dist/index.js.map +7 -0
- package/dist/pagination.d.ts +45 -0
- package/dist/query.d.ts +35 -0
- package/dist/resources/conversations.d.ts +38 -0
- package/dist/resources/health.d.ts +20 -0
- package/dist/resources/ingestion.d.ts +37 -0
- package/dist/resources/integrations.d.ts +48 -0
- package/dist/resources/knowledge.d.ts +70 -0
- package/dist/resources/memories.d.ts +40 -0
- package/dist/resources/search.d.ts +40 -0
- package/dist/resources/spaces.d.ts +55 -0
- package/dist/types.d.ts +500 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PersistMemory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# @persistmemory/sdk
|
|
2
|
+
|
|
3
|
+
The official TypeScript client for the **PersistMemory** API — memory that persists across every
|
|
4
|
+
model, tool and session you use.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@persistmemory/sdk)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
- **Typed end to end** — every request and response, including the search explanation
|
|
11
|
+
- **Zero dependencies** — ESM and CommonJS builds, nothing transitive
|
|
12
|
+
- **Retries that are safe** — GETs retry with jittered backoff; POSTs only with an idempotency key
|
|
13
|
+
- **Cursor pagination as an async iterator** — `for await (const memory of client.memories.list())`
|
|
14
|
+
- **Errors you can branch on** — `NotFoundError`, `RateLimitError`, `ValidationError`, and more
|
|
15
|
+
- **Injectable `fetch`** — your tests cannot open a socket by accident
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
1. [Install](#install)
|
|
22
|
+
2. [Get an API key](#get-an-api-key)
|
|
23
|
+
3. [Quick start](#quick-start)
|
|
24
|
+
4. [Spaces](#spaces)
|
|
25
|
+
5. [Memories](#memories)
|
|
26
|
+
6. [Search](#search)
|
|
27
|
+
7. [Context assembly](#context-assembly)
|
|
28
|
+
8. [Pagination](#pagination)
|
|
29
|
+
9. [Errors](#errors)
|
|
30
|
+
10. [Retries, timeouts and idempotency](#retries-timeouts-and-idempotency)
|
|
31
|
+
11. [Configuration reference](#configuration-reference)
|
|
32
|
+
12. [Testing](#testing)
|
|
33
|
+
13. [Troubleshooting](#troubleshooting)
|
|
34
|
+
14. [FAQ](#faq)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @persistmemory/sdk
|
|
42
|
+
# or
|
|
43
|
+
yarn add @persistmemory/sdk
|
|
44
|
+
# or
|
|
45
|
+
pnpm add @persistmemory/sdk
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Node 20 or newer. Works in ESM and CommonJS:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { PersistMemory } from "@persistmemory/sdk"; // ESM
|
|
52
|
+
```
|
|
53
|
+
```js
|
|
54
|
+
const { PersistMemory } = require("@persistmemory/sdk"); // CommonJS
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Get an API key
|
|
60
|
+
|
|
61
|
+
The key is created on the website, and it is shown to you exactly once.
|
|
62
|
+
|
|
63
|
+
1. **Create an account** at <https://persistmemory.com/signup>, or sign in at
|
|
64
|
+
<https://persistmemory.com/signin>. Google sign-in works too.
|
|
65
|
+
2. **Open Settings.** Once you are signed in, *Settings* appears in the header —
|
|
66
|
+
or go straight to <https://persistmemory.com/settings#keys>.
|
|
67
|
+
3. **Find “API keys”** and give the new key a name. Name it after *the thing that
|
|
68
|
+
will use it* — `billing-worker`, `laptop`, `staging` — not after yourself. The
|
|
69
|
+
name is how you decide which key to revoke when a laptop goes missing.
|
|
70
|
+
4. **Press Create, then copy it immediately.** The key looks like
|
|
71
|
+
`pm_live_` followed by 43 characters. It is stored only as a hash, so nobody —
|
|
72
|
+
including us — can show it to you again. A lost key is revoked and replaced,
|
|
73
|
+
never recovered.
|
|
74
|
+
5. **Put it in your environment**, never in your source:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export PERSISTMEMORY_API_KEY=pm_live_...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const client = new PersistMemory({ apiKey: process.env.PERSISTMEMORY_API_KEY! });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### What a key can and cannot do
|
|
85
|
+
|
|
86
|
+
| A key can | A key cannot |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| Read and write your memories | Change your password |
|
|
89
|
+
| Create, update and archive your spaces | Create another API key |
|
|
90
|
+
| Run search and context assembly | Read anybody else's data |
|
|
91
|
+
|
|
92
|
+
That asymmetry is deliberate: a leaked key is something you revoke, not an account takeover.
|
|
93
|
+
Revoke one at any time in **Settings → API keys → Revoke**; anything using it stops working
|
|
94
|
+
immediately.
|
|
95
|
+
|
|
96
|
+
> **Never commit a key.** If one reaches a git history, a CI log or a screenshot, revoke it and
|
|
97
|
+
> create a new one. That takes fifteen seconds and is always the right move.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { PersistMemory } from "@persistmemory/sdk";
|
|
105
|
+
|
|
106
|
+
const client = new PersistMemory({
|
|
107
|
+
apiKey: process.env.PERSISTMEMORY_API_KEY!
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Capture something. This is asynchronous — see the note below.
|
|
111
|
+
await client.memories.remember({
|
|
112
|
+
text: "We chose Postgres for the ledger, not DynamoDB. Transactions across accounts."
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Ask for it back, in your own words.
|
|
116
|
+
const found = await client.search.query({
|
|
117
|
+
query: "what did we decide about the ledger database"
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
for (const result of found.results) {
|
|
121
|
+
console.log(result.score.toFixed(2), result.memory.title);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> **`remember` returns a job, not a memory.** Extraction, entity resolution and consolidation
|
|
126
|
+
> run afterwards, and may produce one memory, several, or none — the same fact said twice
|
|
127
|
+
> produces one memory with two pieces of evidence. What comes back is
|
|
128
|
+
> `{ status, jobId, note }`. Poll `client.jobs` if you need to know when it has settled.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Spaces
|
|
133
|
+
|
|
134
|
+
A **space** is a boundary around memories. Search inside one and you see only what is in it;
|
|
135
|
+
that is the whole point. Use spaces to separate work from personal, one client from another,
|
|
136
|
+
or a shared team memory from your own.
|
|
137
|
+
|
|
138
|
+
### Create a space
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const work = await client.spaces.create({
|
|
142
|
+
name: "Work",
|
|
143
|
+
description: "Decisions, commitments and context for my job",
|
|
144
|
+
kind: "project"
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
console.log(work.id); // pass this to remember/search
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Kinds
|
|
151
|
+
|
|
152
|
+
| `kind` | Use it for |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `user` | Your own personal memory (the default home for anything uncategorised) |
|
|
155
|
+
| `project` | One project, client or repository |
|
|
156
|
+
| `organizational` | A team or company space, shared by several people |
|
|
157
|
+
| `temporary` | Short-lived context — a trip, an incident, a sprint |
|
|
158
|
+
| `system` | Reserved for spaces PersistMemory manages for you |
|
|
159
|
+
|
|
160
|
+
### Multiple spaces
|
|
161
|
+
|
|
162
|
+
Nothing stops you having many, and a memory can belong to more than one.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const [work, acme, personal] = await Promise.all([
|
|
166
|
+
client.spaces.create({ name: "Work", kind: "project" }),
|
|
167
|
+
client.spaces.create({ name: "Acme migration", kind: "project", parentId: undefined }),
|
|
168
|
+
client.spaces.create({ name: "Personal", kind: "user" })
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
// Write into two at once.
|
|
172
|
+
await client.memories.remember({
|
|
173
|
+
text: "Acme's cutover is the 14th; Priya signs off.",
|
|
174
|
+
spaceIds: [work.id, acme.id]
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Read from one.
|
|
178
|
+
const inAcme = await client.search.query({
|
|
179
|
+
query: "when is the cutover",
|
|
180
|
+
spaceIds: [acme.id],
|
|
181
|
+
scope: "space"
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`scope` decides where search is allowed to look:
|
|
186
|
+
|
|
187
|
+
| `scope` | Meaning |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `"space"` | Only the spaces you named. Nothing else can leak in. |
|
|
190
|
+
| `"universal"` | Everything you have, ignoring space boundaries. |
|
|
191
|
+
| `"combined"` | The named spaces first, then everything else, ranked together. |
|
|
192
|
+
|
|
193
|
+
### Nesting, listing, updating, archiving
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// A child space — "Acme" under "Work".
|
|
197
|
+
const acme = await client.spaces.create({
|
|
198
|
+
name: "Acme",
|
|
199
|
+
kind: "project",
|
|
200
|
+
parentId: work.id
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// List them all (paginated; see below).
|
|
204
|
+
for await (const space of client.spaces.list()) {
|
|
205
|
+
console.log(space.name, space.kind, space.memoryCount);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Rename, or set a retention window.
|
|
209
|
+
await client.spaces.update(acme.id, { description: "Migration, phase 2", retentionDays: 365 });
|
|
210
|
+
|
|
211
|
+
// Archive rather than delete: the memories stay, the space stops appearing.
|
|
212
|
+
await client.spaces.update(acme.id, { archived: true });
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Moving memories between spaces
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
await client.spaces.addMemories(acme.id, [id1, id2]);
|
|
219
|
+
await client.spaces.removeMemories(work.id, [id1]);
|
|
220
|
+
|
|
221
|
+
// Everything in one space.
|
|
222
|
+
for await (const memory of client.spaces.memories(acme.id)) {
|
|
223
|
+
console.log(memory.title);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Memories
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// Capture. Optionally title it and file it into spaces.
|
|
233
|
+
const job = await client.memories.remember({
|
|
234
|
+
text: "Priya prefers async updates over standups.",
|
|
235
|
+
title: "Priya — working style",
|
|
236
|
+
spaceIds: [work.id]
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Read one.
|
|
240
|
+
const memory = await client.memories.get(memoryId);
|
|
241
|
+
|
|
242
|
+
// List, filtered by type.
|
|
243
|
+
for await (const decision of client.memories.list({ type: "decision" })) {
|
|
244
|
+
console.log(decision.title);
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Memory types: `fact`, `preference`, `decision`, `task`, `commitment`, `event`, and more —
|
|
249
|
+
the union is exported as `MemoryType`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Search
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const found = await client.search.query({
|
|
257
|
+
query: "what did we decide about the ledger",
|
|
258
|
+
types: ["decision"],
|
|
259
|
+
spaceIds: [work.id],
|
|
260
|
+
scope: "space",
|
|
261
|
+
limit: 10,
|
|
262
|
+
minScore: 0.4,
|
|
263
|
+
explain: true
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (found.diagnostics.degraded) {
|
|
267
|
+
// Embeddings were unavailable and this fell back to deterministic retrieval.
|
|
268
|
+
// The results are still real; the ranking is weaker. Worth surfacing.
|
|
269
|
+
console.warn("degraded:", found.diagnostics.unavailable);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const result of found.results) {
|
|
273
|
+
console.log(result.score, result.memory.title, result.explanation?.paths);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Read `diagnostics.degraded`.** It is not decoration. Search falls back rather than failing,
|
|
278
|
+
and a silent fallback that nobody checks is how a search quietly gets worse for a week.
|
|
279
|
+
|
|
280
|
+
### Asking about the past
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// What did we believe on the 1st of March, not what we believe now?
|
|
284
|
+
const then = await client.search.query({
|
|
285
|
+
query: "our pricing",
|
|
286
|
+
asOf: "2026-03-01T00:00:00Z",
|
|
287
|
+
includeHistorical: true
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Context assembly
|
|
294
|
+
|
|
295
|
+
For putting memory into a prompt without blowing the window:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const context = await client.search.context({
|
|
299
|
+
query: userMessage,
|
|
300
|
+
tokenBudget: 1500,
|
|
301
|
+
spaceIds: [work.id],
|
|
302
|
+
scope: "space"
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const prompt = `${context.context}\n\nUser: ${userMessage}`;
|
|
306
|
+
|
|
307
|
+
console.log(context.usedTokens, "of 1500", context.truncated ? "(truncated)" : "");
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`context.context` is a formatted block, ready to paste into a system prompt.
|
|
311
|
+
`context.memories` tells you what went into it, so you can cite or debug it.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Pagination
|
|
316
|
+
|
|
317
|
+
Every list endpoint returns a `Paginated<T>` — four ways to consume it, all lazy:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
// 1. Iterate every item, fetching pages as needed.
|
|
321
|
+
for await (const memory of client.memories.list({ type: "task" })) { … }
|
|
322
|
+
|
|
323
|
+
// 2. One page, for a UI that renders one page.
|
|
324
|
+
const page = await client.memories.list({ limit: 50 }).first();
|
|
325
|
+
page.data; // T[]
|
|
326
|
+
page.nextCursor; // undefined means there is no more
|
|
327
|
+
|
|
328
|
+
// 3. Page by page.
|
|
329
|
+
for await (const page of client.memories.list().pages()) { … }
|
|
330
|
+
|
|
331
|
+
// 4. Up to N items, with a hard ceiling so a bug cannot page forever.
|
|
332
|
+
const first200 = await client.memories.list().all(200);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
> `nextCursor` being absent is the **only** signal to stop. An empty `data` array is not the
|
|
336
|
+
> same thing — a page can come back empty because everything in it was filtered out.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Errors
|
|
341
|
+
|
|
342
|
+
Every failure is an instance of `PersistMemoryError` with a subclass you can branch on:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import {
|
|
346
|
+
PersistMemory,
|
|
347
|
+
NotFoundError,
|
|
348
|
+
RateLimitError,
|
|
349
|
+
ValidationError,
|
|
350
|
+
AuthenticationError,
|
|
351
|
+
PersistMemoryError
|
|
352
|
+
} from "@persistmemory/sdk";
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
await client.memories.get(id);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error instanceof NotFoundError) return null;
|
|
358
|
+
if (error instanceof RateLimitError) {
|
|
359
|
+
await sleep((error.retryAfterSeconds ?? 1) * 1000);
|
|
360
|
+
return retry();
|
|
361
|
+
}
|
|
362
|
+
if (error instanceof AuthenticationError) throw new Error("Check PERSISTMEMORY_API_KEY");
|
|
363
|
+
if (error instanceof PersistMemoryError) {
|
|
364
|
+
console.error(error.status, error.code, error.requestId);
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
| Class | HTTP | Means |
|
|
371
|
+
| --- | --- | --- |
|
|
372
|
+
| `AuthenticationError` | 401 | Missing, malformed or revoked key |
|
|
373
|
+
| `PermissionDeniedError` | 403 | The key is valid but not allowed to do this |
|
|
374
|
+
| `NotFoundError` | 404 | No such memory, space or job |
|
|
375
|
+
| `ValidationError` | 422 | The request shape was wrong — read `.message` |
|
|
376
|
+
| `ConflictError` | 409 | Something changed underneath you |
|
|
377
|
+
| `RateLimitError` | 429 | Slow down; carries `retryAfterSeconds` |
|
|
378
|
+
| `ServerError` | 5xx | Ours. Retried automatically for GETs |
|
|
379
|
+
| `ConnectionError` | — | The request never reached us |
|
|
380
|
+
| `TimeoutError` | — | It reached us and did not come back in time |
|
|
381
|
+
| `AbortError` | — | You aborted it via `signal` |
|
|
382
|
+
|
|
383
|
+
Every error carries `requestId` — quote it if you contact support, it is how a request is
|
|
384
|
+
found in our logs.
|
|
385
|
+
|
|
386
|
+
**The key never appears in an error, a log line, or a stringified client.**
|
|
387
|
+
`JSON.stringify(client)` prints `{ baseUrl, apiKey: "[redacted]" }`, and so does
|
|
388
|
+
`console.log(client)`.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Retries, timeouts and idempotency
|
|
393
|
+
|
|
394
|
+
GETs are retried with jittered exponential backoff. **POSTs are not** — a POST that timed out
|
|
395
|
+
may already have been processed, and this client will not guess. Opt in per request when the
|
|
396
|
+
operation is safe to repeat:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
await client.memories.remember(
|
|
400
|
+
{ text: "…" },
|
|
401
|
+
{ idempotencyKey: crypto.randomUUID(), maxAttempts: 3 }
|
|
402
|
+
);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Per-request overrides:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
await client.search.query(
|
|
409
|
+
{ query: "…" },
|
|
410
|
+
{ timeoutMs: 5_000, signal: controller.signal }
|
|
411
|
+
);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Configuration reference
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
const client = new PersistMemory({
|
|
420
|
+
apiKey: process.env.PERSISTMEMORY_API_KEY!, // required
|
|
421
|
+
baseUrl: "https://api.persistmemory.com", // default
|
|
422
|
+
timeoutMs: 30_000,
|
|
423
|
+
maxAttempts: 3,
|
|
424
|
+
backoff: { baseMs: 250, maxMs: 8_000, factor: 2, jitter: 0.2 },
|
|
425
|
+
userAgent: "my-app/1.2.3",
|
|
426
|
+
fetch: myFetch // injectable, see Testing
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
| Option | Default | Notes |
|
|
431
|
+
| --- | --- | --- |
|
|
432
|
+
| `apiKey` | — | Required. From <https://persistmemory.com/settings#keys> |
|
|
433
|
+
| `baseUrl` | `https://api.persistmemory.com` | Trailing slashes are stripped for you |
|
|
434
|
+
| `timeoutMs` | `30000` | Per attempt, not per call |
|
|
435
|
+
| `maxAttempts` | `3` | GETs only, unless you pass `idempotencyKey` |
|
|
436
|
+
| `backoff` | jittered | `DEFAULT_BACKOFF` is exported if you want to read it |
|
|
437
|
+
| `fetch` | `globalThis.fetch` | Anything with the same signature |
|
|
438
|
+
| `userAgent` | SDK name and version | Yours is appended, not substituted |
|
|
439
|
+
|
|
440
|
+
Environment variables the SDK itself reads: **none.** It takes what you pass it, so a test
|
|
441
|
+
cannot pick up a real key from your shell by accident. Read `process.env` yourself, once,
|
|
442
|
+
where you construct the client.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Testing
|
|
447
|
+
|
|
448
|
+
Inject a `fetch` and your test can never reach the network:
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
const client = new PersistMemory({
|
|
452
|
+
apiKey: "pm_live_test",
|
|
453
|
+
fetch: async (url, init) => {
|
|
454
|
+
expect(String(url)).toContain("/api/v1/search");
|
|
455
|
+
return new Response(JSON.stringify({ results: [], diagnostics: { steps: [], degraded: false, tookMs: 1 }, query: "x" }), {
|
|
456
|
+
status: 200,
|
|
457
|
+
headers: { "content-type": "application/json" }
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
`sleep` is injectable too, so a retry test does not spend real seconds waiting.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Troubleshooting
|
|
468
|
+
|
|
469
|
+
**`AuthenticationError: 401`**
|
|
470
|
+
The key is missing, mistyped, or revoked. Check `process.env.PERSISTMEMORY_API_KEY` is actually
|
|
471
|
+
set in the process you are running (a `.env` file is not read for you), and that it starts with
|
|
472
|
+
`pm_live_`. If in doubt, create a fresh key — it costs nothing.
|
|
473
|
+
|
|
474
|
+
**`remember` succeeded but `search` finds nothing**
|
|
475
|
+
`remember` is asynchronous. Extraction runs after the call returns, so there is a short window
|
|
476
|
+
where the job exists and the memory does not. Poll the job id, or search a moment later.
|
|
477
|
+
|
|
478
|
+
**`search` returns fewer results than expected**
|
|
479
|
+
Check `scope`. With `scope: "space"` and a `spaceIds` list, anything outside those spaces is
|
|
480
|
+
invisible by design. Try `scope: "combined"`.
|
|
481
|
+
|
|
482
|
+
**`diagnostics.degraded` is `true`**
|
|
483
|
+
Embeddings were unavailable and search fell back to deterministic retrieval. Results are real
|
|
484
|
+
but ranked more weakly. Usually transient.
|
|
485
|
+
|
|
486
|
+
**Rate limited**
|
|
487
|
+
`RateLimitError.retryAfterSeconds` comes from the `Retry-After` header. Wait that long, not a
|
|
488
|
+
guess of your own.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## FAQ
|
|
493
|
+
|
|
494
|
+
**Does it work in the browser?**
|
|
495
|
+
Technically yes, but do not. An API key in a browser is a key you have given away — every
|
|
496
|
+
visitor can read it. Call your own server, and let the server hold the key.
|
|
497
|
+
|
|
498
|
+
**Does it work in Cloudflare Workers / Deno / Bun?**
|
|
499
|
+
Yes. The package is dependency-free and uses only `fetch` and standard globals. Pass your
|
|
500
|
+
runtime's `fetch` explicitly if it is not on `globalThis`.
|
|
501
|
+
|
|
502
|
+
**Is there a Python client?**
|
|
503
|
+
Yes — `pip install persistmemory`. Same endpoints, same retry policy, same paginator.
|
|
504
|
+
|
|
505
|
+
**How do I use this from a terminal instead?**
|
|
506
|
+
`npm install -g @persistmemory/cli`, then `pm auth login`. It signs in through your browser
|
|
507
|
+
with OAuth, so no key is needed at all.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Links
|
|
512
|
+
|
|
513
|
+
- Documentation — <https://persistmemory.com/docs/sdk>
|
|
514
|
+
- API keys — <https://persistmemory.com/settings#keys>
|
|
515
|
+
- CLI — <https://persistmemory.com/docs/cli>
|
|
516
|
+
- Support — <mailto:hello@persistmemory.com>
|
|
517
|
+
|
|
518
|
+
## License
|
|
519
|
+
|
|
520
|
+
MIT © PersistMemory
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long to wait before trying again.
|
|
3
|
+
*
|
|
4
|
+
* Exponential, capped, and jittered, for the reasons the rest of this repo
|
|
5
|
+
* gives in `@persistmemory/async`:
|
|
6
|
+
*
|
|
7
|
+
* exponential a cause that has not cleared in 200ms may clear in two
|
|
8
|
+
* seconds; hammering it every 200ms spends the attempts faster
|
|
9
|
+
* without giving it time to recover.
|
|
10
|
+
*
|
|
11
|
+
* capped unbounded doubling reaches minutes, and a caller waiting
|
|
12
|
+
* inside one function call cannot tell that from a hang.
|
|
13
|
+
*
|
|
14
|
+
* jittered the one that gets skipped, and the one that matters most. An
|
|
15
|
+
* outage fails every in-flight request at once; without jitter
|
|
16
|
+
* every client waits exactly two seconds and retries in the
|
|
17
|
+
* same millisecond, so the recovering service is hit by the
|
|
18
|
+
* whole fleet and knocked over again. The retry storm is caused
|
|
19
|
+
* by the retry policy.
|
|
20
|
+
*
|
|
21
|
+
* Duplicated here rather than imported from `@persistmemory/async` on purpose:
|
|
22
|
+
* this package is published to npm and installed by people who have no reason
|
|
23
|
+
* to pull in a queue runtime, a Redis client and a dead-letter store to make
|
|
24
|
+
* an HTTP request.
|
|
25
|
+
*/
|
|
26
|
+
export interface BackoffOptions {
|
|
27
|
+
readonly baseMs?: number;
|
|
28
|
+
readonly maxMs?: number;
|
|
29
|
+
readonly factor?: number;
|
|
30
|
+
/** 0 = none, 1 = full. Full is the right default; see below. */
|
|
31
|
+
readonly jitter?: number;
|
|
32
|
+
/** Injected so a test is not at the mercy of the random number generator. */
|
|
33
|
+
readonly random?: () => number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Short by server standards.
|
|
37
|
+
*
|
|
38
|
+
* A queue worker can afford to come back in a minute. A caller is blocked
|
|
39
|
+
* inside `await client.search(...)` and has a user watching, so the whole
|
|
40
|
+
* retry budget has to fit inside a request timeout rather than outlast it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DEFAULT_BACKOFF: {
|
|
43
|
+
readonly baseMs: 250;
|
|
44
|
+
readonly maxMs: 8000;
|
|
45
|
+
readonly factor: 2;
|
|
46
|
+
readonly jitter: 1;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The delay before attempt `attempt` (1-based).
|
|
50
|
+
*
|
|
51
|
+
* FULL jitter by default - a random point in [0, ceiling] rather than
|
|
52
|
+
* ceiling plus or minus a wobble. It sounds worse and measures better:
|
|
53
|
+
* partial jitter leaves the retries clustered around the same instant, which
|
|
54
|
+
* is the thing that overwhelms a service coming back up.
|
|
55
|
+
*/
|
|
56
|
+
export declare function backoffMs(attempt: number, options?: BackoffOptions): number;
|
|
57
|
+
/**
|
|
58
|
+
* The delay to actually use, honouring what the server asked for.
|
|
59
|
+
*
|
|
60
|
+
* `Retry-After` is taken as a FLOOR, not verbatim. Verbatim would let a
|
|
61
|
+
* one-second hint on the fifth consecutive 429 undo the backoff entirely and
|
|
62
|
+
* put us straight back into the limit; ignoring it would have us retry before
|
|
63
|
+
* the window resets and spend an attempt learning what we were already told.
|
|
64
|
+
*
|
|
65
|
+
* A server asking for LONGER than our cap is believed. The cap exists to stop
|
|
66
|
+
* our own growth running away, not to overrule a service that has told us
|
|
67
|
+
* when it will be ready.
|
|
68
|
+
*/
|
|
69
|
+
export declare function delayFor(args: {
|
|
70
|
+
attempt: number;
|
|
71
|
+
retryAfterSeconds?: number;
|
|
72
|
+
options?: BackoffOptions;
|
|
73
|
+
}): number;
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ClientOptions, RequestOptions } from "./http.js";
|
|
2
|
+
import { Memories } from "./resources/memories.js";
|
|
3
|
+
import { Search } from "./resources/search.js";
|
|
4
|
+
import { Spaces } from "./resources/spaces.js";
|
|
5
|
+
import { Documents, Jobs, Sources } from "./resources/ingestion.js";
|
|
6
|
+
import { Conflicts, Entities, Graph } from "./resources/knowledge.js";
|
|
7
|
+
import { Conversations } from "./resources/conversations.js";
|
|
8
|
+
import { Integrations } from "./resources/integrations.js";
|
|
9
|
+
import { Health } from "./resources/health.js";
|
|
10
|
+
/**
|
|
11
|
+
* The client.
|
|
12
|
+
*
|
|
13
|
+
* const client = new PersistMemory({ apiKey: process.env.PERSISTMEMORY_API_KEY! });
|
|
14
|
+
* const found = await client.search.query({ query: "what did we decide about Postgres" });
|
|
15
|
+
*
|
|
16
|
+
* One transport underneath every resource, so retries, timeouts, error mapping
|
|
17
|
+
* and the credential are decided once. Resources are plain objects hanging off
|
|
18
|
+
* this one; they hold a reference to the transport and no state of their own,
|
|
19
|
+
* which is what makes a client safe to share across a process.
|
|
20
|
+
*/
|
|
21
|
+
export declare class PersistMemory {
|
|
22
|
+
#private;
|
|
23
|
+
readonly memories: Memories;
|
|
24
|
+
readonly search: Search;
|
|
25
|
+
readonly spaces: Spaces;
|
|
26
|
+
readonly sources: Sources;
|
|
27
|
+
readonly documents: Documents;
|
|
28
|
+
readonly jobs: Jobs;
|
|
29
|
+
readonly entities: Entities;
|
|
30
|
+
readonly graph: Graph;
|
|
31
|
+
readonly conflicts: Conflicts;
|
|
32
|
+
readonly conversations: Conversations;
|
|
33
|
+
readonly integrations: Integrations;
|
|
34
|
+
readonly health: Health;
|
|
35
|
+
constructor(options: ClientOptions);
|
|
36
|
+
/**
|
|
37
|
+
* An escape hatch for an endpoint this package has not caught up with.
|
|
38
|
+
*
|
|
39
|
+
* Typed as `unknown` on purpose: a caller reaching past the typed surface is
|
|
40
|
+
* taking responsibility for the shape, and handing them `any` would let that
|
|
41
|
+
* responsibility spread silently through their codebase.
|
|
42
|
+
*/
|
|
43
|
+
request<T = unknown>(method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
44
|
+
/** Never the key. See `HttpClient.toJSON`, which this delegates to. */
|
|
45
|
+
toJSON(): Record<string, unknown>;
|
|
46
|
+
}
|