@tpsdev-ai/flair 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.
Files changed (64) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +246 -0
  3. package/SECURITY.md +116 -0
  4. package/config.yaml +16 -0
  5. package/dist/resources/A2AAdapter.js +474 -0
  6. package/dist/resources/Agent.js +9 -0
  7. package/dist/resources/AgentCard.js +45 -0
  8. package/dist/resources/AgentSeed.js +111 -0
  9. package/dist/resources/IngestEvents.js +149 -0
  10. package/dist/resources/Integration.js +13 -0
  11. package/dist/resources/IssueTokens.js +19 -0
  12. package/dist/resources/Memory.js +122 -0
  13. package/dist/resources/MemoryBootstrap.js +263 -0
  14. package/dist/resources/MemoryConsolidate.js +105 -0
  15. package/dist/resources/MemoryFeed.js +41 -0
  16. package/dist/resources/MemoryReflect.js +105 -0
  17. package/dist/resources/OrgEvent.js +43 -0
  18. package/dist/resources/OrgEventCatchup.js +65 -0
  19. package/dist/resources/OrgEventMaintenance.js +29 -0
  20. package/dist/resources/SemanticSearch.js +147 -0
  21. package/dist/resources/SkillScan.js +101 -0
  22. package/dist/resources/Soul.js +9 -0
  23. package/dist/resources/SoulFeed.js +12 -0
  24. package/dist/resources/WorkspaceLatest.js +45 -0
  25. package/dist/resources/WorkspaceState.js +76 -0
  26. package/dist/resources/auth-middleware.js +470 -0
  27. package/dist/resources/embeddings-provider.js +127 -0
  28. package/dist/resources/embeddings.js +42 -0
  29. package/dist/resources/health.js +6 -0
  30. package/dist/resources/memory-feed-lib.js +15 -0
  31. package/dist/resources/table-helpers.js +35 -0
  32. package/package.json +62 -0
  33. package/resources/A2AAdapter.ts +510 -0
  34. package/resources/Agent.ts +10 -0
  35. package/resources/AgentCard.ts +65 -0
  36. package/resources/AgentSeed.ts +119 -0
  37. package/resources/IngestEvents.ts +189 -0
  38. package/resources/Integration.ts +14 -0
  39. package/resources/IssueTokens.ts +29 -0
  40. package/resources/Memory.ts +138 -0
  41. package/resources/MemoryBootstrap.ts +283 -0
  42. package/resources/MemoryConsolidate.ts +121 -0
  43. package/resources/MemoryFeed.ts +48 -0
  44. package/resources/MemoryReflect.ts +122 -0
  45. package/resources/OrgEvent.ts +63 -0
  46. package/resources/OrgEventCatchup.ts +89 -0
  47. package/resources/OrgEventMaintenance.ts +37 -0
  48. package/resources/SemanticSearch.ts +157 -0
  49. package/resources/SkillScan.ts +146 -0
  50. package/resources/Soul.ts +10 -0
  51. package/resources/SoulFeed.ts +15 -0
  52. package/resources/WorkspaceLatest.ts +66 -0
  53. package/resources/WorkspaceState.ts +102 -0
  54. package/resources/auth-middleware.ts +502 -0
  55. package/resources/embeddings-provider.ts +144 -0
  56. package/resources/embeddings.ts +28 -0
  57. package/resources/health.ts +7 -0
  58. package/resources/memory-feed-lib.ts +22 -0
  59. package/resources/table-helpers.ts +46 -0
  60. package/schemas/agent.graphql +22 -0
  61. package/schemas/event.graphql +12 -0
  62. package/schemas/memory.graphql +50 -0
  63. package/schemas/schema.graphql +41 -0
  64. package/schemas/workspace.graphql +14 -0
@@ -0,0 +1,283 @@
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 (unconditional — not subject to token budget) ---
72
+ // Soul is who you are. It's not optional context to be trimmed.
73
+ // Skill assignments (key='skill-assignment') are separated into their own section.
74
+ const skillAssignments: any[] = [];
75
+ if (includeSoul) {
76
+ let soulTokens = 0;
77
+ for await (const record of (databases as any).flair.Soul.search()) {
78
+ if (record.agentId !== agentId) continue;
79
+ if (record.key === "skill-assignment") {
80
+ skillAssignments.push(record);
81
+ continue;
82
+ }
83
+ const line = `**${record.key}:** ${record.value}`;
84
+ sections.soul.push(line);
85
+ soulTokens += estimateTokens(line);
86
+ }
87
+ // Soul tokens are tracked but don't reduce memory budget
88
+ tokenBudget = maxTokens; // memory budget is separate from soul
89
+ }
90
+
91
+ // --- 1b. Skill assignments (ordered by priority, conflict detection) ---
92
+ if (skillAssignments.length > 0) {
93
+ const priorityOrder: Record<string, number> = { critical: 0, high: 1, standard: 2, low: 3 };
94
+ skillAssignments.sort((a, b) => {
95
+ const pa = priorityOrder[a.priority ?? "standard"] ?? 2;
96
+ const pb = priorityOrder[b.priority ?? "standard"] ?? 2;
97
+ return pa - pb;
98
+ });
99
+
100
+ // Detect conflicts at same priority level
101
+ const byPriority = new Map<string, any[]>();
102
+ for (const skill of skillAssignments) {
103
+ const p = skill.priority ?? "standard";
104
+ if (!byPriority.has(p)) byPriority.set(p, []);
105
+ byPriority.get(p)!.push(skill);
106
+ }
107
+
108
+ for (const skill of skillAssignments) {
109
+ const p = skill.priority ?? "standard";
110
+ let meta: any = {};
111
+ try { meta = typeof skill.metadata === "string" ? JSON.parse(skill.metadata) : (skill.metadata ?? {}); } catch {}
112
+ const source = meta.source ? `, source: ${meta.source}` : "";
113
+ let line = `- ${skill.value} (${p} priority${source})`;
114
+ // Flag conflicts at same priority level
115
+ const peers = byPriority.get(p) ?? [];
116
+ if (peers.length > 1) {
117
+ line += " [SKILL_CONFLICT]";
118
+ }
119
+ sections.skills.push(line);
120
+ }
121
+ }
122
+
123
+ // --- 2. Permanent memories (always included, highest priority) ---
124
+ const allMemories: any[] = [];
125
+ for await (const record of (databases as any).flair.Memory.search()) {
126
+ if (record.agentId !== agentId) continue;
127
+ if (record.expiresAt && Date.parse(record.expiresAt) < Date.now()) continue;
128
+ allMemories.push(record);
129
+ }
130
+ memoriesAvailable = allMemories.length;
131
+
132
+ // Build superseded set: exclude memories that have been replaced by newer ones
133
+ const supersededIds = new Set<string>();
134
+ for (const m of allMemories) {
135
+ if (m.supersedes) supersededIds.add(m.supersedes);
136
+ }
137
+ const activeMemories = allMemories.filter((m) => !supersededIds.has(m.id));
138
+
139
+ const permanent = activeMemories.filter((m) => m.durability === "permanent");
140
+ for (const m of permanent) {
141
+ const line = formatMemory(m);
142
+ const cost = estimateTokens(line);
143
+ if (cost <= tokenBudget) {
144
+ sections.permanent.push(line);
145
+ tokenBudget -= cost;
146
+ memoriesIncluded++;
147
+ }
148
+ }
149
+
150
+ // --- 3. Recent memories (last 24-48h, standard + persistent) ---
151
+ const sinceDate = since
152
+ ? new Date(since)
153
+ : new Date(Date.now() - 48 * 3600_000);
154
+ const recent = activeMemories
155
+ .filter(
156
+ (m) =>
157
+ m.durability !== "permanent" &&
158
+ m.createdAt &&
159
+ new Date(m.createdAt) >= sinceDate
160
+ )
161
+ .sort((a: any, b: any) => (b.createdAt || "").localeCompare(a.createdAt || ""));
162
+
163
+ // Budget: up to 40% of remaining for recent
164
+ const recentBudget = Math.floor(tokenBudget * 0.4);
165
+ let recentSpent = 0;
166
+ for (const m of recent) {
167
+ const line = formatMemory(m);
168
+ const cost = estimateTokens(line);
169
+ if (recentSpent + cost > recentBudget) continue;
170
+ sections.recent.push(line);
171
+ recentSpent += cost;
172
+ tokenBudget -= cost;
173
+ memoriesIncluded++;
174
+ }
175
+
176
+ // --- 4. Task-relevant memories (semantic search) ---
177
+ if (currentTask && tokenBudget > 200) {
178
+ let queryEmbedding: number[] | null = null;
179
+ try {
180
+ queryEmbedding = await getEmbedding(currentTask);
181
+ } catch {}
182
+
183
+ if (queryEmbedding) {
184
+ // Score all non-included memories by relevance
185
+ const includedIds = new Set([
186
+ ...permanent.map((m) => m.id),
187
+ ...recent.filter((_, i) => i < sections.recent.length).map((m) => m.id),
188
+ ]);
189
+
190
+ const scored = allMemories
191
+ .filter((m) => !includedIds.has(m.id) && !supersededIds.has(m.id) && m.embedding?.length > 100)
192
+ .map((m) => {
193
+ let dot = 0;
194
+ const len = Math.min(queryEmbedding!.length, m.embedding.length);
195
+ for (let i = 0; i < len; i++) dot += queryEmbedding![i] * m.embedding[i];
196
+ return { memory: m, score: dot };
197
+ })
198
+ .filter((s) => s.score > 0.3)
199
+ .sort((a, b) => b.score - a.score);
200
+
201
+ for (const { memory: m } of scored) {
202
+ const line = formatMemory(m);
203
+ const cost = estimateTokens(line);
204
+ if (cost > tokenBudget) continue;
205
+ sections.relevant.push(line);
206
+ tokenBudget -= cost;
207
+ memoriesIncluded++;
208
+ }
209
+ }
210
+ }
211
+
212
+ // --- 5. Recent OrgEvents for this agent ---
213
+ try {
214
+ const eventSince = data?.lastBootAt
215
+ ? new Date(data.lastBootAt)
216
+ : new Date(Date.now() - 24 * 3600_000);
217
+ const eventSinceStr = eventSince.toISOString();
218
+ const eventResults: any[] = [];
219
+
220
+ for await (const event of (databases as any).flair.OrgEvent.search()) {
221
+ if (!event.createdAt || event.createdAt < eventSinceStr) continue;
222
+ if (event.expiresAt && new Date(event.expiresAt) < new Date()) continue;
223
+ const targets = event.targetIds;
224
+ const isRelevant = !targets || targets.length === 0 || targets.includes(agentId);
225
+ if (!isRelevant) continue;
226
+ eventResults.push(event);
227
+ }
228
+
229
+ eventResults.sort((a: any, b: any) => (a.createdAt || "").localeCompare(b.createdAt || ""));
230
+ for (const evt of eventResults.slice(0, 10)) {
231
+ const elapsed = Date.now() - new Date(evt.createdAt).getTime();
232
+ const mins = Math.floor(elapsed / 60_000);
233
+ const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
234
+ sections.events.push(`- ${evt.kind}: ${evt.summary} (${relTime})`);
235
+ }
236
+ } catch {
237
+ // non-fatal: OrgEvent table may not exist yet
238
+ }
239
+
240
+ // --- Build context string ---
241
+ const parts: string[] = [];
242
+
243
+ if (sections.soul.length > 0) {
244
+ parts.push("## Identity\n" + sections.soul.join("\n"));
245
+ }
246
+ if (sections.skills.length > 0) {
247
+ parts.push("## Active Skills\n" + sections.skills.join("\n"));
248
+ }
249
+ if (sections.permanent.length > 0) {
250
+ parts.push("## Core Principles\n" + sections.permanent.join("\n"));
251
+ }
252
+ if (sections.recent.length > 0) {
253
+ parts.push("## Recent Context\n" + sections.recent.join("\n"));
254
+ }
255
+ if (sections.relevant.length > 0) {
256
+ parts.push("## Relevant Knowledge\n" + sections.relevant.join("\n"));
257
+ }
258
+ if (sections.events.length > 0) {
259
+ parts.push("## Recent Org Events\n" + sections.events.join("\n"));
260
+ }
261
+
262
+ const context = parts.join("\n\n");
263
+ const soulTokens = sections.soul.reduce((sum, line) => sum + estimateTokens(line), 0);
264
+ const memoryTokens = maxTokens - tokenBudget;
265
+
266
+ return {
267
+ context,
268
+ sections: {
269
+ soul: sections.soul.length,
270
+ skills: sections.skills.length,
271
+ permanent: sections.permanent.length,
272
+ recent: sections.recent.length,
273
+ relevant: sections.relevant.length,
274
+ events: sections.events.length,
275
+ },
276
+ tokenEstimate: soulTokens + memoryTokens,
277
+ soulTokens,
278
+ memoryTokens,
279
+ memoriesIncluded,
280
+ memoriesAvailable,
281
+ };
282
+ }
283
+ }
@@ -0,0 +1,121 @@
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
+ }
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * POST /MemoryReflect
3
+ *
4
+ * Gathers recent memories for an agent and returns a structured reflection
5
+ * prompt. The agent feeds the prompt + memories to its LLM and writes
6
+ * insights back as persistent memories (with derivedFrom linking).
7
+ *
8
+ * Request:
9
+ * agentId string — which agent to reflect on
10
+ * scope string — "recent" | "tagged" | "all" (default: "recent")
11
+ * since string? — ISO timestamp lower bound (default: 24h ago)
12
+ * maxMemories number? — cap (default: 50)
13
+ * focus string? — "lessons_learned" | "patterns" | "decisions" | "errors" (default: "lessons_learned")
14
+ * tag string? — required when scope="tagged"
15
+ *
16
+ * Response:
17
+ * memories Memory[] — source memories included in the prompt
18
+ * prompt string — structured LLM prompt
19
+ * suggestedTags string[] — tags Flair detected in the source set
20
+ * count number — number of memories included
21
+ */
22
+
23
+ import { Resource, databases } from "@harperfast/harper";
24
+ import { isAdmin } from "./auth-middleware.js";
25
+ import { patchRecordSilent } from "./table-helpers.js";
26
+
27
+ const FOCUS_PROMPTS: Record<string, string> = {
28
+ lessons_learned:
29
+ "Review these memories and identify concrete lessons learned. For each lesson: what happened, what you learned, and how it should change future behavior. Write atomic memories with durability=persistent.",
30
+ patterns:
31
+ "Identify recurring patterns across these memories. What themes, approaches, or outcomes appear multiple times? Extract each pattern as a persistent memory.",
32
+ decisions:
33
+ "Catalog the key decisions made and their outcomes. For each: what was decided, why, and what resulted. Promote important decisions to persistent.",
34
+ errors:
35
+ "Extract errors, bugs, and failures. For each: what failed, root cause, and fix applied. These are high-value persistent memories.",
36
+ };
37
+
38
+ export class ReflectMemories extends Resource {
39
+ async post(data: any) {
40
+ const {
41
+ agentId,
42
+ scope = "recent",
43
+ since,
44
+ maxMemories = 50,
45
+ focus = "lessons_learned",
46
+ tag,
47
+ } = data || {};
48
+
49
+ if (!agentId) return new Response(JSON.stringify({ error: "agentId required" }), { status: 400 });
50
+
51
+ // Auth: agent can only reflect on own memories unless admin
52
+ const actorId = (this as any).request?.tpsAgent;
53
+ if (actorId && actorId !== agentId && !(await isAdmin(actorId))) {
54
+ return new Response(JSON.stringify({ error: "forbidden: can only reflect on own memories" }), { status: 403 });
55
+ }
56
+
57
+ const sinceDate = since ? new Date(since) : new Date(Date.now() - 24 * 3600_000);
58
+ const memories: any[] = [];
59
+
60
+ for await (const record of (databases as any).flair.Memory.search()) {
61
+ if (record.agentId !== agentId) continue;
62
+ if (record.archived) continue;
63
+ if (record.durability === "permanent") continue; // permanent memories don't need reflection
64
+
65
+ if (scope === "tagged") {
66
+ if (!tag || !(record.tags ?? []).includes(tag)) continue;
67
+ } else if (scope === "recent") {
68
+ if (!record.createdAt || new Date(record.createdAt) < sinceDate) continue;
69
+ }
70
+ // scope="all" passes everything
71
+
72
+ const { embedding, ...rest } = record;
73
+ memories.push(rest);
74
+ if (memories.length >= maxMemories) break;
75
+ }
76
+
77
+ memories.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
78
+
79
+ // Collect tags present in source memories
80
+ const tagSet = new Set<string>();
81
+ for (const m of memories) {
82
+ for (const t of m.tags ?? []) tagSet.add(t);
83
+ }
84
+
85
+ // Build prompt
86
+ const focusText = FOCUS_PROMPTS[focus] ?? FOCUS_PROMPTS.lessons_learned;
87
+ const memorySummary = memories
88
+ .map((m, i) => `[${i + 1}] (${m.id}) ${m.createdAt?.slice(0, 10) ?? "?"}: ${m.content.slice(0, 300)}`)
89
+ .join("\n");
90
+
91
+ const prompt = `# Memory Reflection — ${agentId}
92
+ Focus: ${focus}
93
+ Scope: ${scope} (since ${sinceDate.toISOString()})
94
+ Memories: ${memories.length}
95
+
96
+ ## Task
97
+ ${focusText}
98
+
99
+ ## Source Memories
100
+ ${memorySummary || "(none)"}
101
+
102
+ ## Instructions
103
+ For each insight:
104
+ 1. Write a new memory with durability=persistent
105
+ 2. Set derivedFrom=[<source memory ids>]
106
+ 3. Set tags from the source memories where relevant
107
+ 4. Keep each memory atomic — one insight per record`;
108
+
109
+ // Update lastReflected on source memories (read-modify-write to preserve embeddings)
110
+ const now = new Date().toISOString();
111
+ for (const m of memories) {
112
+ patchRecordSilent((databases as any).flair.Memory, m.id, { lastReflected: now });
113
+ }
114
+
115
+ return {
116
+ memories,
117
+ prompt,
118
+ suggestedTags: [...tagSet].slice(0, 20),
119
+ count: memories.length,
120
+ };
121
+ }
122
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * OrgEvent.ts — Harper table resource for org-wide activity events.
3
+ *
4
+ * Auth: Ed25519 middleware sets request.tpsAgent.
5
+ * Write: authorId must match authenticated agent (or admin).
6
+ * Read: any authenticated participant can read (org-scoped).
7
+ */
8
+
9
+ import { databases } from "@harperfast/harper";
10
+
11
+ export class OrgEvent extends (databases as any).flair.OrgEvent {
12
+ async post(content: any, context?: any) {
13
+ const agentId = context?.request?.tpsAgent;
14
+
15
+ // authorId must match authenticated agent (unless admin)
16
+ if (agentId && !context?.request?.tpsAgentIsAdmin && content.authorId !== agentId) {
17
+ return new Response(
18
+ JSON.stringify({ error: "forbidden: authorId must match authenticated agent" }),
19
+ { status: 403, headers: { "Content-Type": "application/json" } },
20
+ );
21
+ }
22
+
23
+ // Generate composite ID if not provided
24
+ if (!content.id) {
25
+ content.id = `${content.authorId}-${Date.now()}`;
26
+ }
27
+
28
+ content.createdAt = new Date().toISOString();
29
+
30
+ // Harper 5: table resources use put() for create/upsert (post() removed)
31
+ return (databases as any).flair.OrgEvent.put(content);
32
+ }
33
+
34
+ async put(content: any, context?: any) {
35
+ const agentId = context?.request?.tpsAgent;
36
+
37
+ if (agentId && !context?.request?.tpsAgentIsAdmin && content.authorId !== agentId) {
38
+ return new Response(
39
+ JSON.stringify({ error: "forbidden: authorId must match authenticated agent" }),
40
+ { status: 403, headers: { "Content-Type": "application/json" } },
41
+ );
42
+ }
43
+
44
+ return (databases as any).flair.OrgEvent.put(content);
45
+ }
46
+
47
+ async delete(id: any, context?: any) {
48
+ const agentId = context?.request?.tpsAgent;
49
+ if (!agentId) return super.delete(id, context);
50
+
51
+ const record = await this.get(id);
52
+ if (!record) return super.delete(id, context);
53
+
54
+ if (!context?.request?.tpsAgentIsAdmin && record.authorId !== agentId) {
55
+ return new Response(
56
+ JSON.stringify({ error: "forbidden: cannot delete events authored by another agent" }),
57
+ { status: 403, headers: { "Content-Type": "application/json" } },
58
+ );
59
+ }
60
+
61
+ return super.delete(id, context);
62
+ }
63
+ }