@arkimedehq/recordare-client 0.1.0 → 0.2.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/README.md +46 -11
- package/README_it.md +47 -11
- package/dist/client.d.ts +23 -5
- package/dist/client.js +77 -7
- package/dist/contract.d.ts +80 -15
- package/dist/contract.js +3 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/mcp.js +3 -3
- package/dist/people.d.ts +10 -16
- package/dist/people.js +5 -21
- package/dist/tools.d.ts +226 -0
- package/dist/tools.js +286 -0
- package/package.json +2 -2
- package/src/client.ts +76 -7
- package/src/contract.ts +75 -15
- package/src/errors.ts +2 -2
- package/src/index.ts +2 -1
- package/src/mcp.ts +3 -3
- package/src/people.ts +14 -34
- package/src/tools.ts +287 -0
package/README.md
CHANGED
|
@@ -2,28 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
*Italian version: [README_it.md](README_it.md).*
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Since D50 (2026-10-09) Recordare has no consent flag: every memory stores what its client sends; the on/off switch
|
|
6
|
+
> belongs to the client platform (WORK_PLAN 8.1).
|
|
7
|
+
|
|
8
|
+
The one client library every Recordare client uses (WORK_PLAN 6.7): identity, the platform's on/off switch, the person's settings,
|
|
6
9
|
ingest, edits and deletions, recall over MCP, and the delivery policy for a host's outbox. Built on standards, so a
|
|
7
10
|
host adapts to them: MCP through the official SDK (streamable HTTP), errors as RFC 9457 problem details,
|
|
8
11
|
`Retry-After` (RFC 9110) honoured, W3C trace context forwarded, native `fetch` (a host may pass its own, e.g. one
|
|
9
12
|
enforcing an outbound-host policy). No host-specific code.
|
|
10
13
|
|
|
14
|
+
On npm since Recordare v0.1.0: `npm install @arkimedehq/recordare-client` (Node ≥ 20, ES module).
|
|
15
|
+
|
|
11
16
|
```ts
|
|
12
17
|
import { PersonDirectory, RecordareClient, afterFailure } from '@arkimedehq/recordare-client';
|
|
13
18
|
|
|
14
19
|
const rc = new RecordareClient({ baseUrl: 'http://recordare:8080', apiKey: process.env.RECORDARE_API_KEY!,
|
|
15
20
|
headers: () => traceHeaders() }); // e.g. OpenTelemetry propagation.inject
|
|
16
21
|
|
|
17
|
-
//
|
|
22
|
+
// Which memory the user's account opens, its mode (personal | entity) and Atlas address; the name follows the platform's profile. `enabled` is the
|
|
23
|
+
// platform's own memory switch: while false, Recordare is not contacted (no consent flag in Recordare, D50).
|
|
18
24
|
const people = new PersonDirectory(rc, { user: async (id) => ({ enabled: true, name: 'Andrea' }) });
|
|
19
|
-
await people.
|
|
25
|
+
const person = await people.refresh('user-42'); // { memoryId, mode, atlasUrl }; peek() reads the cache
|
|
20
26
|
|
|
21
|
-
// Ingest (split into requests of 500, idempotent on each externalId)
|
|
27
|
+
// Ingest (split into requests of 500, idempotent on each externalId): → { conversationId, accepted, duplicates, conflicts }.
|
|
22
28
|
await rc.ingest('user-42', { conversation: { externalId: 'chat-1' }, messages: [
|
|
23
29
|
{ externalId: 'm1', role: 'user', content: 'Domani vado a Bologna', sentAt: new Date().toISOString() },
|
|
24
30
|
] });
|
|
31
|
+
// The agent's own content (knowledge given to it, a perception, a document): `own: true` on a user / other message.
|
|
32
|
+
await rc.ingest('user-42', { conversation: { externalId: 'doc-1', source: 'document' }, messages: [
|
|
33
|
+
{ externalId: 'd1', role: 'user', own: true, content: 'La caldaia va revisionata ogni due anni.', sentAt: new Date().toISOString() },
|
|
34
|
+
] });
|
|
35
|
+
// Mode (only while the memory is empty, else MemoryNotEmptyError) and the first person's gender, from the profile.
|
|
36
|
+
await rc.updateMe('user-42', { gender: 'feminine' });
|
|
25
37
|
|
|
26
|
-
// Recall: one MCP session per user AND conversation (the
|
|
38
|
+
// Recall: one MCP session per user AND conversation (evidence of writes, the current turn), both bound in code.
|
|
27
39
|
const tools = await rc.mcp.listTools('user-42', 'chat-1'); // build the agent's tools from these schemas
|
|
28
40
|
const res = await rc.mcp.callTool('user-42', 'chat-1', 'search_episodes', { query: 'Bologna' });
|
|
29
41
|
|
|
@@ -31,6 +43,29 @@ const res = await rc.mcp.callTool('user-42', 'chat-1', 'search_episodes', { quer
|
|
|
31
43
|
const next = afterFailure(err, attempts); // { action: 'retry', delayMs } | { action: 'park', reason }
|
|
32
44
|
```
|
|
33
45
|
|
|
46
|
+
## Before each turn, and at the end
|
|
47
|
+
`contextWithTurn(user, turn)` stores the turn and returns its memory context in one round trip (`{block, items}`, the
|
|
48
|
+
block to append to the system prompt; `context(user, conversation, query)` when the turn is already stored); `endConversation(user, conversation)` tells Recordare a conversation ended
|
|
49
|
+
(session closed, /new) so extraction runs now. `TOOLS` holds the MCP tool schemas the service serves (name, title,
|
|
50
|
+
description, JSON Schema), for hosts that must declare tools before connecting; the conformance suite keeps it in
|
|
51
|
+
sync with the service.
|
|
52
|
+
|
|
53
|
+
## Learned sources
|
|
54
|
+
`learnSource(user, {externalId, title, text, kind?, author?, uri?, language?, learnedAt?, providedBy?, conversation?})`
|
|
55
|
+
makes the agent learn a text (D49, WORK_PLAN 8.9: a manual, a page, a note — **text only**, the host converts files).
|
|
56
|
+
Any size: a text bigger than `SOURCE_PART_BYTES` (4 MB, or the `partBytes` argument) is sent in parts cut at paragraph
|
|
57
|
+
boundaries (`POST api/v1/ingest/sources` + `…/parts`); → `{sourceId, status, parts, passages, duplicate}`. The same
|
|
58
|
+
`externalId` again replaces the source; `providedBy` is `'me'` (default), `'someone'` or `{name}`; `conversation:
|
|
59
|
+
{externalId}` links it to the conversation it was learned in (that extraction tells of the learning).
|
|
60
|
+
`forgetSource(user, externalId)` forgets it (one never sent counts as done); `sources(user)` lists what the agent
|
|
61
|
+
learned (`Source[]`). The agent searches it with the MCP tool `search_knowledge`. Send only what the agent should
|
|
62
|
+
learn, never a RAG's search results.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
await rc.learnSource('user-42', { externalId: 'boiler-manual', title: 'Manuale della caldaia', text: manualText,
|
|
66
|
+
kind: 'document', providedBy: { name: 'Paolo' }, conversation: { externalId: 'chat-1' } });
|
|
67
|
+
```
|
|
68
|
+
|
|
34
69
|
## The diary
|
|
35
70
|
`episodes`, `episode`, `digests`, `facts`, `notes`, `plans` read what Recordare remembers for the person's own view in
|
|
36
71
|
the platform's UI; `correctEpisode`, `forgetEpisode`, `pinNote`, `delete`, `decide` are the person's edits (API.md §4).
|
|
@@ -41,13 +76,13 @@ Everything else is here, the same for every client.
|
|
|
41
76
|
|
|
42
77
|
## Conformance
|
|
43
78
|
`service/test/conformance` runs this library against the real service in Recordare's CI: a turn ingested once,
|
|
44
|
-
|
|
45
|
-
user and the conversation. Type checks there fail the build when this contract drifts from the service's schemas.
|
|
79
|
+
deletions propagate, the name, mode and gender follow the platform, own content is marked, a source is learned in
|
|
80
|
+
parts, listed and forgotten, recall over MCP carries the user and the conversation. Type checks there fail the build when this contract drifts from the service's schemas.
|
|
46
81
|
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
the source commit (Arkimede: `backend/src/recordare/client/`). The host never
|
|
50
|
-
|
|
82
|
+
## Synced copy (Arkimede)
|
|
83
|
+
Arkimede still uses a copy of the sources instead of the npm package: `scripts/sync-to.sh <dir>` copies them into a
|
|
84
|
+
host repository with a header naming the source commit (Arkimede: `backend/src/recordare/client/`). The host never
|
|
85
|
+
edits them; change the library here and sync again. Switching to the npm package only changes the imports.
|
|
51
86
|
|
|
52
87
|
## Development
|
|
53
88
|
`npm ci`, `npm run typecheck`, `npm test`, `npm run build` (Node ≥ 20).
|
package/README_it.md
CHANGED
|
@@ -2,28 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
*Versione inglese (di riferimento): [README.md](README.md).*
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Da D50 (2026-10-09) Recordare non ha un flag di consenso: ogni memoria conserva ciò che il suo client invia;
|
|
6
|
+
> l'interruttore acceso / spento appartiene alla piattaforma client (WORK_PLAN 8.1).
|
|
7
|
+
|
|
8
|
+
L'unica libreria client che ogni client di Recordare usa (WORK_PLAN 6.7): identità, l'interruttore acceso / spento della piattaforma, impostazioni della
|
|
6
9
|
persona, ingest, modifiche e cancellazioni, richiamo via MCP e la politica di consegna per l'outbox dell'host. È
|
|
7
10
|
costruita sugli standard, così è l'host ad adattarsi: MCP con l'SDK ufficiale (streamable HTTP), errori come problem
|
|
8
11
|
details RFC 9457, `Retry-After` (RFC 9110) rispettato, contesto di traccia W3C inoltrato, `fetch` nativo (l'host può
|
|
9
12
|
passarne una sua, ad esempio con una politica sugli host in uscita). Nessun codice specifico di un host.
|
|
10
13
|
|
|
14
|
+
Su npm da Recordare v0.1.0: `npm install @arkimedehq/recordare-client` (Node ≥ 20, modulo ES).
|
|
15
|
+
|
|
11
16
|
```ts
|
|
12
17
|
import { PersonDirectory, RecordareClient, afterFailure } from '@arkimedehq/recordare-client';
|
|
13
18
|
|
|
14
19
|
const rc = new RecordareClient({ baseUrl: 'http://recordare:8080', apiKey: process.env.RECORDARE_API_KEY!,
|
|
15
20
|
headers: () => traceHeaders() }); // es. propagation.inject di OpenTelemetry
|
|
16
21
|
|
|
17
|
-
//
|
|
22
|
+
// Quale memoria apre l'account dell'utente, il suo modo (personal | entity) e l'indirizzo di Atlas; il nome segue il profilo sulla piattaforma. `enabled` è
|
|
23
|
+
// l'interruttore della memoria della piattaforma: finché è false Recordare non viene contattato (niente consenso, D50).
|
|
18
24
|
const people = new PersonDirectory(rc, { user: async (id) => ({ enabled: true, name: 'Andrea' }) });
|
|
19
|
-
await people.
|
|
25
|
+
const person = await people.refresh('user-42'); // { memoryId, mode, atlasUrl }; peek() legge la cache
|
|
20
26
|
|
|
21
|
-
// Ingest (diviso in richieste da 500, idempotente su ogni externalId)
|
|
27
|
+
// Ingest (diviso in richieste da 500, idempotente su ogni externalId): → { conversationId, accepted, duplicates, conflicts }.
|
|
22
28
|
await rc.ingest('user-42', { conversation: { externalId: 'chat-1' }, messages: [
|
|
23
29
|
{ externalId: 'm1', role: 'user', content: 'Domani vado a Bologna', sentAt: new Date().toISOString() },
|
|
24
30
|
] });
|
|
31
|
+
// Il contenuto proprio dell'agente (conoscenza data, una percezione, un documento): `own: true` su un messaggio user / other.
|
|
32
|
+
await rc.ingest('user-42', { conversation: { externalId: 'doc-1', source: 'document' }, messages: [
|
|
33
|
+
{ externalId: 'd1', role: 'user', own: true, content: 'La caldaia va revisionata ogni due anni.', sentAt: new Date().toISOString() },
|
|
34
|
+
] });
|
|
35
|
+
// Modo (solo finché la memoria è vuota, altrimenti MemoryNotEmptyError) e genere della prima persona, dal profilo.
|
|
36
|
+
await rc.updateMe('user-42', { gender: 'feminine' });
|
|
25
37
|
|
|
26
|
-
// Richiamo: una sessione MCP per utente E conversazione (
|
|
38
|
+
// Richiamo: una sessione MCP per utente E conversazione (prove delle scritture, il turno corrente), entrambi fissati nel codice.
|
|
27
39
|
const tools = await rc.mcp.listTools('user-42', 'chat-1'); // gli strumenti dell'agente nascono da questi schemi
|
|
28
40
|
const res = await rc.mcp.callTool('user-42', 'chat-1', 'search_episodes', { query: 'Bologna' });
|
|
29
41
|
|
|
@@ -31,6 +43,30 @@ const res = await rc.mcp.callTool('user-42', 'chat-1', 'search_episodes', { quer
|
|
|
31
43
|
const next = afterFailure(err, attempts); // { action: 'retry', delayMs } | { action: 'park', reason }
|
|
32
44
|
```
|
|
33
45
|
|
|
46
|
+
## Prima di ogni turno, e alla fine
|
|
47
|
+
`contextWithTurn(user, turn)` salva il turno e restituisce il suo contesto di memoria in una sola andata e ritorno
|
|
48
|
+
(`{block, items}`, il blocco da aggiungere in coda al prompt di sistema; `context(user, conversation, query)` quando il
|
|
49
|
+
turno è già salvato); `endConversation(user, conversation)` dice a
|
|
50
|
+
Recordare che una conversazione è finita (sessione chiusa, /new), così l'estrazione parte subito. `TOOLS` contiene gli
|
|
51
|
+
schemi degli strumenti MCP che il servizio offre (nome, titolo, descrizione, JSON Schema), per gli host che devono
|
|
52
|
+
dichiarare gli strumenti prima di collegarsi; la suite di conformità li tiene allineati al servizio.
|
|
53
|
+
|
|
54
|
+
## Fonti apprese
|
|
55
|
+
`learnSource(user, {externalId, title, text, kind?, author?, uri?, language?, learnedAt?, providedBy?, conversation?})`
|
|
56
|
+
fa imparare un testo all'agente (D49, WORK_PLAN 8.9: un manuale, una pagina, una nota — **solo testo**, è l'host a
|
|
57
|
+
convertire i file). Qualsiasi dimensione: un testo più grande di `SOURCE_PART_BYTES` (4 MB, o l'argomento `partBytes`)
|
|
58
|
+
viene inviato a parti tagliate ai confini dei paragrafi (`POST api/v1/ingest/sources` + `…/parts`); → `{sourceId,
|
|
59
|
+
status, parts, passages, duplicate}`. Lo stesso `externalId` sostituisce la fonte; `providedBy` è `'me'` (default),
|
|
60
|
+
`'someone'` oppure `{name}`; `conversation: {externalId}` la collega alla conversazione in cui è stata imparata (è
|
|
61
|
+
quell'estrazione a raccontare l'apprendimento). `forgetSource(user, externalId)` la dimentica (una mai inviata conta
|
|
62
|
+
come fatto); `sources(user)` elenca ciò che l'agente ha imparato (`Source[]`). L'agente la cerca con lo strumento MCP
|
|
63
|
+
`search_knowledge`. Inviare solo ciò che l'agente deve imparare, mai i risultati di ricerca di un RAG.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await rc.learnSource('user-42', { externalId: 'boiler-manual', title: 'Manuale della caldaia', text: manualText,
|
|
67
|
+
kind: 'document', providedBy: { name: 'Paolo' }, conversation: { externalId: 'chat-1' } });
|
|
68
|
+
```
|
|
69
|
+
|
|
34
70
|
## Il diario
|
|
35
71
|
`episodes`, `episode`, `digests`, `facts`, `notes`, `plans` leggono ciò che Recordare ricorda, per la vista della persona
|
|
36
72
|
nell'interfaccia della piattaforma; `correctEpisode`, `forgetEpisode`, `pinNote`, `delete`, `decide` sono le modifiche
|
|
@@ -42,14 +78,14 @@ ingest. Tutto il resto è qui, uguale per ogni client.
|
|
|
42
78
|
|
|
43
79
|
## Conformità
|
|
44
80
|
`service/test/conformance` esegue questa libreria contro il servizio vero nella CI di Recordare: un turno salvato una
|
|
45
|
-
volta sola,
|
|
46
|
-
richiamo via MCP porta l'utente e la conversazione. I controlli di tipo lì fanno fallire la build se questo contratto si
|
|
81
|
+
volta sola, le cancellazioni si propagano, nome, modo e genere seguono la piattaforma, il contenuto proprio è marcato, una
|
|
82
|
+
fonte viene imparata a parti, elencata e dimenticata, il richiamo via MCP porta l'utente e la conversazione. I controlli di tipo lì fanno fallire la build se questo contratto si
|
|
47
83
|
allontana dagli schemi del servizio.
|
|
48
84
|
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
indica il commit di origine (Arkimede: `backend/src/recordare/client/`).
|
|
52
|
-
libreria qui e si sincronizza di nuovo.
|
|
85
|
+
## Copia sincronizzata (Arkimede)
|
|
86
|
+
Arkimede usa ancora una copia dei sorgenti invece del pacchetto npm: `scripts/sync-to.sh <dir>` copia i sorgenti nel
|
|
87
|
+
repository dell'host con un'intestazione che indica il commit di origine (Arkimede: `backend/src/recordare/client/`).
|
|
88
|
+
L'host non li modifica mai; si cambia la libreria qui e si sincronizza di nuovo. Passare al pacchetto npm cambia solo
|
|
53
89
|
gli import.
|
|
54
90
|
|
|
55
91
|
## Sviluppo
|
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type Digest, type Episode, type EpisodeDetail, type EpisodeQuery, type Fact, type IngestRequest, type IngestResult, type Me, type MeSettings, type MemoryContext, type Note, type PlanStatus, type Precision } from './contract.js';
|
|
1
|
+
import { type Digest, type Episode, type EpisodeDetail, type EpisodeQuery, type Fact, type IngestRequest, type IngestResult, type LearnSource, type LearnSourceResult, type Me, type MeSettings, type MemoryContext, type Note, type PlanStatus, type Precision, type Source } from './contract.js';
|
|
2
2
|
import { type ClientOptions, Http } from './http.js';
|
|
3
3
|
import { RecordareMcp, type McpOptions } from './mcp.js';
|
|
4
4
|
/**
|
|
5
|
-
* Recordare for one platform (docs/INTEGRATION.md): who a user is
|
|
5
|
+
* Recordare for one platform (docs/INTEGRATION.md): who a user is, their settings, the conversations
|
|
6
6
|
* the platform sends, deletions, and recall over MCP. Stateless apart from MCP sessions; a host keeps its own outbox and
|
|
7
7
|
* cache (see `delivery` and `PersonDirectory`).
|
|
8
8
|
*/
|
|
@@ -12,13 +12,13 @@ export declare class RecordareClient {
|
|
|
12
12
|
constructor(options: ClientOptions & {
|
|
13
13
|
mcp?: McpOptions;
|
|
14
14
|
});
|
|
15
|
-
/** The
|
|
15
|
+
/** The memory behind `user` (auto-provisioned when the client allows it), its mode and gender. */
|
|
16
16
|
me(user: string): Promise<Me>;
|
|
17
|
-
/** The
|
|
17
|
+
/** The memory's settings from the platform: the name (keep it in sync on every rename), the mode and the gender. */
|
|
18
18
|
updateMe(user: string, settings: MeSettings): Promise<void>;
|
|
19
19
|
/**
|
|
20
20
|
* Sends messages of one conversation, split into requests of at most MAX_MESSAGES_PER_REQUEST. Idempotent: Recordare
|
|
21
|
-
* deduplicates on each message's externalId.
|
|
21
|
+
* deduplicates on each message's externalId. Recordare always stores (D50): the on/off switch is the platform's.
|
|
22
22
|
*/
|
|
23
23
|
ingest(user: string, req: IngestRequest): Promise<IngestResult>;
|
|
24
24
|
/**
|
|
@@ -27,12 +27,28 @@ export declare class RecordareClient {
|
|
|
27
27
|
* caching); never store it as a chat message.
|
|
28
28
|
*/
|
|
29
29
|
context(user: string, conversation: string, query: string): Promise<MemoryContext>;
|
|
30
|
+
/**
|
|
31
|
+
* Stores the turn and returns the memory context for it in one round trip (needs the `ingest` and `read` scopes):
|
|
32
|
+
* the context is computed for the ingested conversation, from `query` or else the turn's last user message.
|
|
33
|
+
*/
|
|
34
|
+
contextWithTurn(user: string, turn: IngestRequest, query?: string): Promise<MemoryContext>;
|
|
35
|
+
/** The conversation ended (session closed, /new): extraction runs now. A conversation Recordare never had is done. */
|
|
36
|
+
endConversation(user: string, conversation: string): Promise<void>;
|
|
30
37
|
/** An edited message: Recordare re-extracts what depended on it. */
|
|
31
38
|
editMessage(user: string, conversation: string, message: string, content: string): Promise<void>;
|
|
32
39
|
/** A deleted message: Recordare forgets it and what came only from it. A message it never had counts as done. */
|
|
33
40
|
deleteMessage(user: string, conversation: string, message: string): Promise<void>;
|
|
34
41
|
/** A deleted conversation. A conversation Recordare never had counts as done. */
|
|
35
42
|
deleteConversation(user: string, conversation: string): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* The agent learns a text. A big one is sent in parts (at paragraph boundaries, `partBytes` each); any size works.
|
|
45
|
+
* Sending the same `externalId` again replaces the source; the same first part again is a duplicate.
|
|
46
|
+
*/
|
|
47
|
+
learnSource(user: string, source: LearnSource, partBytes?: number): Promise<LearnSourceResult>;
|
|
48
|
+
/** The agent forgets a source (its text and passages; episodes keep a "forgotten source" marker). One it never had counts as done. */
|
|
49
|
+
forgetSource(user: string, externalId: string): Promise<void>;
|
|
50
|
+
/** What the agent learned, newest first. */
|
|
51
|
+
sources(user: string): Promise<Source[]>;
|
|
36
52
|
/** The timeline, newest first; pass `nextCursor` back as `cursor` for the next page. */
|
|
37
53
|
episodes(user: string, query?: EpisodeQuery): Promise<{
|
|
38
54
|
items: Episode[];
|
|
@@ -75,3 +91,5 @@ export declare class RecordareClient {
|
|
|
75
91
|
close(): Promise<void>;
|
|
76
92
|
private gone;
|
|
77
93
|
}
|
|
94
|
+
/** A text in parts of at most `maxBytes` UTF-8 bytes, cut at paragraph boundaries (inside a long paragraph: at a space, else between characters). */
|
|
95
|
+
export declare function splitParts(text: string, maxBytes: number): string[];
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright © 2026 Andrea Genovese
|
|
3
|
-
import { CONVERSATION_HEADER, MAX_MESSAGES_PER_REQUEST, } from './contract.js';
|
|
3
|
+
import { CONVERSATION_HEADER, MAX_MESSAGES_PER_REQUEST, SOURCE_PART_BYTES, } from './contract.js';
|
|
4
4
|
import { MemoryNotEmptyError, RecordareHttpError } from './errors.js';
|
|
5
5
|
import { Http } from './http.js';
|
|
6
6
|
import { RecordareMcp } from './mcp.js';
|
|
@@ -15,7 +15,7 @@ function qs(query) {
|
|
|
15
15
|
return s ? `?${s}` : '';
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Recordare for one platform (docs/INTEGRATION.md): who a user is
|
|
18
|
+
* Recordare for one platform (docs/INTEGRATION.md): who a user is, their settings, the conversations
|
|
19
19
|
* the platform sends, deletions, and recall over MCP. Stateless apart from MCP sessions; a host keeps its own outbox and
|
|
20
20
|
* cache (see `delivery` and `PersonDirectory`).
|
|
21
21
|
*/
|
|
@@ -26,11 +26,11 @@ export class RecordareClient {
|
|
|
26
26
|
this.http = new Http(options);
|
|
27
27
|
this.mcp = new RecordareMcp(this.http, options.mcp);
|
|
28
28
|
}
|
|
29
|
-
/** The
|
|
29
|
+
/** The memory behind `user` (auto-provisioned when the client allows it), its mode and gender. */
|
|
30
30
|
me(user) {
|
|
31
31
|
return this.http.request('GET', 'api/v1/me', { user });
|
|
32
32
|
}
|
|
33
|
-
/** The
|
|
33
|
+
/** The memory's settings from the platform: the name (keep it in sync on every rename), the mode and the gender. */
|
|
34
34
|
async updateMe(user, settings) {
|
|
35
35
|
try {
|
|
36
36
|
await this.http.request('PATCH', 'api/v1/me', { user, body: settings });
|
|
@@ -43,10 +43,10 @@ export class RecordareClient {
|
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* Sends messages of one conversation, split into requests of at most MAX_MESSAGES_PER_REQUEST. Idempotent: Recordare
|
|
46
|
-
* deduplicates on each message's externalId.
|
|
46
|
+
* deduplicates on each message's externalId. Recordare always stores (D50): the on/off switch is the platform's.
|
|
47
47
|
*/
|
|
48
48
|
async ingest(user, req) {
|
|
49
|
-
const total = { conversationId: null, accepted: 0, duplicates: 0, conflicts: []
|
|
49
|
+
const total = { conversationId: null, accepted: 0, duplicates: 0, conflicts: [] };
|
|
50
50
|
for (let i = 0; i < req.messages.length; i += MAX_MESSAGES_PER_REQUEST) {
|
|
51
51
|
const last = i + MAX_MESSAGES_PER_REQUEST >= req.messages.length;
|
|
52
52
|
const part = await this.http.request('POST', 'api/v1/ingest/messages', {
|
|
@@ -62,7 +62,6 @@ export class RecordareClient {
|
|
|
62
62
|
total.accepted += part.accepted;
|
|
63
63
|
total.duplicates += part.duplicates;
|
|
64
64
|
total.conflicts.push(...part.conflicts);
|
|
65
|
-
total.stored &&= part.stored;
|
|
66
65
|
}
|
|
67
66
|
return total;
|
|
68
67
|
}
|
|
@@ -74,6 +73,17 @@ export class RecordareClient {
|
|
|
74
73
|
context(user, conversation, query) {
|
|
75
74
|
return this.http.request('POST', 'api/v1/context', { user, body: { query }, headers: { [CONVERSATION_HEADER]: conversation } });
|
|
76
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Stores the turn and returns the memory context for it in one round trip (needs the `ingest` and `read` scopes):
|
|
78
|
+
* the context is computed for the ingested conversation, from `query` or else the turn's last user message.
|
|
79
|
+
*/
|
|
80
|
+
contextWithTurn(user, turn, query) {
|
|
81
|
+
return this.http.request('POST', 'api/v1/context', { user, body: { ingest: turn, ...(query ? { query } : {}) } });
|
|
82
|
+
}
|
|
83
|
+
/** The conversation ended (session closed, /new): extraction runs now. A conversation Recordare never had is done. */
|
|
84
|
+
async endConversation(user, conversation) {
|
|
85
|
+
await this.gone(this.http.request('POST', `api/v1/ingest/conversations/${enc(conversation)}/end`, { user }));
|
|
86
|
+
}
|
|
77
87
|
/** An edited message: Recordare re-extracts what depended on it. */
|
|
78
88
|
async editMessage(user, conversation, message, content) {
|
|
79
89
|
await this.http.request('PATCH', `api/v1/ingest/conversations/${enc(conversation)}/messages/${enc(message)}`, { user, body: { content } });
|
|
@@ -86,6 +96,29 @@ export class RecordareClient {
|
|
|
86
96
|
async deleteConversation(user, conversation) {
|
|
87
97
|
await this.gone(this.http.request('DELETE', `api/v1/ingest/conversations/${enc(conversation)}`, { user }));
|
|
88
98
|
}
|
|
99
|
+
// ── Learned sources (WORK_PLAN 8.9, D49): text the agent learns — the client turns files into text ─────────────
|
|
100
|
+
/**
|
|
101
|
+
* The agent learns a text. A big one is sent in parts (at paragraph boundaries, `partBytes` each); any size works.
|
|
102
|
+
* Sending the same `externalId` again replaces the source; the same first part again is a duplicate.
|
|
103
|
+
*/
|
|
104
|
+
async learnSource(user, source, partBytes = SOURCE_PART_BYTES) {
|
|
105
|
+
const [first = '', ...rest] = splitParts(source.text, partBytes);
|
|
106
|
+
let result = await this.http.request('POST', 'api/v1/ingest/sources', { user, body: { ...source, text: first, final: rest.length === 0 } });
|
|
107
|
+
if (result.duplicate)
|
|
108
|
+
return result;
|
|
109
|
+
for (const [i, text] of rest.entries()) {
|
|
110
|
+
result = await this.http.request('POST', `api/v1/ingest/sources/${enc(source.externalId)}/parts`, { user, body: { part: i + 1, text, final: i === rest.length - 1 } });
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/** The agent forgets a source (its text and passages; episodes keep a "forgotten source" marker). One it never had counts as done. */
|
|
115
|
+
async forgetSource(user, externalId) {
|
|
116
|
+
await this.http.request('DELETE', `api/v1/ingest/sources/${enc(externalId)}`, { user });
|
|
117
|
+
}
|
|
118
|
+
/** What the agent learned, newest first. */
|
|
119
|
+
sources(user) {
|
|
120
|
+
return this.http.request('GET', 'api/v1/sources', { user });
|
|
121
|
+
}
|
|
89
122
|
// ── The diary (API.md §4): what Recordare remembers, shown to the person in the platform's UI ──────────────────
|
|
90
123
|
/** The timeline, newest first; pass `nextCursor` back as `cursor` for the next page. */
|
|
91
124
|
episodes(user, query = {}) {
|
|
@@ -140,3 +173,40 @@ export class RecordareClient {
|
|
|
140
173
|
}
|
|
141
174
|
}
|
|
142
175
|
}
|
|
176
|
+
/** A text in parts of at most `maxBytes` UTF-8 bytes, cut at paragraph boundaries (inside a long paragraph: at a space, else between characters). */
|
|
177
|
+
export function splitParts(text, maxBytes) {
|
|
178
|
+
const size = (t) => Buffer.byteLength(t, 'utf8');
|
|
179
|
+
if (size(text) <= maxBytes)
|
|
180
|
+
return [text];
|
|
181
|
+
const parts = [];
|
|
182
|
+
let current = '';
|
|
183
|
+
const push = (piece) => {
|
|
184
|
+
const joined = current ? `${current}\n\n${piece}` : piece;
|
|
185
|
+
if (size(joined) <= maxBytes) {
|
|
186
|
+
current = joined;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (current)
|
|
190
|
+
parts.push(current);
|
|
191
|
+
current = piece;
|
|
192
|
+
};
|
|
193
|
+
for (const paragraph of text.split(/\n\s*\n/)) {
|
|
194
|
+
let rest = paragraph;
|
|
195
|
+
while (size(rest) > maxBytes) {
|
|
196
|
+
let end = rest.length;
|
|
197
|
+
while (end > 0 && size(rest.slice(0, end)) > maxBytes)
|
|
198
|
+
end = Math.floor(end * 0.9);
|
|
199
|
+
const space = rest.lastIndexOf(' ', end);
|
|
200
|
+
let cut = space > end / 2 ? space : end;
|
|
201
|
+
if (/[\uD800-\uDBFF]/.test(rest.charAt(cut - 1)))
|
|
202
|
+
cut--; // never between the halves of a surrogate pair
|
|
203
|
+
push(rest.slice(0, cut));
|
|
204
|
+
rest = rest.slice(cut).trimStart();
|
|
205
|
+
}
|
|
206
|
+
if (rest)
|
|
207
|
+
push(rest);
|
|
208
|
+
}
|
|
209
|
+
if (current)
|
|
210
|
+
parts.push(current);
|
|
211
|
+
return parts;
|
|
212
|
+
}
|
package/dist/contract.d.ts
CHANGED
|
@@ -5,21 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** The client's user the request acts for (with a client key). */
|
|
7
7
|
export declare const USER_HEADER = "X-Recordare-User";
|
|
8
|
-
/** The conversation a read happens in:
|
|
8
|
+
/** The conversation a read or write happens in: evidence of MCP writes, the current turn left out of recall (D50: no viewer filter). */
|
|
9
9
|
export declare const CONVERSATION_HEADER = "X-Recordare-Conversation";
|
|
10
10
|
/** Largest message content Recordare accepts (UTF-8 bytes). */
|
|
11
11
|
export declare const MAX_CONTENT_BYTES: number;
|
|
12
12
|
/** Largest number of messages in one ingest request. */
|
|
13
13
|
export declare const MAX_MESSAGES_PER_REQUEST = 500;
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A personal memory (the account holder is "I": what arrives without a declared identity is the memory's own), or an
|
|
16
|
+
* entity's (a home device, a robot, a place: what arrives undeclared is "someone"'s) — D50.
|
|
17
|
+
*/
|
|
18
|
+
export type MemoryMode = 'personal' | 'entity';
|
|
19
|
+
/** The grammatical gender of the memory's first person in gendered languages (default masculine). */
|
|
20
|
+
export type MemoryGender = 'masculine' | 'feminine' | 'neutral';
|
|
16
21
|
/** `GET api/v1/me`: who a request acts for. */
|
|
17
22
|
export interface Me {
|
|
18
|
-
|
|
23
|
+
memoryId: string;
|
|
19
24
|
displayName: string | null;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
episodicEnabled: boolean;
|
|
25
|
+
mode: MemoryMode;
|
|
26
|
+
gender: MemoryGender;
|
|
23
27
|
/** Recordare Atlas, when installed: link it for the platform's admins only (it shows every person's activity). */
|
|
24
28
|
atlasUrl?: string;
|
|
25
29
|
via: string;
|
|
@@ -30,7 +34,9 @@ export interface MeSettings {
|
|
|
30
34
|
/** Follows the platform's profile: send it again whenever the user renames themselves. */
|
|
31
35
|
displayName?: string;
|
|
32
36
|
/** Accepted only while the memory is empty (else MemoryNotEmptyError). */
|
|
33
|
-
|
|
37
|
+
mode?: MemoryMode;
|
|
38
|
+
/** From the account's profile; changes any time. */
|
|
39
|
+
gender?: MemoryGender;
|
|
34
40
|
}
|
|
35
41
|
export type ParticipantIdentity = {
|
|
36
42
|
externalUserId: string;
|
|
@@ -41,13 +47,19 @@ export type ParticipantIdentity = {
|
|
|
41
47
|
export interface IngestParticipant {
|
|
42
48
|
/** The conversation-local reference messages use in `authorRef`. */
|
|
43
49
|
ref: string;
|
|
44
|
-
role: '
|
|
50
|
+
role: 'holder' | 'assistant' | 'other';
|
|
45
51
|
displayName?: string;
|
|
46
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* The participant's id on the platform (its user id) or on a channel: Recordare links it to a contact of this memory
|
|
54
|
+
* (created on first sight) — never to another memory.
|
|
55
|
+
*/
|
|
47
56
|
identity?: ParticipantIdentity;
|
|
48
57
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Where a conversation comes from (`mcp_tool` is the service's own, never sent by a client). `document`: a document
|
|
60
|
+
* given to the agent; `perception`: what a device perceives; `ambient`: continuous listening.
|
|
61
|
+
*/
|
|
62
|
+
export type ConversationSource = 'chat' | 'voice' | 'import_chat' | 'import_social' | 'import_email' | 'import_notes' | 'interview' | 'document' | 'perception' | 'ambient';
|
|
51
63
|
export interface IngestConversation {
|
|
52
64
|
/** The platform's own stable id of the conversation. */
|
|
53
65
|
externalId: string;
|
|
@@ -69,6 +81,8 @@ export interface IngestMessage {
|
|
|
69
81
|
sentAt: string;
|
|
70
82
|
/** Replace the stored content of an existing message (an edit). */
|
|
71
83
|
upsert?: boolean;
|
|
84
|
+
/** The agent's own content (knowledge given to it, its perceptions, a document); `user` or `other` messages only. */
|
|
85
|
+
own?: boolean;
|
|
72
86
|
}
|
|
73
87
|
export interface IngestRequest {
|
|
74
88
|
conversation: IngestConversation;
|
|
@@ -79,12 +93,11 @@ export interface IngestRequest {
|
|
|
79
93
|
};
|
|
80
94
|
}
|
|
81
95
|
export interface IngestResult {
|
|
96
|
+
/** null only when no message was sent. */
|
|
82
97
|
conversationId: string | null;
|
|
83
98
|
accepted: number;
|
|
84
99
|
duplicates: number;
|
|
85
100
|
conflicts: string[];
|
|
86
|
-
/** false = consent not given yet: nothing was stored. */
|
|
87
|
-
stored: boolean;
|
|
88
101
|
}
|
|
89
102
|
export type PlanStatus = 'open' | 'confirmed' | 'cancelled' | 'rescheduled' | 'unresolved';
|
|
90
103
|
export type Precision = 'minute' | 'day' | 'month' | 'year' | 'approximate' | 'unknown';
|
|
@@ -184,9 +197,61 @@ export interface Note {
|
|
|
184
197
|
supportCount: number;
|
|
185
198
|
recordedAt: string;
|
|
186
199
|
}
|
|
200
|
+
/** Size of the parts `learnSource` sends a big text in (UTF-8 bytes; the service's default request limit is 16 MB). */
|
|
201
|
+
export declare const SOURCE_PART_BYTES: number;
|
|
202
|
+
export type SourceKind = 'document' | 'page' | 'note' | 'book' | 'own_text';
|
|
203
|
+
/** `POST api/v1/ingest/sources` (WORK_PLAN 8.9, D49): a text the agent learns — the client turns files into text. */
|
|
204
|
+
export interface LearnSource {
|
|
205
|
+
/** The client's own id for the source: sending it again replaces it (a new version). */
|
|
206
|
+
externalId: string;
|
|
207
|
+
title: string;
|
|
208
|
+
kind?: SourceKind;
|
|
209
|
+
author?: string;
|
|
210
|
+
uri?: string;
|
|
211
|
+
language?: string;
|
|
212
|
+
learnedAt?: string;
|
|
213
|
+
/** Who gave it: "me" (the memory's self), "someone", or a person by name. Default "me". */
|
|
214
|
+
providedBy?: 'me' | 'someone' | {
|
|
215
|
+
name: string;
|
|
216
|
+
};
|
|
217
|
+
/** The conversation it was learned in: its extraction then tells of it. */
|
|
218
|
+
conversation?: {
|
|
219
|
+
externalId: string;
|
|
220
|
+
};
|
|
221
|
+
text: string;
|
|
222
|
+
}
|
|
223
|
+
export interface LearnSourceResult {
|
|
224
|
+
sourceId: string;
|
|
225
|
+
status: 'receiving' | 'indexing' | 'ready';
|
|
226
|
+
parts: number;
|
|
227
|
+
passages: number;
|
|
228
|
+
duplicate: boolean;
|
|
229
|
+
}
|
|
230
|
+
/** `GET api/v1/sources`: a learned source (the Diary's "what I learned"). */
|
|
231
|
+
export interface Source {
|
|
232
|
+
id: string;
|
|
233
|
+
externalId: string;
|
|
234
|
+
title: string;
|
|
235
|
+
kind: string;
|
|
236
|
+
author: string | null;
|
|
237
|
+
uri: string | null;
|
|
238
|
+
providedBy: {
|
|
239
|
+
kind: 'self';
|
|
240
|
+
} | {
|
|
241
|
+
kind: 'someone';
|
|
242
|
+
} | {
|
|
243
|
+
kind: 'contact';
|
|
244
|
+
name: string;
|
|
245
|
+
};
|
|
246
|
+
learnedAt: string;
|
|
247
|
+
status: 'receiving' | 'indexing' | 'ready';
|
|
248
|
+
chars: number;
|
|
249
|
+
passages: number;
|
|
250
|
+
episodeIds: string[];
|
|
251
|
+
}
|
|
187
252
|
/** `POST api/v1/context`: the memories relevant to the message a host is about to answer (WORK_PLAN 5.7). */
|
|
188
253
|
export interface MemoryContext {
|
|
189
|
-
/** A fenced `<memory-context>` block to append to the prompt, or null (nothing relevant,
|
|
254
|
+
/** A fenced `<memory-context>` block to append to the prompt, or null (nothing relevant, or off). */
|
|
190
255
|
block: string | null;
|
|
191
256
|
items: number;
|
|
192
257
|
}
|
package/dist/contract.js
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** The client's user the request acts for (with a client key). */
|
|
9
9
|
export const USER_HEADER = 'X-Recordare-User';
|
|
10
|
-
/** The conversation a read happens in:
|
|
10
|
+
/** The conversation a read or write happens in: evidence of MCP writes, the current turn left out of recall (D50: no viewer filter). */
|
|
11
11
|
export const CONVERSATION_HEADER = 'X-Recordare-Conversation';
|
|
12
12
|
/** Largest message content Recordare accepts (UTF-8 bytes). */
|
|
13
13
|
export const MAX_CONTENT_BYTES = 64 * 1024;
|
|
14
14
|
/** Largest number of messages in one ingest request. */
|
|
15
15
|
export const MAX_MESSAGES_PER_REQUEST = 500;
|
|
16
|
+
/** Size of the parts `learnSource` sends a big text in (UTF-8 bytes; the service's default request limit is 16 MB). */
|
|
17
|
+
export const SOURCE_PART_BYTES = 4 * 1024 * 1024;
|
|
16
18
|
/**
|
|
17
19
|
* Cuts text to at most `maxBytes` UTF-8 bytes without splitting a character, ending with `marker` so a reader (and the
|
|
18
20
|
* memory engine) knows the text was cut. The marker counts within the limit.
|
package/dist/errors.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare class RecordareHttpError extends Error {
|
|
|
21
21
|
export declare class RecordareUnavailableError extends Error {
|
|
22
22
|
constructor(message: string, cause?: unknown);
|
|
23
23
|
}
|
|
24
|
-
/** The memory
|
|
24
|
+
/** The memory mode can change only while the memory is empty (`PATCH /me {mode}` → 409). */
|
|
25
25
|
export declare class MemoryNotEmptyError extends Error {
|
|
26
26
|
constructor();
|
|
27
27
|
}
|
package/dist/errors.js
CHANGED
|
@@ -23,10 +23,10 @@ export class RecordareUnavailableError extends Error {
|
|
|
23
23
|
this.name = 'RecordareUnavailableError';
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
/** The memory
|
|
26
|
+
/** The memory mode can change only while the memory is empty (`PATCH /me {mode}` → 409). */
|
|
27
27
|
export class MemoryNotEmptyError extends Error {
|
|
28
28
|
constructor() {
|
|
29
|
-
super('The memory
|
|
29
|
+
super('The memory mode can change only while the memory is empty');
|
|
30
30
|
this.name = 'MemoryNotEmptyError';
|
|
31
31
|
}
|
|
32
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export * from './errors.js';
|
|
|
4
4
|
export { type ClientOptions, type FetchLike, parseRetryAfter } from './http.js';
|
|
5
5
|
export { RecordareClient } from './client.js';
|
|
6
6
|
export { RecordareMcp, type McpOptions, type Tool, type ToolResult } from './mcp.js';
|
|
7
|
-
export { PersonDirectory, type
|
|
7
|
+
export { PersonDirectory, type Person, type PersonDirectoryOptions } from './people.js';
|
|
8
|
+
export { TOOLS } from './tools.js';
|
|
8
9
|
export { afterFailure, backoffMs, DEFAULT_DELIVERY, type DeliveryPolicy, type Next } from './delivery.js';
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,5 @@ export { parseRetryAfter } from './http.js';
|
|
|
7
7
|
export { RecordareClient } from './client.js';
|
|
8
8
|
export { RecordareMcp } from './mcp.js';
|
|
9
9
|
export { PersonDirectory } from './people.js';
|
|
10
|
+
export { TOOLS } from './tools.js';
|
|
10
11
|
export { afterFailure, backoffMs, DEFAULT_DELIVERY } from './delivery.js';
|
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright © 2026 Andrea Genovese
|
|
3
3
|
/**
|
|
4
|
-
* Recall over MCP with the official SDK (streamable HTTP). Recordare fixes the
|
|
5
|
-
* the
|
|
4
|
+
* Recall over MCP with the official SDK (streamable HTTP). Recordare fixes the memory when a session starts and resolves
|
|
5
|
+
* the conversation from its header (docs/API.md §1: evidence of writes), so a session belongs to one user AND one
|
|
6
6
|
* conversation: both headers are bound here, in code — neither the LLM nor the user can change them. Sessions are
|
|
7
7
|
* reused (bounded, least recently used closed first) and an expired one is re-opened once.
|
|
8
8
|
*/
|
|
@@ -80,7 +80,7 @@ export class RecordareMcp {
|
|
|
80
80
|
},
|
|
81
81
|
requestInit: { headers: { ...this.http.headers(user), [CONVERSATION_HEADER]: conversation } },
|
|
82
82
|
});
|
|
83
|
-
const client = new Client(this.options.clientInfo ?? { name: 'recordare-client', version: '0.
|
|
83
|
+
const client = new Client(this.options.clientInfo ?? { name: 'recordare-client', version: '0.2.0' });
|
|
84
84
|
await client.connect(transport);
|
|
85
85
|
return client;
|
|
86
86
|
}
|