@tpsdev-ai/flair 0.3.17 → 0.3.19
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 +30 -27
- package/dist/cli.js +38 -0
- package/package.json +1 -2
- package/resources/A2AAdapter.ts +0 -510
- package/resources/Agent.ts +0 -10
- package/resources/AgentCard.ts +0 -65
- package/resources/AgentSeed.ts +0 -119
- package/resources/IngestEvents.ts +0 -189
- package/resources/Integration.ts +0 -14
- package/resources/IssueTokens.ts +0 -29
- package/resources/Memory.ts +0 -151
- package/resources/MemoryBootstrap.ts +0 -323
- package/resources/MemoryConsolidate.ts +0 -121
- package/resources/MemoryFeed.ts +0 -48
- package/resources/MemoryMaintenance.ts +0 -95
- package/resources/MemoryReflect.ts +0 -122
- package/resources/OrgEvent.ts +0 -63
- package/resources/OrgEventCatchup.ts +0 -89
- package/resources/OrgEventMaintenance.ts +0 -37
- package/resources/SemanticSearch.ts +0 -197
- package/resources/SkillScan.ts +0 -146
- package/resources/Soul.ts +0 -10
- package/resources/SoulFeed.ts +0 -15
- package/resources/WorkspaceLatest.ts +0 -66
- package/resources/WorkspaceState.ts +0 -102
- package/resources/auth-middleware.ts +0 -501
- package/resources/embeddings-provider.ts +0 -61
- package/resources/embeddings.ts +0 -28
- package/resources/health.ts +0 -7
- package/resources/memory-feed-lib.ts +0 -22
- package/resources/table-helpers.ts +0 -46
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { Resource, databases } from "@harperfast/harper";
|
|
2
|
-
import { getEmbedding } from "./embeddings-provider.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* POST /MemoryBootstrap
|
|
6
|
-
*
|
|
7
|
-
* One-call context builder for agent cold starts.
|
|
8
|
-
* Returns prioritized, token-budgeted context with:
|
|
9
|
-
* 1. Soul records (identity, role, preferences)
|
|
10
|
-
* 2. Permanent memories (safety rules, core principles)
|
|
11
|
-
* 3. Recent memories (last 24-48h standard/persistent)
|
|
12
|
-
* 4. Task-relevant memories (semantic search if currentTask provided)
|
|
13
|
-
*
|
|
14
|
-
* Request:
|
|
15
|
-
* { agentId, currentTask?, maxTokens?, includeSoul?, since? }
|
|
16
|
-
*
|
|
17
|
-
* Response:
|
|
18
|
-
* { context, sections, tokenEstimate, memoriesIncluded, memoriesAvailable }
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// Rough token estimate: ~4 chars per token for English text
|
|
22
|
-
function estimateTokens(text: string): number {
|
|
23
|
-
return Math.ceil(text.length / 4);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function formatMemory(m: any, supersedes?: boolean): string {
|
|
27
|
-
const tag = m.durability === "permanent" ? "🔒" : m.durability === "persistent" ? "📌" : "📝";
|
|
28
|
-
const date = m.createdAt ? ` (${m.createdAt.slice(0, 10)})` : "";
|
|
29
|
-
const chain = m.supersedes ? " [supersedes earlier decision]" : "";
|
|
30
|
-
return `${tag} ${m.content}${date}${chain}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class BootstrapMemories extends Resource {
|
|
34
|
-
async post(data: any, _context?: any) {
|
|
35
|
-
const {
|
|
36
|
-
agentId,
|
|
37
|
-
currentTask,
|
|
38
|
-
maxTokens = 4000,
|
|
39
|
-
includeSoul = true,
|
|
40
|
-
since,
|
|
41
|
-
} = data || {};
|
|
42
|
-
|
|
43
|
-
if (!agentId) {
|
|
44
|
-
return new Response(JSON.stringify({ error: "agentId required" }), {
|
|
45
|
-
status: 400,
|
|
46
|
-
headers: { "Content-Type": "application/json" },
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Defense-in-depth: agentId must match authenticated agent
|
|
51
|
-
const authenticatedAgent: string | undefined = (this as any).request?.headers?.get?.("x-tps-agent");
|
|
52
|
-
const callerIsAdmin: boolean = (this as any).request?.tpsAgentIsAdmin === true;
|
|
53
|
-
if (authenticatedAgent && !callerIsAdmin && agentId !== authenticatedAgent) {
|
|
54
|
-
return new Response(JSON.stringify({
|
|
55
|
-
error: "forbidden: agentId must match authenticated agent",
|
|
56
|
-
}), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const sections: Record<string, string[]> = {
|
|
60
|
-
soul: [],
|
|
61
|
-
skills: [],
|
|
62
|
-
permanent: [],
|
|
63
|
-
recent: [],
|
|
64
|
-
relevant: [],
|
|
65
|
-
events: [],
|
|
66
|
-
};
|
|
67
|
-
let tokenBudget = maxTokens;
|
|
68
|
-
let memoriesIncluded = 0;
|
|
69
|
-
let memoriesAvailable = 0;
|
|
70
|
-
|
|
71
|
-
// --- 1. Soul records (budgeted — prioritized by key importance) ---
|
|
72
|
-
// Soul is who you are, but we still need to respect token budgets.
|
|
73
|
-
// Workspace files (SOUL.md, AGENTS.md) can be massive — they're already
|
|
74
|
-
// injected by the runtime via workspace context, so we prioritize
|
|
75
|
-
// concise soul entries over full file dumps.
|
|
76
|
-
const SOUL_KEY_PRIORITY: Record<string, number> = {
|
|
77
|
-
role: 0, identity: 1, thinking: 2, communication_style: 3,
|
|
78
|
-
team: 4, ownership: 5, infrastructure: 6, "user-context": 7,
|
|
79
|
-
// Full workspace files — lowest priority (runtime already injects these)
|
|
80
|
-
soul: 90, "workspace-rules": 91,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const skillAssignments: any[] = [];
|
|
84
|
-
const soulMaxTokens = Math.floor(maxTokens * 0.4); // 40% of budget for soul
|
|
85
|
-
if (includeSoul) {
|
|
86
|
-
let soulTokens = 0;
|
|
87
|
-
const soulEntries: { key: string; line: string; tokens: number; priority: number }[] = [];
|
|
88
|
-
|
|
89
|
-
for await (const record of (databases as any).flair.Soul.search()) {
|
|
90
|
-
if (record.agentId !== agentId) continue;
|
|
91
|
-
if (record.key === "skill-assignment") {
|
|
92
|
-
skillAssignments.push(record);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const line = `**${record.key}:** ${record.value}`;
|
|
96
|
-
const tokens = estimateTokens(line);
|
|
97
|
-
const priority = SOUL_KEY_PRIORITY[record.key] ?? 50;
|
|
98
|
-
soulEntries.push({ key: record.key, line, tokens, priority });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Sort by priority (lower = more important)
|
|
102
|
-
soulEntries.sort((a, b) => a.priority - b.priority);
|
|
103
|
-
|
|
104
|
-
for (const entry of soulEntries) {
|
|
105
|
-
if (soulTokens + entry.tokens > soulMaxTokens) {
|
|
106
|
-
// Skip large entries that exceed budget — truncate or skip
|
|
107
|
-
if (entry.priority >= 90) continue; // skip full workspace files
|
|
108
|
-
// Truncate if it's important but too long
|
|
109
|
-
const maxChars = (soulMaxTokens - soulTokens) * 4;
|
|
110
|
-
if (maxChars > 100) {
|
|
111
|
-
const truncated = `**${entry.key}:** ${entry.line.slice(entry.key.length + 6, entry.key.length + 6 + maxChars)}…(truncated)`;
|
|
112
|
-
sections.soul.push(truncated);
|
|
113
|
-
soulTokens += estimateTokens(truncated);
|
|
114
|
-
}
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
sections.soul.push(entry.line);
|
|
118
|
-
soulTokens += entry.tokens;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// --- 1b. Skill assignments (ordered by priority, conflict detection) ---
|
|
123
|
-
if (skillAssignments.length > 0) {
|
|
124
|
-
const priorityOrder: Record<string, number> = { critical: 0, high: 1, standard: 2, low: 3 };
|
|
125
|
-
skillAssignments.sort((a, b) => {
|
|
126
|
-
const pa = priorityOrder[a.priority ?? "standard"] ?? 2;
|
|
127
|
-
const pb = priorityOrder[b.priority ?? "standard"] ?? 2;
|
|
128
|
-
return pa - pb;
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Detect conflicts at same priority level
|
|
132
|
-
const byPriority = new Map<string, any[]>();
|
|
133
|
-
for (const skill of skillAssignments) {
|
|
134
|
-
const p = skill.priority ?? "standard";
|
|
135
|
-
if (!byPriority.has(p)) byPriority.set(p, []);
|
|
136
|
-
byPriority.get(p)!.push(skill);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (const skill of skillAssignments) {
|
|
140
|
-
const p = skill.priority ?? "standard";
|
|
141
|
-
let meta: any = {};
|
|
142
|
-
try { meta = typeof skill.metadata === "string" ? JSON.parse(skill.metadata) : (skill.metadata ?? {}); } catch {}
|
|
143
|
-
const source = meta.source ? `, source: ${meta.source}` : "";
|
|
144
|
-
let line = `- ${skill.value} (${p} priority${source})`;
|
|
145
|
-
// Flag conflicts at same priority level
|
|
146
|
-
const peers = byPriority.get(p) ?? [];
|
|
147
|
-
if (peers.length > 1) {
|
|
148
|
-
line += " [SKILL_CONFLICT]";
|
|
149
|
-
}
|
|
150
|
-
sections.skills.push(line);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// --- 2. Permanent memories (always included, highest priority) ---
|
|
155
|
-
const allMemories: any[] = [];
|
|
156
|
-
for await (const record of (databases as any).flair.Memory.search()) {
|
|
157
|
-
if (record.agentId !== agentId) continue;
|
|
158
|
-
if (record.expiresAt && Date.parse(record.expiresAt) < Date.now()) continue;
|
|
159
|
-
allMemories.push(record);
|
|
160
|
-
}
|
|
161
|
-
memoriesAvailable = allMemories.length;
|
|
162
|
-
|
|
163
|
-
// Build superseded set: exclude memories that have been replaced by newer ones
|
|
164
|
-
const supersededIds = new Set<string>();
|
|
165
|
-
for (const m of allMemories) {
|
|
166
|
-
if (m.supersedes) supersededIds.add(m.supersedes);
|
|
167
|
-
}
|
|
168
|
-
const activeMemories = allMemories.filter((m) => !supersededIds.has(m.id));
|
|
169
|
-
|
|
170
|
-
const permanent = activeMemories.filter((m) => m.durability === "permanent");
|
|
171
|
-
for (const m of permanent) {
|
|
172
|
-
const line = formatMemory(m);
|
|
173
|
-
const cost = estimateTokens(line);
|
|
174
|
-
if (cost <= tokenBudget) {
|
|
175
|
-
sections.permanent.push(line);
|
|
176
|
-
tokenBudget -= cost;
|
|
177
|
-
memoriesIncluded++;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// --- 3. Recent memories (adaptive window) ---
|
|
182
|
-
// Start with 48h. If nothing found, widen to 7d, then 30d.
|
|
183
|
-
// This prevents empty recent sections for agents that were idle.
|
|
184
|
-
const nonPermanent = activeMemories
|
|
185
|
-
.filter((m) => m.durability !== "permanent" && m.createdAt)
|
|
186
|
-
.sort((a: any, b: any) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
187
|
-
|
|
188
|
-
let effectiveSince: Date;
|
|
189
|
-
if (since) {
|
|
190
|
-
effectiveSince = new Date(since);
|
|
191
|
-
} else {
|
|
192
|
-
const windows = [48 * 3600_000, 7 * 24 * 3600_000, 30 * 24 * 3600_000];
|
|
193
|
-
effectiveSince = new Date(Date.now() - windows[0]);
|
|
194
|
-
for (const w of windows) {
|
|
195
|
-
effectiveSince = new Date(Date.now() - w);
|
|
196
|
-
const count = nonPermanent.filter((m) => new Date(m.createdAt!) >= effectiveSince).length;
|
|
197
|
-
if (count >= 3) break; // found enough recent memories
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const recent = nonPermanent.filter((m) => new Date(m.createdAt!) >= effectiveSince);
|
|
202
|
-
|
|
203
|
-
// Budget: up to 40% of remaining for recent
|
|
204
|
-
const recentBudget = Math.floor(tokenBudget * 0.4);
|
|
205
|
-
let recentSpent = 0;
|
|
206
|
-
for (const m of recent) {
|
|
207
|
-
const line = formatMemory(m);
|
|
208
|
-
const cost = estimateTokens(line);
|
|
209
|
-
if (recentSpent + cost > recentBudget) continue;
|
|
210
|
-
sections.recent.push(line);
|
|
211
|
-
recentSpent += cost;
|
|
212
|
-
tokenBudget -= cost;
|
|
213
|
-
memoriesIncluded++;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// --- 4. Task-relevant memories (semantic search) ---
|
|
217
|
-
if (currentTask && tokenBudget > 200) {
|
|
218
|
-
let queryEmbedding: number[] | null = null;
|
|
219
|
-
try {
|
|
220
|
-
queryEmbedding = await getEmbedding(currentTask);
|
|
221
|
-
} catch {}
|
|
222
|
-
|
|
223
|
-
if (queryEmbedding) {
|
|
224
|
-
// Score all non-included memories by relevance
|
|
225
|
-
const includedIds = new Set([
|
|
226
|
-
...permanent.map((m) => m.id),
|
|
227
|
-
...recent.filter((_, i) => i < sections.recent.length).map((m) => m.id),
|
|
228
|
-
]);
|
|
229
|
-
|
|
230
|
-
const scored = allMemories
|
|
231
|
-
.filter((m) => !includedIds.has(m.id) && !supersededIds.has(m.id) && m.embedding?.length > 100)
|
|
232
|
-
.map((m) => {
|
|
233
|
-
let dot = 0;
|
|
234
|
-
const len = Math.min(queryEmbedding!.length, m.embedding.length);
|
|
235
|
-
for (let i = 0; i < len; i++) dot += queryEmbedding![i] * m.embedding[i];
|
|
236
|
-
return { memory: m, score: dot };
|
|
237
|
-
})
|
|
238
|
-
.filter((s) => s.score > 0.3)
|
|
239
|
-
.sort((a, b) => b.score - a.score);
|
|
240
|
-
|
|
241
|
-
for (const { memory: m } of scored) {
|
|
242
|
-
const line = formatMemory(m);
|
|
243
|
-
const cost = estimateTokens(line);
|
|
244
|
-
if (cost > tokenBudget) continue;
|
|
245
|
-
sections.relevant.push(line);
|
|
246
|
-
tokenBudget -= cost;
|
|
247
|
-
memoriesIncluded++;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// --- 5. Recent OrgEvents for this agent ---
|
|
253
|
-
try {
|
|
254
|
-
const eventSince = data?.lastBootAt
|
|
255
|
-
? new Date(data.lastBootAt)
|
|
256
|
-
: new Date(Date.now() - 24 * 3600_000);
|
|
257
|
-
const eventSinceStr = eventSince.toISOString();
|
|
258
|
-
const eventResults: any[] = [];
|
|
259
|
-
|
|
260
|
-
for await (const event of (databases as any).flair.OrgEvent.search()) {
|
|
261
|
-
if (!event.createdAt || event.createdAt < eventSinceStr) continue;
|
|
262
|
-
if (event.expiresAt && new Date(event.expiresAt) < new Date()) continue;
|
|
263
|
-
const targets = event.targetIds;
|
|
264
|
-
const isRelevant = !targets || targets.length === 0 || targets.includes(agentId);
|
|
265
|
-
if (!isRelevant) continue;
|
|
266
|
-
eventResults.push(event);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
eventResults.sort((a: any, b: any) => (a.createdAt || "").localeCompare(b.createdAt || ""));
|
|
270
|
-
for (const evt of eventResults.slice(0, 10)) {
|
|
271
|
-
const elapsed = Date.now() - new Date(evt.createdAt).getTime();
|
|
272
|
-
const mins = Math.floor(elapsed / 60_000);
|
|
273
|
-
const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
|
|
274
|
-
sections.events.push(`- ${evt.kind}: ${evt.summary} (${relTime})`);
|
|
275
|
-
}
|
|
276
|
-
} catch {
|
|
277
|
-
// non-fatal: OrgEvent table may not exist yet
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// --- Build context string ---
|
|
281
|
-
const parts: string[] = [];
|
|
282
|
-
|
|
283
|
-
if (sections.soul.length > 0) {
|
|
284
|
-
parts.push("## Identity\n" + sections.soul.join("\n"));
|
|
285
|
-
}
|
|
286
|
-
if (sections.skills.length > 0) {
|
|
287
|
-
parts.push("## Active Skills\n" + sections.skills.join("\n"));
|
|
288
|
-
}
|
|
289
|
-
if (sections.permanent.length > 0) {
|
|
290
|
-
parts.push("## Core Principles\n" + sections.permanent.join("\n"));
|
|
291
|
-
}
|
|
292
|
-
if (sections.recent.length > 0) {
|
|
293
|
-
parts.push("## Recent Context\n" + sections.recent.join("\n"));
|
|
294
|
-
}
|
|
295
|
-
if (sections.relevant.length > 0) {
|
|
296
|
-
parts.push("## Relevant Knowledge\n" + sections.relevant.join("\n"));
|
|
297
|
-
}
|
|
298
|
-
if (sections.events.length > 0) {
|
|
299
|
-
parts.push("## Recent Org Events\n" + sections.events.join("\n"));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const context = parts.join("\n\n");
|
|
303
|
-
const soulTokens = sections.soul.reduce((sum, line) => sum + estimateTokens(line), 0);
|
|
304
|
-
const memoryTokens = maxTokens - tokenBudget;
|
|
305
|
-
|
|
306
|
-
return {
|
|
307
|
-
context,
|
|
308
|
-
sections: {
|
|
309
|
-
soul: sections.soul.length,
|
|
310
|
-
skills: sections.skills.length,
|
|
311
|
-
permanent: sections.permanent.length,
|
|
312
|
-
recent: sections.recent.length,
|
|
313
|
-
relevant: sections.relevant.length,
|
|
314
|
-
events: sections.events.length,
|
|
315
|
-
},
|
|
316
|
-
tokenEstimate: soulTokens + memoryTokens,
|
|
317
|
-
soulTokens,
|
|
318
|
-
memoryTokens,
|
|
319
|
-
memoriesIncluded,
|
|
320
|
-
memoriesAvailable,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /MemoryConsolidate
|
|
3
|
-
*
|
|
4
|
-
* Reviews an agent's persistent memories and returns candidates for
|
|
5
|
-
* promotion (standard→persistent or persistent→permanent proposal) or
|
|
6
|
-
* archival, based on retrieval count and age.
|
|
7
|
-
*
|
|
8
|
-
* Request:
|
|
9
|
-
* agentId string — which agent
|
|
10
|
-
* scope string — "persistent" | "standard" | "all" (default: "persistent")
|
|
11
|
-
* olderThan string? — duration like "30d", "7d" (default: "30d")
|
|
12
|
-
* limit number? — max candidates (default: 20)
|
|
13
|
-
*
|
|
14
|
-
* Response:
|
|
15
|
-
* candidates Array<{ memory, suggestion, reason }>
|
|
16
|
-
* prompt string
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { Resource, databases } from "@harperfast/harper";
|
|
20
|
-
import { isAdmin } from "./auth-middleware.js";
|
|
21
|
-
|
|
22
|
-
function parseDuration(s: string): number {
|
|
23
|
-
const m = s.match(/^(\d+)([dhm])$/);
|
|
24
|
-
if (!m) return 30 * 86400_000;
|
|
25
|
-
const n = Number(m[1]);
|
|
26
|
-
if (m[2] === "d") return n * 86400_000;
|
|
27
|
-
if (m[2] === "h") return n * 3600_000;
|
|
28
|
-
if (m[2] === "m") return n * 60_000;
|
|
29
|
-
return 30 * 86400_000;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type Suggestion = "promote" | "archive" | "keep";
|
|
33
|
-
|
|
34
|
-
interface Candidate {
|
|
35
|
-
memory: Record<string, unknown>;
|
|
36
|
-
suggestion: Suggestion;
|
|
37
|
-
reason: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function evaluate(record: any, now: number, olderThanMs: number): Candidate {
|
|
41
|
-
const ageMs = record.createdAt ? now - new Date(record.createdAt).getTime() : 0;
|
|
42
|
-
const count = record.retrievalCount ?? 0;
|
|
43
|
-
const daysSinceRetrieved = record.lastRetrieved
|
|
44
|
-
? (now - new Date(record.lastRetrieved).getTime()) / 86400_000
|
|
45
|
-
: Infinity;
|
|
46
|
-
const { embedding, ...memory } = record;
|
|
47
|
-
|
|
48
|
-
// Promote: high retrieval + persistent durability
|
|
49
|
-
if (record.durability === "persistent" && count >= 5) {
|
|
50
|
-
return { memory, suggestion: "promote", reason: `Retrieved ${count} times — strong promotion candidate for permanent` };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Promote: standard → persistent if retrieved frequently
|
|
54
|
-
if (record.durability === "standard" && count >= 3 && ageMs > 7 * 86400_000) {
|
|
55
|
-
return { memory, suggestion: "promote", reason: `Retrieved ${count} times over ${Math.round(ageMs / 86400_000)} days — worth persisting` };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Archive: old + never retrieved
|
|
59
|
-
if (daysSinceRetrieved > 30 && count === 0 && ageMs > olderThanMs) {
|
|
60
|
-
return { memory, suggestion: "archive", reason: `Never retrieved, ${Math.round(ageMs / 86400_000)} days old` };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Archive: last retrieved > 60 days
|
|
64
|
-
if (daysSinceRetrieved > 60 && count < 2) {
|
|
65
|
-
return { memory, suggestion: "archive", reason: `Not retrieved in ${Math.round(daysSinceRetrieved)} days (only ${count} total retrievals)` };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { memory, suggestion: "keep", reason: `Retrieved ${count} times, ${Math.round(daysSinceRetrieved)} days since last retrieval` };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class ConsolidateMemories extends Resource {
|
|
72
|
-
async post(data: any) {
|
|
73
|
-
const { agentId, scope = "persistent", olderThan = "30d", limit = 20 } = data || {};
|
|
74
|
-
|
|
75
|
-
if (!agentId) return new Response(JSON.stringify({ error: "agentId required" }), { status: 400 });
|
|
76
|
-
|
|
77
|
-
const actorId = (this as any).request?.tpsAgent;
|
|
78
|
-
if (actorId && actorId !== agentId && !(await isAdmin(actorId))) {
|
|
79
|
-
return new Response(JSON.stringify({ error: "forbidden: can only consolidate own memories" }), { status: 403 });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const olderThanMs = parseDuration(olderThan);
|
|
83
|
-
const now = Date.now();
|
|
84
|
-
const candidates: Candidate[] = [];
|
|
85
|
-
|
|
86
|
-
for await (const record of (databases as any).flair.Memory.search()) {
|
|
87
|
-
if (record.agentId !== agentId) continue;
|
|
88
|
-
if (record.archived) continue;
|
|
89
|
-
if (record.durability === "permanent") continue; // permanent can't be demoted
|
|
90
|
-
|
|
91
|
-
if (scope === "persistent" && record.durability !== "persistent") continue;
|
|
92
|
-
if (scope === "standard" && record.durability !== "standard") continue;
|
|
93
|
-
|
|
94
|
-
const candidate = evaluate(record, now, olderThanMs);
|
|
95
|
-
candidates.push(candidate);
|
|
96
|
-
if (candidates.length >= limit * 3) break; // over-fetch to sort
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Sort: promote first, then archive, then keep
|
|
100
|
-
const order: Record<Suggestion, number> = { promote: 0, archive: 1, keep: 2 };
|
|
101
|
-
candidates.sort((a, b) => order[a.suggestion] - order[b.suggestion]);
|
|
102
|
-
const top = candidates.slice(0, limit);
|
|
103
|
-
|
|
104
|
-
const promoteCount = top.filter(c => c.suggestion === "promote").length;
|
|
105
|
-
const archiveCount = top.filter(c => c.suggestion === "archive").length;
|
|
106
|
-
|
|
107
|
-
const prompt = `# Memory Consolidation Review — ${agentId}
|
|
108
|
-
Scope: ${scope} | OlderThan: ${olderThan} | Candidates: ${top.length}
|
|
109
|
-
|
|
110
|
-
Summary: ${promoteCount} promote candidates, ${archiveCount} archive candidates.
|
|
111
|
-
|
|
112
|
-
For each candidate below:
|
|
113
|
-
- PROMOTE: Upgrade standard→persistent (self-approve) or propose persistent→permanent (needs human)
|
|
114
|
-
- ARCHIVE: Soft-delete (hidden from search, recoverable)
|
|
115
|
-
- KEEP: No action
|
|
116
|
-
|
|
117
|
-
Use: tps memory approve <id> | tps memory archive <id> | skip`;
|
|
118
|
-
|
|
119
|
-
return { candidates: top, prompt };
|
|
120
|
-
}
|
|
121
|
-
}
|
package/resources/MemoryFeed.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Resource, databases } from "@harperfast/harper";
|
|
2
|
-
import { computeContentHash, findExistingMemoryByContentHash } from "./memory-feed-lib.js";
|
|
3
|
-
|
|
4
|
-
export class FeedMemories extends Resource {
|
|
5
|
-
async post(content: any) {
|
|
6
|
-
const agentId = String(content?.agentId ?? "");
|
|
7
|
-
const body = String(content?.content ?? "");
|
|
8
|
-
if (!agentId || !body) {
|
|
9
|
-
return new Response(JSON.stringify({ error: "agentId and content are required" }), {
|
|
10
|
-
status: 400,
|
|
11
|
-
headers: { "Content-Type": "application/json" },
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const now = new Date().toISOString();
|
|
16
|
-
const contentHash = computeContentHash(agentId, body);
|
|
17
|
-
|
|
18
|
-
const existing = await findExistingMemoryByContentHash((databases as any).flair.Memory.search(), agentId, contentHash);
|
|
19
|
-
if (existing) return existing;
|
|
20
|
-
|
|
21
|
-
const record = {
|
|
22
|
-
...content,
|
|
23
|
-
id: content.id ?? `${agentId}-${Date.now()}`,
|
|
24
|
-
agentId,
|
|
25
|
-
content: body,
|
|
26
|
-
contentHash,
|
|
27
|
-
durability: content.durability ?? "standard",
|
|
28
|
-
createdAt: content.createdAt ?? now,
|
|
29
|
-
updatedAt: content.updatedAt ?? now,
|
|
30
|
-
archived: content.archived ?? false,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
await (databases as any).flair.Memory.put(record);
|
|
34
|
-
return record;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async *connect(target: any, incomingMessages: any) {
|
|
38
|
-
const subscription = await (databases as any).flair.Memory.subscribe(target);
|
|
39
|
-
|
|
40
|
-
if (!incomingMessages) {
|
|
41
|
-
return subscription;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
for await (const event of subscription) {
|
|
45
|
-
yield event;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MemoryMaintenance.ts — Maintenance worker for memory hygiene.
|
|
3
|
-
*
|
|
4
|
-
* POST /MemoryMaintenance/ — runs cleanup tasks:
|
|
5
|
-
* 1. Delete expired ephemeral memories (expiresAt < now)
|
|
6
|
-
* 2. Archive old session memories (> 30 days, standard durability)
|
|
7
|
-
* 3. Report stats
|
|
8
|
-
*
|
|
9
|
-
* Designed to run periodically (daily cron or heartbeat).
|
|
10
|
-
* Requires admin auth.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export default class MemoryMaintenance {
|
|
14
|
-
static ROUTE = "MemoryMaintenance";
|
|
15
|
-
static METHOD = "POST";
|
|
16
|
-
|
|
17
|
-
async post(data: any) {
|
|
18
|
-
const { databases }: any = this;
|
|
19
|
-
const request = (this as any).request;
|
|
20
|
-
const { dryRun = false, agentId } = data || {};
|
|
21
|
-
|
|
22
|
-
// Scope to authenticated agent. Admin can pass agentId for system-wide
|
|
23
|
-
// maintenance; non-admin always scoped to their own agent.
|
|
24
|
-
const authAgent = request?.headers?.get?.("x-tps-agent");
|
|
25
|
-
const isAdmin = (request as any)?.tpsAgentIsAdmin === true;
|
|
26
|
-
const targetAgent = isAdmin && agentId ? agentId : authAgent;
|
|
27
|
-
|
|
28
|
-
if (!targetAgent && !isAdmin) {
|
|
29
|
-
return { error: "agentId required" };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const now = new Date();
|
|
33
|
-
const stats = { expired: 0, archived: 0, total: 0, errors: 0, agent: targetAgent || "all" };
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
for await (const record of (databases as any).flair.Memory.search()) {
|
|
37
|
-
// Skip records not belonging to target agent (unless admin running system-wide)
|
|
38
|
-
if (targetAgent && record.agentId !== targetAgent) continue;
|
|
39
|
-
stats.total++;
|
|
40
|
-
|
|
41
|
-
// 1. Delete expired memories
|
|
42
|
-
if (record.expiresAt && new Date(record.expiresAt) < now) {
|
|
43
|
-
if (!dryRun) {
|
|
44
|
-
try {
|
|
45
|
-
await (databases as any).flair.Memory.delete(record.id);
|
|
46
|
-
stats.expired++;
|
|
47
|
-
} catch {
|
|
48
|
-
stats.errors++;
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
stats.expired++;
|
|
52
|
-
}
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 2. Archive old standard session memories (> 30 days)
|
|
57
|
-
// These are low-value session notes that weren't promoted to persistent.
|
|
58
|
-
// Archiving removes them from search results but keeps the data.
|
|
59
|
-
if (
|
|
60
|
-
record.durability === "standard" &&
|
|
61
|
-
record.type === "session" &&
|
|
62
|
-
!record.archived &&
|
|
63
|
-
record.createdAt
|
|
64
|
-
) {
|
|
65
|
-
const ageMs = now.getTime() - new Date(record.createdAt).getTime();
|
|
66
|
-
const ageDays = ageMs / (24 * 3600_000);
|
|
67
|
-
if (ageDays > 30) {
|
|
68
|
-
if (!dryRun) {
|
|
69
|
-
try {
|
|
70
|
-
// Soft archive — set archived flag, keep data
|
|
71
|
-
await (databases as any).flair.Memory.update(record.id, {
|
|
72
|
-
...record,
|
|
73
|
-
archived: true,
|
|
74
|
-
archivedAt: now.toISOString(),
|
|
75
|
-
});
|
|
76
|
-
stats.archived++;
|
|
77
|
-
} catch {
|
|
78
|
-
stats.errors++;
|
|
79
|
-
}
|
|
80
|
-
} else {
|
|
81
|
-
stats.archived++;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} catch (err: any) {
|
|
87
|
-
return { error: err.message, stats };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
message: dryRun ? "Dry run complete" : "Maintenance complete",
|
|
92
|
-
stats,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
}
|