@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 CHANGED
@@ -2,28 +2,40 @@
2
2
 
3
3
  *Italian version: [README_it.md](README_it.md).*
4
4
 
5
- The one client library every Recordare client uses (WORK_PLAN 6.7): identity and consent, the person's settings,
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
- // Who the user is, their consent, kind of memory and Atlas address; the name follows the platform's profile.
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.status('user-42'); // 'active' | 'waiting_activation' | 'unknown'
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). Nothing is stored before consent.
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 viewer context), both bound in code.
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
- nothing stored before consent, deletions propagate, the name and kind follow the platform, recall over MCP carries the
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
- ## Hosts that cannot install the package yet
48
- Recordare is not published: `scripts/sync-to.sh <dir>` copies the sources into a host repository with a header naming
49
- the source commit (Arkimede: `backend/src/recordare/client/`). The host never edits them; change the library here and
50
- sync again. When Recordare is published the host switches to the npm package by changing its imports.
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
- L'unica libreria client che ogni client di Recordare usa (WORK_PLAN 6.7): identità e consenso, impostazioni della
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
- // Chi è l'utente, il suo consenso, il tipo di memoria e l'indirizzo di Atlas; il nome segue il profilo sulla piattaforma.
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.status('user-42'); // 'active' | 'waiting_activation' | 'unknown'
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). Prima del consenso non si conserva nulla.
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 (il contesto di chi vede), entrambi fissati nel codice.
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, nulla conservato prima del consenso, le cancellazioni si propagano, nome e tipo seguono la piattaforma, il
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
- ## Host che non possono ancora installare il pacchetto
50
- Recordare non è pubblicato: `scripts/sync-to.sh <dir>` copia i sorgenti nel repository dell'host con un'intestazione che
51
- indica il commit di origine (Arkimede: `backend/src/recordare/client/`). L'host non li modifica mai; si cambia la
52
- libreria qui e si sincronizza di nuovo. Quando Recordare sarà pubblicato l'host passerà al pacchetto npm cambiando solo
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 and their consent, their settings, the conversations
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 person behind `user` (auto-provisioned when the client allows it), their consent and kind of memory. */
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 person's settings from the platform: the name (keep it in sync on every rename) and the kind of memory. */
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. Before consent nothing is stored (`stored: false`).
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 and their consent, their settings, the conversations
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 person behind `user` (auto-provisioned when the client allows it), their consent and kind of memory. */
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 person's settings from the platform: the name (keep it in sync on every rename) and the kind of memory. */
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. Before consent nothing is stored (`stored: false`).
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: [], stored: true };
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
+ }
@@ -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: Recordare resolves who will see the answer from it (viewer context). */
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
- /** A personal memory, or one shared by everyone using the account (an entity: a home device, a robot, a place). */
15
- export type MemoryKind = 'human' | 'entity';
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
- ownerId: string;
23
+ memoryId: string;
19
24
  displayName: string | null;
20
- kind: MemoryKind;
21
- /** The person's consent, given by the Recordare admin: until then ingest stores nothing. */
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
- kind?: MemoryKind;
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: 'owner' | 'assistant' | 'other';
50
+ role: 'holder' | 'assistant' | 'other';
45
51
  displayName?: string;
46
- /** A verified identity on the platform: lets Recordare link the participant to a known person. */
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
- /** Where a conversation comes from (`mcp_tool` is the service's own, never sent by a client). */
50
- export type ConversationSource = 'chat' | 'voice' | 'import_chat' | 'import_social' | 'import_email' | 'import_notes' | 'interview';
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, not owner-only, or off). */
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: Recordare resolves who will see the answer from it (viewer context). */
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 kind can change only while the memory is empty (`PATCH /me {kind}` → 409). */
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 kind can change only while the memory is empty (`PATCH /me {kind}` → 409). */
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 kind can change only while the memory is empty');
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 ConsentState, type Person, type PersonDirectoryOptions } from './people.js';
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 owner when a session starts and resolves
5
- * the viewer context from the conversation header (docs/API.md §1), so a session belongs to one user AND one
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.1.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
  }