crewswarm-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/crew.js +94 -0
- package/dist/crew.mjs +22964 -0
- package/dist/crew.mjs.map +7 -0
- package/dist/engine.mjs +3381 -0
- package/dist/engine.mjs.map +7 -0
- package/dist/memory.mjs +850 -0
- package/dist/memory.mjs.map +7 -0
- package/package.json +113 -0
package/dist/memory.mjs
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
// src/memory/agentkeeper.ts
|
|
2
|
+
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
function tokenize(text) {
|
|
6
|
+
return new Set(
|
|
7
|
+
text.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((t) => t.length > 2)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function similarity(a, b) {
|
|
11
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
12
|
+
let intersection = 0;
|
|
13
|
+
for (const token of a) {
|
|
14
|
+
if (b.has(token)) intersection++;
|
|
15
|
+
}
|
|
16
|
+
return intersection / Math.max(a.size, b.size);
|
|
17
|
+
}
|
|
18
|
+
var AgentKeeper = class {
|
|
19
|
+
constructor(baseDir, options = {}) {
|
|
20
|
+
this.writeCount = 0;
|
|
21
|
+
const storageBase = options.storageDir || process.env.CREW_MEMORY_DIR || baseDir;
|
|
22
|
+
this.storePath = join(storageBase, ".crew", "agentkeeper.jsonl");
|
|
23
|
+
this.maxEntries = options.maxEntries ?? 500;
|
|
24
|
+
this.maxBytes = options.maxBytes ?? 2e6;
|
|
25
|
+
this.maxAgeDays = options.maxAgeDays ?? 30;
|
|
26
|
+
this.autoCompactEvery = options.autoCompactEvery ?? 20;
|
|
27
|
+
this.semanticDedupe = options.semanticDedupe ?? true;
|
|
28
|
+
this.dedupeThreshold = options.dedupeThreshold ?? 0.9;
|
|
29
|
+
}
|
|
30
|
+
redactText(input) {
|
|
31
|
+
let out = String(input || "");
|
|
32
|
+
const replacements = [
|
|
33
|
+
[/\bsk-[A-Za-z0-9]{16,}\b/g, "[REDACTED_API_KEY]"],
|
|
34
|
+
[/\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{16,}\b/g, "[REDACTED_GITHUB_TOKEN]"],
|
|
35
|
+
[/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]"],
|
|
36
|
+
[/\beyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\b/g, "[REDACTED_JWT]"],
|
|
37
|
+
[/\b[A-Fa-f0-9]{40,}\b/g, "[REDACTED_HEX_TOKEN]"],
|
|
38
|
+
[/\b[A-Za-z0-9+/]{80,}={0,2}\b/g, "[REDACTED_BASE64_BLOB]"]
|
|
39
|
+
];
|
|
40
|
+
for (const [rx, replacement] of replacements) {
|
|
41
|
+
out = out.replace(rx, replacement);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
sanitizeText(input, maxChars = 6e3) {
|
|
46
|
+
const redacted = this.redactText(String(input || ""));
|
|
47
|
+
if (redacted.length <= maxChars) return redacted;
|
|
48
|
+
return `${redacted.slice(0, maxChars)}
|
|
49
|
+
... [truncated ${redacted.length - maxChars} chars]`;
|
|
50
|
+
}
|
|
51
|
+
sanitizeMetadata(value, depth = 0) {
|
|
52
|
+
if (depth > 3) return "[TRUNCATED_DEPTH]";
|
|
53
|
+
if (value === null || value === void 0) return value;
|
|
54
|
+
if (typeof value === "string") return this.sanitizeText(value, 1e3);
|
|
55
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
56
|
+
if (Array.isArray(value)) return value.slice(0, 50).map((item) => this.sanitizeMetadata(item, depth + 1));
|
|
57
|
+
if (typeof value === "object") {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const [k, v] of Object.entries(value).slice(0, 100)) {
|
|
60
|
+
out[k] = this.sanitizeMetadata(v, depth + 1);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
normalizeStructured(structured) {
|
|
67
|
+
if (!structured) return void 0;
|
|
68
|
+
const out = {};
|
|
69
|
+
if (structured.problem) out.problem = this.sanitizeText(structured.problem, 1200);
|
|
70
|
+
if (Array.isArray(structured.plan)) {
|
|
71
|
+
out.plan = structured.plan.slice(0, 50).map((step) => this.sanitizeText(step, 300));
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(structured.edits)) {
|
|
74
|
+
out.edits = structured.edits.slice(0, 100).map((edit) => ({
|
|
75
|
+
path: edit.path ? this.sanitizeText(edit.path, 300) : void 0,
|
|
76
|
+
summary: edit.summary ? this.sanitizeText(edit.summary, 300) : void 0
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
if (structured.validation) {
|
|
80
|
+
out.validation = {
|
|
81
|
+
lintPassed: Boolean(structured.validation.lintPassed),
|
|
82
|
+
testsPassed: Boolean(structured.validation.testsPassed),
|
|
83
|
+
notes: structured.validation.notes ? this.sanitizeText(structured.validation.notes, 600) : void 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (structured.outcome) out.outcome = this.sanitizeText(structured.outcome, 600);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
estimateStoreBytes(entries) {
|
|
90
|
+
return entries.reduce((sum, entry) => sum + Buffer.byteLength(JSON.stringify(entry) + "\n", "utf8"), 0);
|
|
91
|
+
}
|
|
92
|
+
pruneEntries(entries) {
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const maxAgeMs = Math.max(1, this.maxAgeDays) * 24 * 60 * 60 * 1e3;
|
|
95
|
+
let kept = entries.filter((entry) => {
|
|
96
|
+
const ts = Date.parse(entry.timestamp || "");
|
|
97
|
+
if (!Number.isFinite(ts)) return true;
|
|
98
|
+
return now - ts <= maxAgeMs;
|
|
99
|
+
});
|
|
100
|
+
if (kept.length > this.maxEntries) {
|
|
101
|
+
kept = kept.slice(-this.maxEntries);
|
|
102
|
+
}
|
|
103
|
+
while (kept.length > 1 && this.estimateStoreBytes(kept) > this.maxBytes) {
|
|
104
|
+
kept = kept.slice(1);
|
|
105
|
+
}
|
|
106
|
+
return kept;
|
|
107
|
+
}
|
|
108
|
+
dedupeSemantically(entries) {
|
|
109
|
+
if (!this.semanticDedupe || entries.length < 2) return entries;
|
|
110
|
+
const keptNewestFirst = [];
|
|
111
|
+
const keptTokens = [];
|
|
112
|
+
const descending = entries.slice().reverse();
|
|
113
|
+
for (const entry of descending) {
|
|
114
|
+
const entryText = `${entry.task || ""}
|
|
115
|
+
${String(entry.result || "").slice(0, 1200)}`;
|
|
116
|
+
const entryTokenSet = tokenize(entryText);
|
|
117
|
+
if (entryTokenSet.size < 6) {
|
|
118
|
+
keptNewestFirst.push(entry);
|
|
119
|
+
keptTokens.push(entryTokenSet);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
let duplicate = false;
|
|
123
|
+
for (let i = 0; i < keptNewestFirst.length; i += 1) {
|
|
124
|
+
const existing = keptNewestFirst[i];
|
|
125
|
+
if (existing.tier !== entry.tier) continue;
|
|
126
|
+
if ((existing.agent || "") !== (entry.agent || "")) continue;
|
|
127
|
+
if (keptTokens[i].size < 6) continue;
|
|
128
|
+
const sim = similarity(entryTokenSet, keptTokens[i]);
|
|
129
|
+
if (sim >= this.dedupeThreshold) {
|
|
130
|
+
duplicate = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!duplicate) {
|
|
135
|
+
keptNewestFirst.push(entry);
|
|
136
|
+
keptTokens.push(entryTokenSet);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return keptNewestFirst.reverse();
|
|
140
|
+
}
|
|
141
|
+
async maybeAutoCompact() {
|
|
142
|
+
this.writeCount += 1;
|
|
143
|
+
if (this.writeCount % this.autoCompactEvery !== 0) return;
|
|
144
|
+
try {
|
|
145
|
+
await this.compact();
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Append a new memory entry. */
|
|
150
|
+
async record(entry) {
|
|
151
|
+
await mkdir(dirname(this.storePath), { recursive: true });
|
|
152
|
+
const full = {
|
|
153
|
+
...entry,
|
|
154
|
+
id: randomUUID(),
|
|
155
|
+
task: this.sanitizeText(entry.task, 1200),
|
|
156
|
+
result: this.sanitizeText(entry.result, 6e3),
|
|
157
|
+
structured: this.normalizeStructured(entry.structured),
|
|
158
|
+
metadata: this.sanitizeMetadata(entry.metadata),
|
|
159
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
160
|
+
};
|
|
161
|
+
const line = JSON.stringify(full) + "\n";
|
|
162
|
+
await appendFile(this.storePath, line, "utf8");
|
|
163
|
+
await this.maybeAutoCompact();
|
|
164
|
+
return full;
|
|
165
|
+
}
|
|
166
|
+
/** Best-effort append: never throws, returns success state. */
|
|
167
|
+
async recordSafe(entry) {
|
|
168
|
+
try {
|
|
169
|
+
const saved = await this.record(entry);
|
|
170
|
+
return { ok: true, entry: saved };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return { ok: false, error: error.message };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Load all entries from the JSONL store. */
|
|
176
|
+
async loadAll() {
|
|
177
|
+
let raw;
|
|
178
|
+
try {
|
|
179
|
+
raw = await readFile(this.storePath, "utf8");
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const entries = [];
|
|
184
|
+
for (const line of raw.split("\n")) {
|
|
185
|
+
if (!line.trim()) continue;
|
|
186
|
+
try {
|
|
187
|
+
entries.push(JSON.parse(line));
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return entries;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Retrieve entries similar to the given task description.
|
|
195
|
+
* Returns up to `maxResults` matches sorted by similarity score descending.
|
|
196
|
+
*/
|
|
197
|
+
async recall(task, maxResults = 5, options = {}) {
|
|
198
|
+
const entries = await this.loadAll();
|
|
199
|
+
if (entries.length === 0) return [];
|
|
200
|
+
const queryTokens = tokenize(task);
|
|
201
|
+
const now = options.nowMs || Date.now();
|
|
202
|
+
const hints = new Set((options.pathHints || []).map((x) => String(x || "").trim()).filter(Boolean));
|
|
203
|
+
const scored = [];
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const entryTokens = tokenize(entry.task);
|
|
206
|
+
const sim = similarity(queryTokens, entryTokens);
|
|
207
|
+
let recencyBoost = 0;
|
|
208
|
+
const ts = Date.parse(entry.timestamp || "");
|
|
209
|
+
if (Number.isFinite(ts)) {
|
|
210
|
+
const ageDays = Math.max(0, (now - ts) / (24 * 60 * 60 * 1e3));
|
|
211
|
+
recencyBoost = Math.max(0, 1 - Math.min(ageDays, 30) / 30) * 0.15;
|
|
212
|
+
}
|
|
213
|
+
let successBoost = 0;
|
|
214
|
+
const success = Boolean(entry.metadata?.success) || Boolean(entry.structured?.outcome?.toLowerCase().includes("success"));
|
|
215
|
+
if (options.preferSuccessful !== false && success) {
|
|
216
|
+
successBoost = 0.1;
|
|
217
|
+
}
|
|
218
|
+
let pathBoost = 0;
|
|
219
|
+
if (hints.size > 0) {
|
|
220
|
+
const entryPaths = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const edit of entry.structured?.edits || []) {
|
|
222
|
+
if (edit.path) entryPaths.add(edit.path);
|
|
223
|
+
}
|
|
224
|
+
const metadataPaths = entry.metadata?.paths;
|
|
225
|
+
if (Array.isArray(metadataPaths)) {
|
|
226
|
+
for (const p of metadataPaths) {
|
|
227
|
+
entryPaths.add(String(p || ""));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let overlap = 0;
|
|
231
|
+
for (const h of hints) {
|
|
232
|
+
if (entryPaths.has(h)) overlap += 1;
|
|
233
|
+
}
|
|
234
|
+
if (overlap > 0) {
|
|
235
|
+
pathBoost = Math.min(0.2, overlap / Math.max(1, hints.size) * 0.2);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const score = sim + recencyBoost + successBoost + pathBoost;
|
|
239
|
+
if (score > 0.15) {
|
|
240
|
+
scored.push({ entry, score: Math.round(score * 100) / 100 });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
scored.sort((a, b) => b.score - a.score);
|
|
244
|
+
return scored.slice(0, maxResults);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Format recalled memories into a context block for prompt injection.
|
|
248
|
+
* Recent entries shown full, older entries compressed (progressive disclosure pattern).
|
|
249
|
+
*/
|
|
250
|
+
async recallAsContext(task, maxResults = 3, options = {}) {
|
|
251
|
+
const matches = await this.recall(task, maxResults, options);
|
|
252
|
+
if (matches.length === 0) return "";
|
|
253
|
+
const lines = ["## Prior Task Memory"];
|
|
254
|
+
const keepFullCount = Math.min(5, Math.ceil(matches.length * 0.5));
|
|
255
|
+
for (let i = 0; i < matches.length; i++) {
|
|
256
|
+
const m = matches[i];
|
|
257
|
+
const isFull = i < keepFullCount;
|
|
258
|
+
if (isFull) {
|
|
259
|
+
const resultPreview = m.entry.result.length > 400 ? m.entry.result.slice(0, 400) + "..." : m.entry.result;
|
|
260
|
+
lines.push(`### [${m.entry.tier}] ${m.entry.task} (score: ${m.score})`);
|
|
261
|
+
if (m.entry.agent) lines.push(`Agent: ${m.entry.agent}`);
|
|
262
|
+
lines.push(`Result: ${resultPreview}`);
|
|
263
|
+
lines.push("");
|
|
264
|
+
} else {
|
|
265
|
+
const hasError = /error|failed|exception/i.test(m.entry.result);
|
|
266
|
+
const statusIcon = hasError ? "\u274C" : "\u2713";
|
|
267
|
+
const preview = m.entry.result.slice(0, 120);
|
|
268
|
+
lines.push(`### ${statusIcon} [${m.entry.tier}] ${m.entry.task}`);
|
|
269
|
+
lines.push(`${preview}... [${hasError ? "failed" : "completed"}]`);
|
|
270
|
+
lines.push("");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Compact the store: keep only the most recent `maxEntries` entries.
|
|
277
|
+
*/
|
|
278
|
+
async compact() {
|
|
279
|
+
const entries = await this.loadAll();
|
|
280
|
+
const entriesBefore = entries.length;
|
|
281
|
+
let bytesBefore = 0;
|
|
282
|
+
try {
|
|
283
|
+
const st = await stat(this.storePath);
|
|
284
|
+
bytesBefore = st.size;
|
|
285
|
+
} catch {
|
|
286
|
+
bytesBefore = 0;
|
|
287
|
+
}
|
|
288
|
+
const deduped = this.dedupeSemantically(entries);
|
|
289
|
+
const kept = this.pruneEntries(deduped);
|
|
290
|
+
if (kept.length === entries.length) {
|
|
291
|
+
return { entriesBefore, entriesAfter: kept.length, bytesFreed: 0 };
|
|
292
|
+
}
|
|
293
|
+
const content = kept.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
294
|
+
await writeFile(this.storePath, content, "utf8");
|
|
295
|
+
let bytesAfter = 0;
|
|
296
|
+
try {
|
|
297
|
+
const st = await stat(this.storePath);
|
|
298
|
+
bytesAfter = st.size;
|
|
299
|
+
} catch {
|
|
300
|
+
bytesAfter = content.length;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
entriesBefore,
|
|
304
|
+
entriesAfter: kept.length,
|
|
305
|
+
bytesFreed: Math.max(0, bytesBefore - bytesAfter)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/** Get summary stats. */
|
|
309
|
+
async stats() {
|
|
310
|
+
const entries = await this.loadAll();
|
|
311
|
+
const byTier = {};
|
|
312
|
+
const byAgent = {};
|
|
313
|
+
for (const e of entries) {
|
|
314
|
+
byTier[e.tier] = (byTier[e.tier] || 0) + 1;
|
|
315
|
+
if (e.agent) byAgent[e.agent] = (byAgent[e.agent] || 0) + 1;
|
|
316
|
+
}
|
|
317
|
+
return { entries: entries.length, byTier, byAgent, bytes: this.estimateStoreBytes(entries) };
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/pipeline/agent-memory.ts
|
|
322
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
323
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
324
|
+
import { join as join2, resolve } from "node:path";
|
|
325
|
+
var AgentMemory = class {
|
|
326
|
+
constructor(agentId, options) {
|
|
327
|
+
const baseDir = options?.storageDir || process.env.CREW_MEMORY_DIR || process.cwd();
|
|
328
|
+
this.storageDir = resolve(baseDir, ".crew", "agent-memory");
|
|
329
|
+
this.ensureStorageDir();
|
|
330
|
+
this.state = this.loadOrCreate(agentId);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Store a fact in agent memory
|
|
334
|
+
*/
|
|
335
|
+
remember(content, options = {}) {
|
|
336
|
+
const fact = {
|
|
337
|
+
id: randomUUID2(),
|
|
338
|
+
content,
|
|
339
|
+
critical: options.critical || false,
|
|
340
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
341
|
+
tags: options.tags || [],
|
|
342
|
+
provider: options.provider
|
|
343
|
+
};
|
|
344
|
+
this.state.facts.push(fact);
|
|
345
|
+
this.state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
346
|
+
this.persist();
|
|
347
|
+
return fact.id;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Remove a fact by ID
|
|
351
|
+
*/
|
|
352
|
+
forget(factId) {
|
|
353
|
+
const before = this.state.facts.length;
|
|
354
|
+
this.state.facts = this.state.facts.filter((f) => f.id !== factId);
|
|
355
|
+
if (this.state.facts.length < before) {
|
|
356
|
+
this.state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
357
|
+
this.persist();
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Recall facts optimized for current context
|
|
364
|
+
* Priority: critical facts first, then most recent
|
|
365
|
+
*/
|
|
366
|
+
recall(options = {}) {
|
|
367
|
+
const budget = options.tokenBudget || 2e3;
|
|
368
|
+
const estimatedCharsPerToken = 4;
|
|
369
|
+
const charBudget = budget * estimatedCharsPerToken;
|
|
370
|
+
let facts = this.state.facts;
|
|
371
|
+
if (options.criticalOnly) {
|
|
372
|
+
facts = facts.filter((f) => f.critical);
|
|
373
|
+
}
|
|
374
|
+
if (options.tags && options.tags.length > 0) {
|
|
375
|
+
facts = facts.filter(
|
|
376
|
+
(f) => options.tags.some((tag) => f.tags.includes(tag))
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (options.provider) {
|
|
380
|
+
facts = facts.filter((f) => !f.provider || f.provider === options.provider);
|
|
381
|
+
}
|
|
382
|
+
facts.sort((a, b) => {
|
|
383
|
+
if (a.critical && !b.critical) return -1;
|
|
384
|
+
if (!a.critical && b.critical) return 1;
|
|
385
|
+
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
|
|
386
|
+
});
|
|
387
|
+
const selected = [];
|
|
388
|
+
let used = 0;
|
|
389
|
+
for (const fact of facts) {
|
|
390
|
+
const block = `[${fact.critical ? "CRITICAL" : "INFO"}] ${fact.content}
|
|
391
|
+
`;
|
|
392
|
+
if (used + block.length > charBudget) break;
|
|
393
|
+
selected.push(fact);
|
|
394
|
+
used += block.length;
|
|
395
|
+
}
|
|
396
|
+
if (selected.length === 0) return "";
|
|
397
|
+
const header = "=== AGENT MEMORY (Context from previous decisions) ===\n";
|
|
398
|
+
const body = selected.map(
|
|
399
|
+
(f) => `[${f.critical ? "CRITICAL" : "INFO"}] ${f.content}`
|
|
400
|
+
).join("\n");
|
|
401
|
+
const footer = "\n=== END AGENT MEMORY ===\n";
|
|
402
|
+
return header + body + footer;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Search facts by lexical similarity for brokered retrieval.
|
|
406
|
+
*/
|
|
407
|
+
search(query, options = {}) {
|
|
408
|
+
const maxResults = Math.max(1, Number(options.maxResults || 5));
|
|
409
|
+
const qTokens = new Set(
|
|
410
|
+
String(query || "").toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((t) => t.length > 2)
|
|
411
|
+
);
|
|
412
|
+
let facts = this.state.facts.slice();
|
|
413
|
+
if (options.tags && options.tags.length > 0) {
|
|
414
|
+
facts = facts.filter((f) => options.tags.some((tag) => f.tags.includes(tag)));
|
|
415
|
+
}
|
|
416
|
+
if (options.provider) {
|
|
417
|
+
facts = facts.filter((f) => !f.provider || f.provider === options.provider);
|
|
418
|
+
}
|
|
419
|
+
const score = (fact) => {
|
|
420
|
+
const toks = new Set(
|
|
421
|
+
String(fact.content || "").toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((t) => t.length > 2)
|
|
422
|
+
);
|
|
423
|
+
if (qTokens.size === 0 || toks.size === 0) return 0;
|
|
424
|
+
let inter = 0;
|
|
425
|
+
for (const t of qTokens) {
|
|
426
|
+
if (toks.has(t)) inter += 1;
|
|
427
|
+
}
|
|
428
|
+
const sim = inter / Math.max(qTokens.size, toks.size);
|
|
429
|
+
return sim + (fact.critical ? 0.1 : 0);
|
|
430
|
+
};
|
|
431
|
+
const ranked = facts.map((f) => ({ fact: f, score: score(f) })).filter((x) => x.score > 0.12).sort((a, b) => b.score - a.score).slice(0, maxResults).map((x) => x.fact);
|
|
432
|
+
return ranked;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Get memory statistics
|
|
436
|
+
*/
|
|
437
|
+
stats() {
|
|
438
|
+
const facts = this.state.facts || [];
|
|
439
|
+
const timestamps = facts.map((f) => f.timestamp).sort();
|
|
440
|
+
const providers = Array.from(new Set(
|
|
441
|
+
facts.map((f) => f.provider).filter(Boolean)
|
|
442
|
+
));
|
|
443
|
+
return {
|
|
444
|
+
totalFacts: facts.length,
|
|
445
|
+
criticalFacts: facts.filter((f) => f.critical).length,
|
|
446
|
+
providers,
|
|
447
|
+
oldestFact: timestamps[0] || null,
|
|
448
|
+
newestFact: timestamps[timestamps.length - 1] || null
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Clear all facts (useful for testing)
|
|
453
|
+
*/
|
|
454
|
+
clear() {
|
|
455
|
+
this.state.facts = [];
|
|
456
|
+
this.state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
457
|
+
this.persist();
|
|
458
|
+
}
|
|
459
|
+
loadOrCreate(agentId) {
|
|
460
|
+
const path = this.getStatePath(agentId);
|
|
461
|
+
if (existsSync(path)) {
|
|
462
|
+
try {
|
|
463
|
+
const raw = readFileSync(path, "utf8");
|
|
464
|
+
return JSON.parse(raw);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.warn(`[AgentMemory] Failed to load state for ${agentId}, creating new`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
470
|
+
return {
|
|
471
|
+
agentId,
|
|
472
|
+
facts: [],
|
|
473
|
+
createdAt: now,
|
|
474
|
+
updatedAt: now
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
persist() {
|
|
478
|
+
const path = this.getStatePath(this.state.agentId);
|
|
479
|
+
writeFileSync(path, JSON.stringify(this.state, null, 2), "utf8");
|
|
480
|
+
}
|
|
481
|
+
getStatePath(agentId) {
|
|
482
|
+
return join2(this.storageDir, `${agentId}.json`);
|
|
483
|
+
}
|
|
484
|
+
ensureStorageDir() {
|
|
485
|
+
if (!existsSync(this.storageDir)) {
|
|
486
|
+
mkdirSync(this.storageDir, { recursive: true });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/memory/broker.ts
|
|
492
|
+
import { resolve as resolve3, join as join4 } from "node:path";
|
|
493
|
+
|
|
494
|
+
// src/collections/index.ts
|
|
495
|
+
import { readdir, readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
496
|
+
import { extname, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
497
|
+
var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".rst", ".adoc"]);
|
|
498
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
499
|
+
".ts",
|
|
500
|
+
".tsx",
|
|
501
|
+
".js",
|
|
502
|
+
".jsx",
|
|
503
|
+
".mjs",
|
|
504
|
+
".cjs",
|
|
505
|
+
".json",
|
|
506
|
+
".py",
|
|
507
|
+
".go",
|
|
508
|
+
".rs",
|
|
509
|
+
".java",
|
|
510
|
+
".kt",
|
|
511
|
+
".swift",
|
|
512
|
+
".sh",
|
|
513
|
+
".bash",
|
|
514
|
+
".zsh",
|
|
515
|
+
".yaml",
|
|
516
|
+
".yml",
|
|
517
|
+
".toml"
|
|
518
|
+
]);
|
|
519
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
520
|
+
"node_modules",
|
|
521
|
+
".git",
|
|
522
|
+
"dist",
|
|
523
|
+
"build",
|
|
524
|
+
".crew",
|
|
525
|
+
".next",
|
|
526
|
+
".turbo",
|
|
527
|
+
"coverage",
|
|
528
|
+
"__pycache__"
|
|
529
|
+
]);
|
|
530
|
+
function tokenize2(text) {
|
|
531
|
+
return text.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((t) => t.length > 1);
|
|
532
|
+
}
|
|
533
|
+
function hashToken(token, dim) {
|
|
534
|
+
let h = 2166136261;
|
|
535
|
+
for (let i = 0; i < token.length; i++) {
|
|
536
|
+
h ^= token.charCodeAt(i);
|
|
537
|
+
h = Math.imul(h, 16777619);
|
|
538
|
+
}
|
|
539
|
+
return Math.abs(h) % dim;
|
|
540
|
+
}
|
|
541
|
+
function toHashedVector(text, dim = 256) {
|
|
542
|
+
const vec = new Float64Array(dim);
|
|
543
|
+
const toks = tokenize2(text);
|
|
544
|
+
for (const t of toks) {
|
|
545
|
+
vec[hashToken(t, dim)] += 1;
|
|
546
|
+
}
|
|
547
|
+
let norm = 0;
|
|
548
|
+
for (let i = 0; i < dim; i++) norm += vec[i] * vec[i];
|
|
549
|
+
norm = Math.sqrt(norm);
|
|
550
|
+
if (norm > 0) {
|
|
551
|
+
for (let i = 0; i < dim; i++) vec[i] /= norm;
|
|
552
|
+
}
|
|
553
|
+
return vec;
|
|
554
|
+
}
|
|
555
|
+
function cosineSimilarity(a, b) {
|
|
556
|
+
const dim = Math.min(a.length, b.length);
|
|
557
|
+
let dot = 0;
|
|
558
|
+
for (let i = 0; i < dim; i++) {
|
|
559
|
+
dot += a[i] * b[i];
|
|
560
|
+
}
|
|
561
|
+
return dot;
|
|
562
|
+
}
|
|
563
|
+
function chunkFile(content, source) {
|
|
564
|
+
const lines = content.split("\n");
|
|
565
|
+
const chunks = [];
|
|
566
|
+
let currentLines = [];
|
|
567
|
+
let currentStart = 1;
|
|
568
|
+
const flush = () => {
|
|
569
|
+
const text = currentLines.join("\n").trim();
|
|
570
|
+
if (text.length > 0) {
|
|
571
|
+
chunks.push({ source, startLine: currentStart, text, score: 0 });
|
|
572
|
+
}
|
|
573
|
+
currentLines = [];
|
|
574
|
+
};
|
|
575
|
+
for (let i = 0; i < lines.length; i++) {
|
|
576
|
+
const line = lines[i];
|
|
577
|
+
if (/^#{1,4}\s/.test(line) && currentLines.length > 0) {
|
|
578
|
+
flush();
|
|
579
|
+
currentStart = i + 1;
|
|
580
|
+
}
|
|
581
|
+
currentLines.push(line);
|
|
582
|
+
if (currentLines.length >= 40 && !/^#{1,4}\s/.test(line)) {
|
|
583
|
+
flush();
|
|
584
|
+
currentStart = i + 2;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
flush();
|
|
588
|
+
return chunks;
|
|
589
|
+
}
|
|
590
|
+
async function walkDocs(rootDir, includeCode = false) {
|
|
591
|
+
const files = [];
|
|
592
|
+
async function walk(dir) {
|
|
593
|
+
let entries;
|
|
594
|
+
try {
|
|
595
|
+
entries = await readdir(dir);
|
|
596
|
+
} catch {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
for (const entry of entries) {
|
|
600
|
+
if (IGNORED_DIRS.has(entry)) continue;
|
|
601
|
+
const fullPath = join3(dir, entry);
|
|
602
|
+
let st;
|
|
603
|
+
try {
|
|
604
|
+
st = await stat2(fullPath);
|
|
605
|
+
} catch {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (st.isDirectory()) {
|
|
609
|
+
await walk(fullPath);
|
|
610
|
+
} else {
|
|
611
|
+
const ext = extname(entry).toLowerCase();
|
|
612
|
+
if (DOC_EXTENSIONS.has(ext) || includeCode && CODE_EXTENSIONS.has(ext)) {
|
|
613
|
+
files.push(fullPath);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
await walk(rootDir);
|
|
619
|
+
return files;
|
|
620
|
+
}
|
|
621
|
+
async function buildCollectionIndex(paths, options = {}) {
|
|
622
|
+
const allChunks = [];
|
|
623
|
+
const roots = paths.map((p) => resolve2(p));
|
|
624
|
+
let fileCount = 0;
|
|
625
|
+
for (const rootPath of roots) {
|
|
626
|
+
let st;
|
|
627
|
+
try {
|
|
628
|
+
st = await stat2(rootPath);
|
|
629
|
+
} catch {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const files = st.isDirectory() ? await walkDocs(rootPath, Boolean(options.includeCode)) : [rootPath];
|
|
633
|
+
for (const file of files) {
|
|
634
|
+
let content;
|
|
635
|
+
try {
|
|
636
|
+
content = await readFile2(file, "utf8");
|
|
637
|
+
} catch {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
fileCount++;
|
|
641
|
+
const rel = relative(resolve2(rootPath, st.isDirectory() ? "." : ".."), file);
|
|
642
|
+
const chunks = chunkFile(content, rel);
|
|
643
|
+
allChunks.push(...chunks);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const terms = /* @__PURE__ */ new Map();
|
|
647
|
+
for (let i = 0; i < allChunks.length; i++) {
|
|
648
|
+
const tokens = tokenize2(allChunks[i].text);
|
|
649
|
+
for (const token of tokens) {
|
|
650
|
+
if (!terms.has(token)) terms.set(token, /* @__PURE__ */ new Set());
|
|
651
|
+
terms.get(token).add(i);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
root: roots[0] || ".",
|
|
656
|
+
fileCount,
|
|
657
|
+
chunkCount: allChunks.length,
|
|
658
|
+
terms,
|
|
659
|
+
chunks: allChunks
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function searchCollection(index, query, maxResults = 10) {
|
|
663
|
+
const queryTokens = tokenize2(query);
|
|
664
|
+
if (queryTokens.length === 0) {
|
|
665
|
+
return { query, hits: [], totalChunks: index.chunkCount };
|
|
666
|
+
}
|
|
667
|
+
const scores = new Float64Array(index.chunkCount);
|
|
668
|
+
for (const token of queryTokens) {
|
|
669
|
+
const matchingChunks = index.terms.get(token);
|
|
670
|
+
if (!matchingChunks) continue;
|
|
671
|
+
const idf = Math.log(1 + index.chunkCount / matchingChunks.size);
|
|
672
|
+
for (const idx of matchingChunks) {
|
|
673
|
+
scores[idx] += idf;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const candidates = [];
|
|
677
|
+
for (let i = 0; i < scores.length; i++) {
|
|
678
|
+
if (scores[i] > 0) {
|
|
679
|
+
candidates.push({ idx: i, score: scores[i] });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
683
|
+
const queryVector = toHashedVector(query);
|
|
684
|
+
const maxTfidf = candidates.length > 0 ? candidates[0].score : 1;
|
|
685
|
+
const tfidfWeight = 0.7;
|
|
686
|
+
const vectorWeight = 0.3;
|
|
687
|
+
const hybrid = candidates.map((c) => {
|
|
688
|
+
const chunk = index.chunks[c.idx];
|
|
689
|
+
const chunkVector = toHashedVector(chunk.text);
|
|
690
|
+
const cosine = Math.max(0, cosineSimilarity(queryVector, chunkVector));
|
|
691
|
+
const tfidfNorm = maxTfidf > 0 ? c.score / maxTfidf : 0;
|
|
692
|
+
const hybridScore = tfidfNorm * tfidfWeight + cosine * vectorWeight;
|
|
693
|
+
return { idx: c.idx, score: hybridScore };
|
|
694
|
+
});
|
|
695
|
+
hybrid.sort((a, b) => b.score - a.score);
|
|
696
|
+
const hits = hybrid.slice(0, maxResults).map((c) => ({
|
|
697
|
+
...index.chunks[c.idx],
|
|
698
|
+
score: Math.round(c.score * 1e3) / 1e3
|
|
699
|
+
}));
|
|
700
|
+
return { query, hits, totalChunks: index.chunkCount };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/memory/broker.ts
|
|
704
|
+
function tokenize3(text) {
|
|
705
|
+
return new Set(
|
|
706
|
+
String(text || "").toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((t) => t.length > 2)
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
function similarity2(a, b) {
|
|
710
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
711
|
+
let intersection = 0;
|
|
712
|
+
for (const token of a) {
|
|
713
|
+
if (b.has(token)) intersection += 1;
|
|
714
|
+
}
|
|
715
|
+
return intersection / Math.max(a.size, b.size);
|
|
716
|
+
}
|
|
717
|
+
function mapKeeperHit(m) {
|
|
718
|
+
return {
|
|
719
|
+
source: "agentkeeper",
|
|
720
|
+
score: Number(m.score || 0),
|
|
721
|
+
title: `[${m.entry.tier}] ${m.entry.task}`,
|
|
722
|
+
text: m.entry.result,
|
|
723
|
+
metadata: {
|
|
724
|
+
agent: m.entry.agent,
|
|
725
|
+
runId: m.entry.runId,
|
|
726
|
+
timestamp: m.entry.timestamp
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function mapFactHit(f, score) {
|
|
731
|
+
return {
|
|
732
|
+
source: "agent-memory",
|
|
733
|
+
score: Number(score.toFixed(3)),
|
|
734
|
+
title: `[${f.critical ? "CRITICAL" : "INFO"}] ${f.tags.join(", ") || "memory-fact"}`,
|
|
735
|
+
text: f.content,
|
|
736
|
+
metadata: {
|
|
737
|
+
critical: f.critical,
|
|
738
|
+
tags: f.tags,
|
|
739
|
+
timestamp: f.timestamp,
|
|
740
|
+
provider: f.provider
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function mapCollectionHit(c) {
|
|
745
|
+
return {
|
|
746
|
+
source: "collections",
|
|
747
|
+
score: Number(c.score || 0),
|
|
748
|
+
title: `${c.source}:${c.startLine}`,
|
|
749
|
+
text: c.text,
|
|
750
|
+
metadata: {
|
|
751
|
+
path: c.source,
|
|
752
|
+
startLine: c.startLine
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function normalizeCollectionPathForDedupe(input) {
|
|
757
|
+
const value = String(input || "").replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
758
|
+
if (value.startsWith("docs/")) return value.slice("docs/".length);
|
|
759
|
+
return value;
|
|
760
|
+
}
|
|
761
|
+
var MemoryBroker = class {
|
|
762
|
+
constructor(projectDir, options = {}) {
|
|
763
|
+
this.docsIndexCache = /* @__PURE__ */ new Map();
|
|
764
|
+
this.projectDir = resolve3(projectDir);
|
|
765
|
+
this.keeper = new AgentKeeper(this.projectDir, {
|
|
766
|
+
storageDir: options.storageDir || process.env.CREW_MEMORY_DIR
|
|
767
|
+
});
|
|
768
|
+
this.factMemory = new AgentMemory(options.crewId || "crew-lead", {
|
|
769
|
+
storageDir: options.storageDir || process.env.CREW_MEMORY_DIR || this.projectDir
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
scoreFacts(query, facts, max) {
|
|
773
|
+
const queryTokens = tokenize3(query);
|
|
774
|
+
const scored = facts.map((f) => {
|
|
775
|
+
const sim = similarity2(queryTokens, tokenize3(f.content));
|
|
776
|
+
const criticalBoost = f.critical ? 0.3 : 0;
|
|
777
|
+
const tagBoost = f.tags.some((t) => query.toLowerCase().includes(t.toLowerCase())) ? 0.15 : 0;
|
|
778
|
+
return { fact: f, score: sim + criticalBoost + tagBoost };
|
|
779
|
+
}).filter((x) => x.score > 0.08);
|
|
780
|
+
scored.sort((a, b) => {
|
|
781
|
+
if (a.fact.critical && !b.fact.critical) return -1;
|
|
782
|
+
if (!a.fact.critical && b.fact.critical) return 1;
|
|
783
|
+
return b.score - a.score;
|
|
784
|
+
});
|
|
785
|
+
return scored.slice(0, max).map((x) => mapFactHit(x.fact, x.score));
|
|
786
|
+
}
|
|
787
|
+
async getDocsIndex(paths, includeCode) {
|
|
788
|
+
const key = `${paths.map((p) => resolve3(p)).join("|")}::${includeCode ? "code" : "docs"}`;
|
|
789
|
+
if (this.docsIndexCache.has(key)) return this.docsIndexCache.get(key);
|
|
790
|
+
const idx = await buildCollectionIndex(paths, { includeCode });
|
|
791
|
+
this.docsIndexCache.set(key, idx);
|
|
792
|
+
return idx;
|
|
793
|
+
}
|
|
794
|
+
async recall(query, options = {}) {
|
|
795
|
+
const maxResults = Math.max(1, Number(options.maxResults || 5));
|
|
796
|
+
const includeDocs = options.includeDocs !== false;
|
|
797
|
+
const includeCode = Boolean(options.includeCode);
|
|
798
|
+
const docsPaths = options.docsPaths && options.docsPaths.length > 0 ? options.docsPaths : [join4(this.projectDir, "docs"), this.projectDir];
|
|
799
|
+
const [keeperHits, factHits, collectionHits] = await Promise.all([
|
|
800
|
+
this.keeper.recall(query, Math.max(maxResults, 8), {
|
|
801
|
+
preferSuccessful: options.preferSuccessful !== false,
|
|
802
|
+
pathHints: options.pathHints || []
|
|
803
|
+
}),
|
|
804
|
+
this.factMemory.search(query, { maxResults: Math.max(maxResults, 8) }),
|
|
805
|
+
includeDocs ? (async () => {
|
|
806
|
+
const index = await this.getDocsIndex(docsPaths, includeCode);
|
|
807
|
+
return searchCollection(index, query, Math.max(maxResults, 8)).hits;
|
|
808
|
+
})() : Promise.resolve([])
|
|
809
|
+
]);
|
|
810
|
+
const merged = [
|
|
811
|
+
...keeperHits.map(mapKeeperHit),
|
|
812
|
+
...this.scoreFacts(query, factHits, Math.max(maxResults, 8)),
|
|
813
|
+
...collectionHits.map(mapCollectionHit)
|
|
814
|
+
];
|
|
815
|
+
merged.sort((a, b) => b.score - a.score);
|
|
816
|
+
const seen = /* @__PURE__ */ new Set();
|
|
817
|
+
const deduped = [];
|
|
818
|
+
for (const hit of merged) {
|
|
819
|
+
let signature = `${hit.source}|${hit.title}|${hit.text.slice(0, 180)}`;
|
|
820
|
+
if (hit.source === "collections") {
|
|
821
|
+
const path = normalizeCollectionPathForDedupe(String(hit.metadata?.path || hit.title.split(":")[0] || ""));
|
|
822
|
+
const startLine = Number(hit.metadata?.startLine || 0);
|
|
823
|
+
signature = `${hit.source}|${path}|${startLine}|${hit.text.slice(0, 220)}`;
|
|
824
|
+
}
|
|
825
|
+
if (seen.has(signature)) continue;
|
|
826
|
+
seen.add(signature);
|
|
827
|
+
deduped.push(hit);
|
|
828
|
+
if (deduped.length >= maxResults) break;
|
|
829
|
+
}
|
|
830
|
+
return deduped;
|
|
831
|
+
}
|
|
832
|
+
async recallAsContext(query, options = {}) {
|
|
833
|
+
const hits = await this.recall(query, options);
|
|
834
|
+
if (hits.length === 0) return "";
|
|
835
|
+
const lines = ["## Shared Memory + RAG Context"];
|
|
836
|
+
for (const h of hits) {
|
|
837
|
+
const preview = h.text.length > 260 ? `${h.text.slice(0, 260)}...` : h.text;
|
|
838
|
+
lines.push(`### [${h.source}] ${h.title} (score: ${h.score.toFixed(3)})`);
|
|
839
|
+
lines.push(preview);
|
|
840
|
+
lines.push("");
|
|
841
|
+
}
|
|
842
|
+
return lines.join("\n");
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
export {
|
|
846
|
+
AgentKeeper,
|
|
847
|
+
AgentMemory,
|
|
848
|
+
MemoryBroker
|
|
849
|
+
};
|
|
850
|
+
//# sourceMappingURL=memory.mjs.map
|