memtrails 0.0.0-stage → 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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +221 -2
- package/dist/core/consolidate.d.ts +144 -0
- package/dist/core/consolidate.js +660 -0
- package/dist/core/embedder.d.ts +30 -0
- package/dist/core/embedder.js +137 -0
- package/dist/core/episodes.d.ts +32 -0
- package/dist/core/episodes.js +53 -0
- package/dist/core/extract.d.ts +142 -0
- package/dist/core/extract.js +452 -0
- package/dist/core/gather.d.ts +17 -0
- package/dist/core/gather.js +98 -0
- package/dist/core/graph.d.ts +48 -0
- package/dist/core/graph.js +208 -0
- package/dist/core/local-embedder.d.ts +40 -0
- package/dist/core/local-embedder.js +98 -0
- package/dist/core/model.d.ts +16 -0
- package/dist/core/model.js +74 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/profiles.d.ts +30 -0
- package/dist/core/profiles.js +147 -0
- package/dist/core/qdrant-binary.d.ts +25 -0
- package/dist/core/qdrant-binary.js +108 -0
- package/dist/core/qdrant-client.d.ts +115 -0
- package/dist/core/qdrant-client.js +327 -0
- package/dist/core/qdrant-process.d.ts +30 -0
- package/dist/core/qdrant-process.js +160 -0
- package/dist/core/rebuild.d.ts +31 -0
- package/dist/core/rebuild.js +109 -0
- package/dist/core/recall-log.d.ts +29 -0
- package/dist/core/recall-log.js +82 -0
- package/dist/core/recall.d.ts +91 -0
- package/dist/core/recall.js +172 -0
- package/dist/core/redact.d.ts +2 -0
- package/dist/core/redact.js +48 -0
- package/dist/core/reranker.d.ts +26 -0
- package/dist/core/reranker.js +67 -0
- package/dist/core/room.d.ts +20 -0
- package/dist/core/room.js +63 -0
- package/dist/core/sleep.d.ts +44 -0
- package/dist/core/sleep.js +199 -0
- package/dist/core/store.d.ts +198 -0
- package/dist/core/store.js +190 -0
- package/dist/core/text.d.ts +31 -0
- package/dist/core/text.js +101 -0
- package/dist/core/words.d.ts +32 -0
- package/dist/core/words.js +127 -0
- package/dist/core/writer.d.ts +36 -0
- package/dist/core/writer.js +146 -0
- package/dist/importers/files.d.ts +31 -0
- package/dist/importers/files.js +157 -0
- package/dist/importers/git.d.ts +30 -0
- package/dist/importers/git.js +118 -0
- package/dist/importers/index.d.ts +14 -0
- package/dist/importers/index.js +26 -0
- package/dist/importers/jsonl.d.ts +20 -0
- package/dist/importers/jsonl.js +65 -0
- package/dist/importers/mbox.d.ts +20 -0
- package/dist/importers/mbox.js +347 -0
- package/dist/importers/slack.d.ts +21 -0
- package/dist/importers/slack.js +134 -0
- package/dist/importers/text.d.ts +31 -0
- package/dist/importers/text.js +191 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +22 -0
- package/dist/llm.d.ts +60 -0
- package/dist/llm.js +60 -0
- package/dist/memory.d.ts +106 -0
- package/dist/memory.js +262 -0
- package/dist/records.d.ts +62 -0
- package/dist/records.js +107 -0
- package/dist/tools/agent-tools.d.ts +52 -0
- package/dist/tools/agent-tools.js +142 -0
- package/package.json +78 -4
package/dist/memory.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memtrails: open a memory, add anything with text, ask it things.
|
|
3
|
+
*
|
|
4
|
+
* const memory = await openMemory({ home: './memory' });
|
|
5
|
+
* await memory.add({ id: 'mail-1', kind: 'email', title: 'Gateway certificate', author: 'Ana',
|
|
6
|
+
* text: 'It expired on the 1st; the renewal job had been removed.', at: '2026-04-02' });
|
|
7
|
+
* await memory.search({ query: 'why did the certificate stop renewing?' });
|
|
8
|
+
*
|
|
9
|
+
* What is added is searchable at once, with no language model and no server: the embedding model runs here (fetched
|
|
10
|
+
* on first use) and orders results word by word (words.ts). A language model is needed only for `consolidate`,
|
|
11
|
+
* `sleep` and `gather`, which turn records into typed knowledge, dated observations and profiles — and memory works
|
|
12
|
+
* without them.
|
|
13
|
+
*
|
|
14
|
+
* The JSONL logs in `home` are the truth. The Qdrant index and the word vectors are derived: lost or out of date, they
|
|
15
|
+
* are written again from the logs.
|
|
16
|
+
*/
|
|
17
|
+
import { join, resolve } from 'node:path';
|
|
18
|
+
import { mkdirSync } from 'node:fs';
|
|
19
|
+
import { Consolidator } from './core/consolidate.js';
|
|
20
|
+
import { remoteEmbedder } from './core/embedder.js';
|
|
21
|
+
import { memoryGather } from './core/gather.js';
|
|
22
|
+
import { loadLocalEmbedder } from './core/local-embedder.js';
|
|
23
|
+
import { ensureModel } from './core/model.js';
|
|
24
|
+
import { ensureQdrant } from './core/qdrant-binary.js';
|
|
25
|
+
import { COLLECTION, QdrantClient } from './core/qdrant-client.js';
|
|
26
|
+
import { QdrantProcess } from './core/qdrant-process.js';
|
|
27
|
+
import { memoryExplore, memoryOpen, memorySearch } from './core/recall.js';
|
|
28
|
+
import { RecallBook } from './core/recall-log.js';
|
|
29
|
+
import { episodeBody } from './core/episodes.js';
|
|
30
|
+
import { MemoryRebuild } from './core/rebuild.js';
|
|
31
|
+
import { remoteReranker } from './core/reranker.js';
|
|
32
|
+
import { sentenceRoom, sentenceSpans } from './core/room.js';
|
|
33
|
+
import { Sleep } from './core/sleep.js';
|
|
34
|
+
import { ltmLog, stmLog, UsageBook } from './core/store.js';
|
|
35
|
+
import { WordStore, packWords, wordReranker } from './core/words.js';
|
|
36
|
+
import { rawKey, stmItem, stmTitle, writeLtm, writeStm } from './core/writer.js';
|
|
37
|
+
import { defaultAsk, openAiAsk } from './llm.js';
|
|
38
|
+
import { conversationEntries, entryOf, invalidRecord, recordUid } from './records.js';
|
|
39
|
+
import { buildAgentTools, lastedText, readableDate, searchText } from './tools/agent-tools.js';
|
|
40
|
+
const BATCH = 32;
|
|
41
|
+
const ROOM_TEXT = 1500;
|
|
42
|
+
const ROOM_CHARS = 6000;
|
|
43
|
+
const RECORD_LIMIT = 2_000_000;
|
|
44
|
+
export async function openMemory(o) {
|
|
45
|
+
const dir = resolve(o.home);
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
const log = o.log ?? (() => { });
|
|
48
|
+
// Embedding: here, or on a server.
|
|
49
|
+
const e = o.embedding ?? {};
|
|
50
|
+
const local = !e.server || e.modelDir
|
|
51
|
+
? await loadLocalEmbedder(e.modelDir ?? await ensureModel({ log }), { threads: e.threads ?? 2 })
|
|
52
|
+
: null;
|
|
53
|
+
const embed = e.server ? remoteEmbedder({ route: () => e.server }) : local.embed;
|
|
54
|
+
const encode = local?.words ?? null;
|
|
55
|
+
const words = new WordStore(join(dir, 'words.bin'));
|
|
56
|
+
await words.load();
|
|
57
|
+
// The index: a Qdrant of its own, or yours.
|
|
58
|
+
const q = o.qdrant;
|
|
59
|
+
let proc = null;
|
|
60
|
+
let endpoint;
|
|
61
|
+
if (q && 'url' in q) {
|
|
62
|
+
const fixed = { url: q.url.replace(/\/+$/, ''), apiKey: q.apiKey ?? '' };
|
|
63
|
+
endpoint = () => fixed;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
proc = new QdrantProcess({ binary: q && 'binary' in q ? q.binary : await ensureQdrant({ log }), dir: join(dir, 'qdrant'), log });
|
|
67
|
+
await proc.start();
|
|
68
|
+
endpoint = () => proc.current();
|
|
69
|
+
}
|
|
70
|
+
const client = new QdrantClient(endpoint, 10_000, { collection: q && 'url' in q ? q.collection ?? COLLECTION : COLLECTION });
|
|
71
|
+
await client.ensureCollection();
|
|
72
|
+
const stm = stmLog(join(dir, 'records.jsonl'));
|
|
73
|
+
const ltm = ltmLog(join(dir, 'knowledge.jsonl'));
|
|
74
|
+
await stm.load();
|
|
75
|
+
await ltm.load();
|
|
76
|
+
const usage = new UsageBook(join(dir, 'usage.json'));
|
|
77
|
+
const book = new RecallBook(join(dir, 'recall.jsonl'));
|
|
78
|
+
const ask = typeof o.llm === 'function' ? o.llm : o.llm ? openAiAsk(o.llm) : defaultAsk;
|
|
79
|
+
const base = { client, embed, stm, ltm, log, aborted: () => false, ask };
|
|
80
|
+
const rerank = o.rerank === false ? undefined
|
|
81
|
+
: typeof o.rerank === 'function' ? o.rerank
|
|
82
|
+
: o.rerank && typeof o.rerank === 'object' ? remoteReranker({ route: () => o.rerank })
|
|
83
|
+
: encode ? wordReranker(encode, words) : undefined;
|
|
84
|
+
const deps = { client, embed, rerank, used: (ids, what) => usage.note(ids, what), record: (ev) => book.record(ev) };
|
|
85
|
+
// The index is derived: empty while the logs are not (a new Qdrant, a deleted folder), it is written again.
|
|
86
|
+
const rebuild = new MemoryRebuild({
|
|
87
|
+
path: join(dir, 'rebuild.json'), stm, ltm, aborted: () => false,
|
|
88
|
+
writeStm: (xs) => writeStm(base, xs), writeLtm: (xs) => writeLtm(base, xs),
|
|
89
|
+
});
|
|
90
|
+
if (rebuild.pending() || (!(await client.count()) && (stm.size() || ltm.size()))) {
|
|
91
|
+
log('[memtrails] writing the index from the logs');
|
|
92
|
+
if (!rebuild.pending())
|
|
93
|
+
rebuild.begin();
|
|
94
|
+
await rebuild.run();
|
|
95
|
+
}
|
|
96
|
+
/** The text word by word scores an item by: its key, as search returns it. */
|
|
97
|
+
const keyText = (title, summary) => `${title}\n${summary}`;
|
|
98
|
+
/** A found item's whole text for the room: a record's own, or what consolidation wrote. */
|
|
99
|
+
const roomItem = (h) => {
|
|
100
|
+
const r = h.id.startsWith('stm:') ? stm.get(h.id.slice(4)) : null;
|
|
101
|
+
const it = r ? null : ltm.get(h.id);
|
|
102
|
+
const text = r ? `${r.request}\n${r.reply}` : it ? `${it.title}\n${it.summary}\n${it.body}` : `${h.title}\n${h.summary}`;
|
|
103
|
+
const told = h.ts ? new Date(h.ts).toISOString().slice(0, 10) : '';
|
|
104
|
+
const lasted = lastedText(h);
|
|
105
|
+
const happened = [h.eventAt ? readableDate(h.eventAt) : '', lasted].filter(Boolean).join(', ');
|
|
106
|
+
const when = happened ? `${happened}${h.eventAt !== told || lasted ? `; told ${readableDate(told)}` : ''}` : readableDate(told);
|
|
107
|
+
return { id: h.id, head: `(${when} · ${h.kind})`, text: text.slice(0, ROOM_TEXT), ts: h.eventAt ? Date.parse(h.eventAt) : h.ts };
|
|
108
|
+
};
|
|
109
|
+
const encodeMissing = async (texts) => {
|
|
110
|
+
if (!encode)
|
|
111
|
+
return 0;
|
|
112
|
+
const todo = [...new Set(texts)].filter((t) => t.trim() && !words.get(t));
|
|
113
|
+
for (let i = 0; i < todo.length; i += BATCH) {
|
|
114
|
+
const part = todo.slice(i, i + BATCH);
|
|
115
|
+
const encoded = await encode(part, 'document');
|
|
116
|
+
await words.put(part.map((t, n) => [t, packWords(encoded[n])]));
|
|
117
|
+
}
|
|
118
|
+
return todo.length;
|
|
119
|
+
};
|
|
120
|
+
const wordsOfEntries = (es) => es.flatMap((x) => {
|
|
121
|
+
const room = `${x.request}\n${x.reply}`.slice(0, ROOM_TEXT);
|
|
122
|
+
return [keyText(stmTitle(x), rawKey(x)), ...sentenceSpans(room).map(([a, b]) => room.slice(a, b))];
|
|
123
|
+
});
|
|
124
|
+
const write = async (entries, total) => {
|
|
125
|
+
const fresh = entries.filter((x) => stm.meta(x.uid)?.fp !== x.fingerprint);
|
|
126
|
+
for (let i = 0; i < fresh.length; i += BATCH) {
|
|
127
|
+
const part = fresh.slice(i, i + BATCH);
|
|
128
|
+
await writeStm(base, part);
|
|
129
|
+
stm.put(part);
|
|
130
|
+
await encodeMissing(wordsOfEntries(part));
|
|
131
|
+
}
|
|
132
|
+
stm.compact();
|
|
133
|
+
return { added: fresh.length, unchanged: total - fresh.length };
|
|
134
|
+
};
|
|
135
|
+
const memory = {
|
|
136
|
+
async add(records) {
|
|
137
|
+
const list = Array.isArray(records) ? records : [records];
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (const r of list) {
|
|
140
|
+
const bad = invalidRecord(r);
|
|
141
|
+
if (bad)
|
|
142
|
+
throw new TypeError(bad);
|
|
143
|
+
if (r.text.length > RECORD_LIMIT)
|
|
144
|
+
throw new TypeError(`record ${r.id}: longer than ${RECORD_LIMIT} characters; split it`);
|
|
145
|
+
}
|
|
146
|
+
return write(list.map((r) => entryOf(r, now)), list.length);
|
|
147
|
+
},
|
|
148
|
+
async addConversation(c) {
|
|
149
|
+
if (!c?.id || !Array.isArray(c.turns))
|
|
150
|
+
throw new TypeError('a conversation needs an id and turns');
|
|
151
|
+
const entries = conversationEntries(c, Date.now());
|
|
152
|
+
return write(entries, entries.length);
|
|
153
|
+
},
|
|
154
|
+
async import(records, opts = {}) {
|
|
155
|
+
const size = opts.batch ?? 256;
|
|
156
|
+
let batch = [];
|
|
157
|
+
const total = { added: 0, unchanged: 0 };
|
|
158
|
+
const flush = async () => {
|
|
159
|
+
if (!batch.length)
|
|
160
|
+
return;
|
|
161
|
+
const r = await memory.add(batch);
|
|
162
|
+
total.added += r.added;
|
|
163
|
+
total.unchanged += r.unchanged;
|
|
164
|
+
opts.onProgress?.(total.added + total.unchanged);
|
|
165
|
+
batch = [];
|
|
166
|
+
};
|
|
167
|
+
for await (const r of records) {
|
|
168
|
+
batch.push(r);
|
|
169
|
+
if (batch.length >= size)
|
|
170
|
+
await flush();
|
|
171
|
+
}
|
|
172
|
+
await flush();
|
|
173
|
+
return total;
|
|
174
|
+
},
|
|
175
|
+
async remove(ids) {
|
|
176
|
+
const uids = ids.map((x) => recordUid(typeof x === 'string' ? { id: x } : x)).filter((u) => stm.meta(u));
|
|
177
|
+
if (!uids.length)
|
|
178
|
+
return 0;
|
|
179
|
+
await client.deleteItems(uids.flatMap((u) => [stmItem(u), `ep:${u}`]));
|
|
180
|
+
stm.remove(uids);
|
|
181
|
+
return uids.length;
|
|
182
|
+
},
|
|
183
|
+
search: (s) => memorySearch(deps, s),
|
|
184
|
+
searchText: async (s) => searchText(await memorySearch(deps, s), s.order ?? 'relevance'),
|
|
185
|
+
async room(s) {
|
|
186
|
+
if (!encode)
|
|
187
|
+
throw new Error('the room needs the local embedding model (embedding.modelDir, or no embedding.server)');
|
|
188
|
+
const found = await memorySearch(deps, { ...s, limit: s.limit ?? 20 });
|
|
189
|
+
return sentenceRoom(s.query, found.map(roomItem), encode, words, s.chars ?? ROOM_CHARS);
|
|
190
|
+
},
|
|
191
|
+
async open(id) {
|
|
192
|
+
const p = await memoryOpen(deps, id);
|
|
193
|
+
if (!p)
|
|
194
|
+
return null;
|
|
195
|
+
// A record: its kind as given. Something learned: the records it was learned from, whole.
|
|
196
|
+
if (p.tier === 'stm')
|
|
197
|
+
return { ...p, kind: p.engine };
|
|
198
|
+
const it = ltm.get(id);
|
|
199
|
+
const sources = (it?.provenance ?? []).map((u) => stm.get(u)).filter((x) => !!x)
|
|
200
|
+
.map((x) => ({ id: stmItem(x.uid), when: x.ts, title: x.title, body: episodeBody(x), ...(x.record ? { record: x.record } : {}) }));
|
|
201
|
+
return sources.length ? { ...p, sources } : p;
|
|
202
|
+
},
|
|
203
|
+
explore: (s) => memoryExplore(deps, s),
|
|
204
|
+
gather: (s) => memoryGather(deps, s, { ask, ...(s.limit ? { limit: s.limit } : {}) }),
|
|
205
|
+
async consolidate() {
|
|
206
|
+
const worker = new Consolidator({ ...base, concurrency: o.concurrency ?? 4 });
|
|
207
|
+
const total = { windows: 0, added: 0, merged: 0, updated: 0, superseded: 0, linked: 0, dropped: 0, episodes: 0, observed: 0, cued: 0 };
|
|
208
|
+
for (let round = 0; round < 1000 && worker.pending().length; round++) {
|
|
209
|
+
const r = await worker.run(64);
|
|
210
|
+
for (const k of Object.keys(total))
|
|
211
|
+
if (typeof r[k] === 'number')
|
|
212
|
+
total[k] += r[k];
|
|
213
|
+
if (r.paused !== undefined) {
|
|
214
|
+
total.paused = r.paused;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
if (!r.windows)
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
await memory.indexWords();
|
|
221
|
+
return total;
|
|
222
|
+
},
|
|
223
|
+
async sleep() {
|
|
224
|
+
const r = await new Sleep({ ...base, usage, statePath: join(dir, 'sleep.json') }).run();
|
|
225
|
+
await memory.indexWords();
|
|
226
|
+
return r;
|
|
227
|
+
},
|
|
228
|
+
async indexWords() {
|
|
229
|
+
if (!encode)
|
|
230
|
+
return 0;
|
|
231
|
+
const texts = [...stm.all()].flatMap((x) => wordsOfEntries([x]));
|
|
232
|
+
for (const [uid, m] of ltm.entries()) {
|
|
233
|
+
if (m.status !== 'active')
|
|
234
|
+
continue;
|
|
235
|
+
const it = ltm.get(uid);
|
|
236
|
+
if (!it)
|
|
237
|
+
continue;
|
|
238
|
+
const room = `${it.title}\n${it.summary}\n${it.body}`.slice(0, ROOM_TEXT);
|
|
239
|
+
texts.push(keyText(it.title, it.summary), ...sentenceSpans(room).map(([a, b]) => room.slice(a, b)));
|
|
240
|
+
}
|
|
241
|
+
return encodeMissing(texts);
|
|
242
|
+
},
|
|
243
|
+
async rebuild() {
|
|
244
|
+
await client.dropCollection();
|
|
245
|
+
await client.ensureCollection();
|
|
246
|
+
rebuild.begin();
|
|
247
|
+
await rebuild.run();
|
|
248
|
+
},
|
|
249
|
+
tools: () => buildAgentTools({
|
|
250
|
+
search: (s) => memorySearch(deps, s),
|
|
251
|
+
open: (id) => memory.open(id),
|
|
252
|
+
explore: (s) => memoryExplore(deps, s),
|
|
253
|
+
}),
|
|
254
|
+
stats: () => ({ records: stm.size(), items: [...ltm.entries()].filter(([, m]) => m.status === 'active').length, words: words.size }),
|
|
255
|
+
async close() {
|
|
256
|
+
usage.flush();
|
|
257
|
+
await proc?.stopAndWait();
|
|
258
|
+
await local?.close();
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
return memory;
|
|
262
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { StmEntry } from './core/store.js';
|
|
2
|
+
/** One piece of information to remember. */
|
|
3
|
+
export interface MemoryRecord {
|
|
4
|
+
/** Stable and unique within its `source`: the same id added again replaces the record (unchanged: nothing is done). */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The content. */
|
|
7
|
+
text: string;
|
|
8
|
+
/** A subject, headline or name; when absent, the first line of the text. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** What it is, in one word of your choosing: `email`, `message`, `note`, `memo`, `article`, `commit`, `ticket`, … */
|
|
11
|
+
kind?: string;
|
|
12
|
+
/** Where it came from: a mailbox, a workspace, a feed, a repository. Part of the record's identity. */
|
|
13
|
+
source?: string;
|
|
14
|
+
/** What it belongs to, for searches scoped to one collection: a project, a team, a customer, a person. */
|
|
15
|
+
collection?: string;
|
|
16
|
+
/** Records that belong together share a thread (an e-mail thread, a chat thread, a repository's history). */
|
|
17
|
+
thread?: string;
|
|
18
|
+
/** Who wrote it. */
|
|
19
|
+
author?: string;
|
|
20
|
+
/** When it was written: milliseconds since the epoch, an ISO date, or a Date. Default: when it was added. */
|
|
21
|
+
at?: number | string | Date;
|
|
22
|
+
/** Where to open it. */
|
|
23
|
+
url?: string;
|
|
24
|
+
/** Anything else, kept with the record and returned with it. Not searched. */
|
|
25
|
+
metadata?: Record<string, string | number | boolean | null>;
|
|
26
|
+
}
|
|
27
|
+
/** A conversation: turns between a person and an assistant (or any two parties), in order. */
|
|
28
|
+
export interface Conversation {
|
|
29
|
+
/** Stable: the same id added again is read again, not doubled. */
|
|
30
|
+
id: string;
|
|
31
|
+
collection?: string;
|
|
32
|
+
source?: string;
|
|
33
|
+
/** When it began (each turn may carry its own `at`). */
|
|
34
|
+
at?: number | string | Date;
|
|
35
|
+
turns: Array<{
|
|
36
|
+
role: 'user' | 'assistant';
|
|
37
|
+
text: string;
|
|
38
|
+
author?: string;
|
|
39
|
+
at?: number | string | Date;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/** The collection records without one belong to. */
|
|
43
|
+
export declare const DEFAULT_COLLECTION = "default";
|
|
44
|
+
/** A time as milliseconds; undefined for nothing or something unreadable. */
|
|
45
|
+
export declare function timeOf(at: number | string | Date | undefined): number | undefined;
|
|
46
|
+
/** A collection as the index keys it: one path segment, any script kept. */
|
|
47
|
+
export declare function collectionKey(collection: string | undefined): string;
|
|
48
|
+
/** A record's id in memory: its source and id, hashed (any characters may be in either). */
|
|
49
|
+
export declare function recordUid(r: Pick<MemoryRecord, 'id' | 'source'>): string;
|
|
50
|
+
/** Checks a record and says what is wrong with it, or null. */
|
|
51
|
+
export declare function invalidRecord(r: MemoryRecord): string | null;
|
|
52
|
+
/**
|
|
53
|
+
* A record as memory keeps it: the title is what it is about (the first line when there is none), the text after the
|
|
54
|
+
* author is the content. Searchable as written: its start is its key, and every word of it is in a chunk.
|
|
55
|
+
*/
|
|
56
|
+
export declare function entryOf(r: MemoryRecord, now: number, turn?: number): StmEntry;
|
|
57
|
+
/**
|
|
58
|
+
* A conversation's turns as records: each time the person speaks, what they said is the request and what the other
|
|
59
|
+
* side answered until they speak again is its reply — one record a request, so a question finds the exchange it was in.
|
|
60
|
+
* Turns before the first request (an assistant that opened) are the first record's own.
|
|
61
|
+
*/
|
|
62
|
+
export declare function conversationEntries(c: Conversation, now: number): StmEntry[];
|
package/dist/records.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What memtrails stores: records. Anything with text is a record — an e-mail, a chat message, a note, a memo, a news
|
|
3
|
+
* article, a commit message, a ticket, a page of a document. Records that belong together (an e-mail thread, a chat
|
|
4
|
+
* thread, a meeting's notes) share a `thread`; a conversation between a person and an assistant is a thread of turns.
|
|
5
|
+
*
|
|
6
|
+
* Every record is searchable the moment it is added, with no model asked. Consolidation (optional, with a language
|
|
7
|
+
* model) reads threads in windows and keeps typed knowledge from them; the records themselves are never lost.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
/** The collection records without one belong to. */
|
|
11
|
+
export const DEFAULT_COLLECTION = 'default';
|
|
12
|
+
/** A time as milliseconds; undefined for nothing or something unreadable. */
|
|
13
|
+
export function timeOf(at) {
|
|
14
|
+
if (at === undefined || at === null || at === '')
|
|
15
|
+
return undefined;
|
|
16
|
+
const t = at instanceof Date ? at.getTime() : typeof at === 'number' ? at : Date.parse(at);
|
|
17
|
+
return Number.isFinite(t) ? t : undefined;
|
|
18
|
+
}
|
|
19
|
+
/** A collection as the index keys it: one path segment, any script kept. */
|
|
20
|
+
export function collectionKey(collection) {
|
|
21
|
+
const c = (collection ?? '').trim();
|
|
22
|
+
return c ? c.replace(/[\s/\\:]+/g, '-') : DEFAULT_COLLECTION;
|
|
23
|
+
}
|
|
24
|
+
function hash(...parts) {
|
|
25
|
+
return createHash('sha256').update(parts.map((p) => p ?? '').join('\u0000')).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
/** A record's id in memory: its source and id, hashed (any characters may be in either). */
|
|
28
|
+
export function recordUid(r) {
|
|
29
|
+
return hash(r.source ?? '', r.id).slice(0, 32);
|
|
30
|
+
}
|
|
31
|
+
/** Checks a record and says what is wrong with it, or null. */
|
|
32
|
+
export function invalidRecord(r) {
|
|
33
|
+
if (!r || typeof r !== 'object')
|
|
34
|
+
return 'a record must be an object';
|
|
35
|
+
if (typeof r.id !== 'string' || !r.id.trim())
|
|
36
|
+
return 'a record needs an id';
|
|
37
|
+
if (typeof r.text !== 'string')
|
|
38
|
+
return `record ${r.id}: text must be a string`;
|
|
39
|
+
if (!r.text.trim() && !r.title?.trim())
|
|
40
|
+
return `record ${r.id}: it has no text and no title`;
|
|
41
|
+
if (r.at !== undefined && timeOf(r.at) === undefined)
|
|
42
|
+
return `record ${r.id}: its time cannot be read (${String(r.at)})`;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A record as memory keeps it: the title is what it is about (the first line when there is none), the text after the
|
|
47
|
+
* author is the content. Searchable as written: its start is its key, and every word of it is in a chunk.
|
|
48
|
+
*/
|
|
49
|
+
export function entryOf(r, now, turn = 0) {
|
|
50
|
+
const text = r.text.trim();
|
|
51
|
+
const firstLine = text.split('\n').map((l) => l.trim()).find(Boolean) ?? '';
|
|
52
|
+
const title = r.title?.trim() || firstLine;
|
|
53
|
+
// Without a title its first line is the title, and not said twice.
|
|
54
|
+
const body = r.title?.trim() ? text : text.slice(text.indexOf(firstLine) + firstLine.length).trim();
|
|
55
|
+
const by = r.author?.trim() ? `${r.author.trim()}: ` : '';
|
|
56
|
+
const uid = recordUid(r);
|
|
57
|
+
const reply = `${by}${body}`;
|
|
58
|
+
return {
|
|
59
|
+
uid, fingerprint: hash(title, reply, r.url, r.kind, r.collection, r.thread, String(timeOf(r.at) ?? ''), JSON.stringify(r.metadata ?? {})),
|
|
60
|
+
engine: r.kind?.trim() || 'record', session: r.thread?.trim() || uid, turn, cwd: null, project: collectionKey(r.collection),
|
|
61
|
+
title, ts: timeOf(r.at) ?? now, synthetic: false, typed: 'human', request: title, reply, said: reply, steps: [],
|
|
62
|
+
files: r.url ? [r.url] : [], done: false,
|
|
63
|
+
record: { id: r.id, ...(r.source ? { source: r.source } : {}), ...(r.author ? { author: r.author } : {}), ...(r.url ? { url: r.url } : {}),
|
|
64
|
+
...(r.metadata ? { metadata: r.metadata } : {}) },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A conversation's turns as records: each time the person speaks, what they said is the request and what the other
|
|
69
|
+
* side answered until they speak again is its reply — one record a request, so a question finds the exchange it was in.
|
|
70
|
+
* Turns before the first request (an assistant that opened) are the first record's own.
|
|
71
|
+
*/
|
|
72
|
+
export function conversationEntries(c, now) {
|
|
73
|
+
const start = timeOf(c.at) ?? now;
|
|
74
|
+
const out = [];
|
|
75
|
+
let request = [];
|
|
76
|
+
let reply = [];
|
|
77
|
+
let at;
|
|
78
|
+
const flush = () => {
|
|
79
|
+
if (!request.length && !reply.length)
|
|
80
|
+
return;
|
|
81
|
+
const n = out.length;
|
|
82
|
+
const e = entryOf({ id: `${c.id}#${n}`, source: c.source, collection: c.collection, thread: c.id, kind: 'conversation',
|
|
83
|
+
title: request.join('\n').split('\n').find((l) => l.trim())?.trim() || reply.join('\n').split('\n').find((l) => l.trim())?.trim() || c.id,
|
|
84
|
+
text: request.join('\n'), at: at ?? start + n * 60_000 }, now, n);
|
|
85
|
+
out.push({ ...e, request: request.join('\n') || e.title, reply: reply.join('\n'), said: reply.join('\n') });
|
|
86
|
+
request = [];
|
|
87
|
+
reply = [];
|
|
88
|
+
at = undefined;
|
|
89
|
+
};
|
|
90
|
+
for (const t of c.turns) {
|
|
91
|
+
const text = `${t.author?.trim() ? `${t.author.trim()}: ` : ''}${t.text.trim()}`;
|
|
92
|
+
if (!t.text.trim())
|
|
93
|
+
continue;
|
|
94
|
+
if (t.role === 'user') {
|
|
95
|
+
if (reply.length)
|
|
96
|
+
flush();
|
|
97
|
+
request.push(text);
|
|
98
|
+
at ??= timeOf(t.at);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
reply.push(text);
|
|
102
|
+
at ??= timeOf(t.at);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
flush();
|
|
106
|
+
return out.map((e) => ({ ...e, fingerprint: hash(e.fingerprint, e.request, e.reply) }));
|
|
107
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An agent's tools over a memory: `memory_search` (find), `memory_open` (read one whole, with the records it was learned
|
|
3
|
+
* from) and `memory_explore` (from a topic or items to what is related but not found by search). Defined with zod
|
|
4
|
+
* schemas, as MCP servers declare tools; `jsonSchema` gives the same input schema for function calling APIs.
|
|
5
|
+
*
|
|
6
|
+
* const tools = memory.tools(); // hand them to an MCP server, or to a model's tool calling
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { Explored, RecalledItem, SearchQuery } from '../core/recall.js';
|
|
10
|
+
type ToolResult = {
|
|
11
|
+
content: Array<{
|
|
12
|
+
type: 'text';
|
|
13
|
+
text: string;
|
|
14
|
+
}>;
|
|
15
|
+
isError?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export interface AgentTool {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
inputSchema: Record<string, z.ZodType>;
|
|
21
|
+
/** The input schema as JSON Schema, for function calling. */
|
|
22
|
+
jsonSchema: Record<string, unknown>;
|
|
23
|
+
handler: (args: any, extra?: unknown) => Promise<ToolResult>;
|
|
24
|
+
}
|
|
25
|
+
/** What the tools read: a memory (openMemory) gives these. */
|
|
26
|
+
export interface ToolProvider {
|
|
27
|
+
search(q: SearchQuery): Promise<RecalledItem[]>;
|
|
28
|
+
open(id: string): Promise<Record<string, unknown> | null>;
|
|
29
|
+
explore?(q: {
|
|
30
|
+
topic?: string;
|
|
31
|
+
from?: string[];
|
|
32
|
+
collections?: string[];
|
|
33
|
+
limit?: number;
|
|
34
|
+
}): Promise<Explored>;
|
|
35
|
+
}
|
|
36
|
+
type CrossMemoryHit = RecalledItem;
|
|
37
|
+
/** A date as a reader reckons with it — the weekday too ("the Friday before …"): 2023-07-14 → "Fri 14 Jul 2023". */
|
|
38
|
+
export declare function readableDate(d: string): string;
|
|
39
|
+
/** How long something lasted, as a reader reckons with it: "since Feb 2022", "Nov 2021 – May 2022". */
|
|
40
|
+
export declare function lastedText(h: {
|
|
41
|
+
startAt?: unknown;
|
|
42
|
+
endAt?: unknown;
|
|
43
|
+
}): string;
|
|
44
|
+
/** All of a profile — what memory knows about one subject, one dated fact a line (core/profiles.ts) — up to this:
|
|
45
|
+
* measured, a profile cut like any other item lost most of its gain. */
|
|
46
|
+
export declare const PROFILE_SHOWN = 1500;
|
|
47
|
+
/** What memory_search shows: an observation as one dated line (when it happened, and when it was told when that
|
|
48
|
+
* differs), a profile whole (up to PROFILE_SHOWN), anything else as its title line and summary — shorter when many
|
|
49
|
+
* are asked for. Best first, or by time. */
|
|
50
|
+
export declare function searchText(hits: CrossMemoryHit[], order?: 'relevance' | 'time'): string;
|
|
51
|
+
export declare function buildAgentTools(p: ToolProvider): AgentTool[];
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An agent's tools over a memory: `memory_search` (find), `memory_open` (read one whole, with the records it was learned
|
|
3
|
+
* from) and `memory_explore` (from a topic or items to what is related but not found by search). Defined with zod
|
|
4
|
+
* schemas, as MCP servers declare tools; `jsonSchema` gives the same input schema for function calling APIs.
|
|
5
|
+
*
|
|
6
|
+
* const tools = memory.tools(); // hand them to an MCP server, or to a model's tool calling
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
function tool(name, description, inputSchema, handler) {
|
|
10
|
+
return { name, description, inputSchema, jsonSchema: z.toJSONSchema(z.object(inputSchema)), handler: handler };
|
|
11
|
+
}
|
|
12
|
+
const DESCRIPTIONS = {
|
|
13
|
+
memory_search: 'Search memory: everything it was given (e-mails, messages, notes, documents, conversations, commits, …) and '
|
|
14
|
+
+ 'what was learned from them — facts, decisions, dated events, profiles of people and things. Best first; each result '
|
|
15
|
+
+ 'with its date and id. Ask in plain words, as you would ask a colleague who read it all. Name dates when the question '
|
|
16
|
+
+ 'names a time. Open a result with memory_open for all of it.',
|
|
17
|
+
memory_open: 'Read one memory item whole, by the id memory_search or memory_explore gave: everything it holds, when it '
|
|
18
|
+
+ 'happened, and for something learned, the records it was learned from.',
|
|
19
|
+
memory_explore: 'Explore memory around a topic, or around items you already found: it follows what they share — the same '
|
|
20
|
+
+ 'people, places, systems, names — and links between memories, to what a search for the words does not find. Returns '
|
|
21
|
+
+ 'what search found, what is related, and the concepts around it to explore next.',
|
|
22
|
+
};
|
|
23
|
+
const day = (ts) => (ts ? new Date(ts).toISOString().slice(0, 10) : '');
|
|
24
|
+
const hitLine = (h, n) => `${n + 1}. ${h.title} (${h.kind === 'episode' ? 'earlier work' : h.kind} · ${day(h.ts)}${h.record?.author ? ` · ${h.record.author}` : ''} · id ${h.id})`;
|
|
25
|
+
const WEEKDAY = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
26
|
+
const MONTH = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
27
|
+
/** A date as a reader reckons with it — the weekday too ("the Friday before …"): 2023-07-14 → "Fri 14 Jul 2023". */
|
|
28
|
+
export function readableDate(d) {
|
|
29
|
+
const [y, m, dd] = d.split('-').map(Number);
|
|
30
|
+
if (!y)
|
|
31
|
+
return '';
|
|
32
|
+
if (!m)
|
|
33
|
+
return String(y);
|
|
34
|
+
if (!dd)
|
|
35
|
+
return `${MONTH[m - 1]} ${y}`;
|
|
36
|
+
return `${WEEKDAY[new Date(Date.UTC(y, m - 1, dd)).getUTCDay()]} ${dd} ${MONTH[m - 1]} ${y}`;
|
|
37
|
+
}
|
|
38
|
+
/** How long something lasted, as a reader reckons with it: "since Feb 2022", "Nov 2021 – May 2022". */
|
|
39
|
+
export function lastedText(h) {
|
|
40
|
+
const [from, to] = [h.startAt, h.endAt].map((d) => (typeof d === 'string' && d ? readableDate(d) : ''));
|
|
41
|
+
if (from && to)
|
|
42
|
+
return `${from} – ${to}`;
|
|
43
|
+
return from ? `since ${from}` : to ? `until ${to}` : '';
|
|
44
|
+
}
|
|
45
|
+
/** When a hit happened, for ordering by time: an observation's own date, else when it was learned. */
|
|
46
|
+
const when = (h) => h.eventAt || day(h.ts);
|
|
47
|
+
/** All of a profile — what memory knows about one subject, one dated fact a line (core/profiles.ts) — up to this:
|
|
48
|
+
* measured, a profile cut like any other item lost most of its gain. */
|
|
49
|
+
export const PROFILE_SHOWN = 1500;
|
|
50
|
+
const cutAt = (s, n) => (s.length > n ? `${s.slice(0, n)}…` : s);
|
|
51
|
+
/** What memory_search shows: an observation as one dated line (when it happened, and when it was told when that
|
|
52
|
+
* differs), a profile whole (up to PROFILE_SHOWN), anything else as its title line and summary — shorter when many
|
|
53
|
+
* are asked for. Best first, or by time. */
|
|
54
|
+
export function searchText(hits, order = 'relevance') {
|
|
55
|
+
const list = order === 'time' ? [...hits].sort((a, b) => when(a).localeCompare(when(b))) : hits;
|
|
56
|
+
// Many hits: one line each — the date, what it is, its summary — so a list question sees every one of them clearly.
|
|
57
|
+
const compact = list.length > 20;
|
|
58
|
+
const lines = list.map((h, n) => {
|
|
59
|
+
if (h.kind === 'profile')
|
|
60
|
+
return `${hitLine(h, n)}\n ${cutAt(h.summary, PROFILE_SHOWN).replace(/\n/g, '\n ')}`;
|
|
61
|
+
if (h.kind !== 'observation') {
|
|
62
|
+
// The passage that matched, when it was not the start: a fact said deep in a long request is what was found.
|
|
63
|
+
const matched = h.match ? ` … ${h.match.replace(/\s+/g, ' ')}` : '';
|
|
64
|
+
if (!compact)
|
|
65
|
+
return `${hitLine(h, n)}\n ${h.summary.slice(0, h.match ? 200 : 500)}${matched}`;
|
|
66
|
+
const what = h.kind === 'episode' ? 'earlier work' : h.kind;
|
|
67
|
+
return `${n + 1}. (${readableDate(day(h.ts))} · ${what}) ${h.summary.replace(/\s+/g, ' ').slice(0, h.match ? 150 : 300)}${matched} (id ${h.id})`;
|
|
68
|
+
}
|
|
69
|
+
const told = day(h.ts);
|
|
70
|
+
const lasted = lastedText(h);
|
|
71
|
+
const happened = [h.eventAt ? readableDate(h.eventAt) : '', lasted].filter(Boolean).join(', ');
|
|
72
|
+
const at = happened ? `${happened}${h.eventAt !== told || lasted ? `; told ${readableDate(told)}` : ''}` : `told ${readableDate(told)}`;
|
|
73
|
+
return `${n + 1}. (${at}) ${h.summary} (id ${h.id})`;
|
|
74
|
+
});
|
|
75
|
+
return `${order === 'time' ? 'Oldest first — the date is when it happened (and when it was told, when that differs).\n' : ''}${lines.join('\n')}`;
|
|
76
|
+
}
|
|
77
|
+
function memoryTools(p) {
|
|
78
|
+
return [
|
|
79
|
+
tool('memory_search', DESCRIPTIONS.memory_search, {
|
|
80
|
+
query: z.string().describe('What you need to know, in plain words.'),
|
|
81
|
+
collections: z.array(z.string()).optional().describe('Only these collections (default: all).'),
|
|
82
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many of each to return — dated observations, and other memories (default 20; 100 to 200 when you gather everything on a topic, list or count things, or need their dates).'),
|
|
83
|
+
order: z.enum(['relevance', 'time']).optional().describe('Best first (default), or oldest first — for what happened when, and in what order.'),
|
|
84
|
+
from: z.string().regex(/^\d{4}(-\d{2}(-\d{2})?)?$/).optional().describe('Only what happened on or after this date (YYYY-MM-DD, YYYY-MM or YYYY) — when the request names a time ("in June 2023", "last week").'),
|
|
85
|
+
to: z.string().regex(/^\d{4}(-\d{2}(-\d{2})?)?$/).optional().describe('Only what happened on or before this date (YYYY-MM-DD, YYYY-MM or YYYY).'),
|
|
86
|
+
}, async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
const hits = await p.search({ query: args.query, collections: args.collections, limit: args.limit ?? 20, from: args.from, to: args.to });
|
|
89
|
+
if (!hits.length)
|
|
90
|
+
return text('Nothing in memory matches that.');
|
|
91
|
+
return text(`${searchText(hits, args.order)}\n\nOpen one with memory_open and its id. It can be stale: check it against the current state before relying on it.`);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return text(err.message, true);
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
tool('memory_open', DESCRIPTIONS.memory_open, { id: z.string().describe('The memory id, as memory_search or memory_explore gave it.') }, async (args) => {
|
|
98
|
+
const it = await p.open(args.id).catch(() => null);
|
|
99
|
+
if (!it)
|
|
100
|
+
return text(`There is no memory "${args.id}" (it may have been merged or cleared).`, true);
|
|
101
|
+
const files = Array.isArray(it.files) && it.files.length ? `\nFiles: ${it.files.join(', ')}` : '';
|
|
102
|
+
const told = day(Number(it.ts ?? 0));
|
|
103
|
+
const happened = [typeof it.eventAt === 'string' && it.eventAt ? readableDate(it.eventAt) : '', lastedText(it)].filter(Boolean).join(', ');
|
|
104
|
+
const when = happened ? `happened ${happened}; told ${readableDate(told)}` : told;
|
|
105
|
+
// The raw requests it was learned from, whole (the truth it is a key to), and the texts a merge folded into it.
|
|
106
|
+
const sources = Array.isArray(it.sources) && it.sources.length
|
|
107
|
+
? `\n\n## Where it came from\n${it.sources.map((x) => `### ${day(x.when)} · ${x.title} (id ${x.id})\n${x.body}`).join('\n\n')}` : '';
|
|
108
|
+
const absorbed = Array.isArray(it.absorbed) && it.absorbed.length
|
|
109
|
+
? `\n\n## Texts merged into it\n${it.absorbed.map((x) => `### ${x.title}\n${x.summary}\n${x.body}`).join('\n\n')}` : '';
|
|
110
|
+
return text(`# ${String(it.title ?? '')}\n(${String(it.kind ?? '')} · ${when} · ${String(it.project ?? '')})\n\n${String(it.summary ?? '')}\n\n${String(it.body ?? '')}${files}${sources}${absorbed}`);
|
|
111
|
+
}),
|
|
112
|
+
...(p.explore ? [tool('memory_explore', DESCRIPTIONS.memory_explore, {
|
|
113
|
+
topic: z.string().optional().describe('What to explore, in plain words — a system, a project, a problem, a name.'),
|
|
114
|
+
from: z.array(z.string()).max(5).optional().describe('Memory ids to explore from, as memory_search or memory_explore gave them.'),
|
|
115
|
+
collections: z.array(z.string()).optional().describe('Only these collections (default: all).'),
|
|
116
|
+
limit: z.number().int().min(1).max(20).optional().describe('How many related items to return (default 10).'),
|
|
117
|
+
}, async (args) => {
|
|
118
|
+
if (!args.topic?.trim() && !args.from?.length)
|
|
119
|
+
return text('Give a topic, or memory ids to explore from.', true);
|
|
120
|
+
try {
|
|
121
|
+
const r = await p.explore({ topic: args.topic, from: args.from, collections: args.collections, limit: args.limit ?? 10 });
|
|
122
|
+
const parts = [];
|
|
123
|
+
if (r.found.length)
|
|
124
|
+
parts.push(`Found by search:\n${r.found.map(hitLine).join('\n')}`);
|
|
125
|
+
if (r.related.length)
|
|
126
|
+
parts.push(`Related — not found by search, reached through what they share:\n${r.related.map((h, n) => `${hitLine(h, n)} — ${h.via}\n ${h.summary.slice(0, 300)}`).join('\n')}`);
|
|
127
|
+
if (r.concepts.length)
|
|
128
|
+
parts.push(`Concepts around it: ${r.concepts.join(', ')}`);
|
|
129
|
+
if (!parts.length)
|
|
130
|
+
return text('Nothing in memory reaches that.');
|
|
131
|
+
return text(`${parts.join('\n\n')}\n\nOpen one with memory_open; explore further from an item (from: [its id]) or with one of the concepts as the topic. It can be stale: check it against the current state before relying on it.`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
return text(err.message, true);
|
|
135
|
+
}
|
|
136
|
+
})] : []),
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
const text = (t, isError = false) => ({ content: [{ type: 'text', text: t }], ...(isError ? { isError: true } : {}) });
|
|
140
|
+
export function buildAgentTools(p) {
|
|
141
|
+
return memoryTools(p);
|
|
142
|
+
}
|