@loreai/core 0.0.1 → 0.10.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -5
  3. package/dist/bun/agents-file.d.ts +59 -0
  4. package/dist/bun/agents-file.d.ts.map +1 -0
  5. package/dist/bun/config.d.ts +58 -0
  6. package/dist/bun/config.d.ts.map +1 -0
  7. package/dist/bun/curator.d.ts +35 -0
  8. package/dist/bun/curator.d.ts.map +1 -0
  9. package/dist/bun/db/driver.bun.d.ts +5 -0
  10. package/dist/bun/db/driver.bun.d.ts.map +1 -0
  11. package/dist/bun/db/driver.node.d.ts +15 -0
  12. package/dist/bun/db/driver.node.d.ts.map +1 -0
  13. package/dist/bun/db.d.ts +22 -0
  14. package/dist/bun/db.d.ts.map +1 -0
  15. package/dist/bun/distillation.d.ts +32 -0
  16. package/dist/bun/distillation.d.ts.map +1 -0
  17. package/dist/bun/embedding.d.ts +90 -0
  18. package/dist/bun/embedding.d.ts.map +1 -0
  19. package/dist/bun/gradient.d.ts +73 -0
  20. package/dist/bun/gradient.d.ts.map +1 -0
  21. package/dist/bun/index.d.ts +19 -0
  22. package/dist/bun/index.d.ts.map +1 -0
  23. package/dist/bun/index.js +28236 -0
  24. package/dist/bun/index.js.map +7 -0
  25. package/dist/bun/lat-reader.d.ts +69 -0
  26. package/dist/bun/lat-reader.d.ts.map +1 -0
  27. package/dist/bun/log.d.ts +17 -0
  28. package/dist/bun/log.d.ts.map +1 -0
  29. package/dist/bun/ltm.d.ts +138 -0
  30. package/dist/bun/ltm.d.ts.map +1 -0
  31. package/dist/bun/markdown.d.ts +37 -0
  32. package/dist/bun/markdown.d.ts.map +1 -0
  33. package/dist/bun/prompt.d.ts +47 -0
  34. package/dist/bun/prompt.d.ts.map +1 -0
  35. package/dist/bun/recall.d.ts +41 -0
  36. package/dist/bun/recall.d.ts.map +1 -0
  37. package/dist/bun/search.d.ts +113 -0
  38. package/dist/bun/search.d.ts.map +1 -0
  39. package/dist/bun/temporal.d.ts +66 -0
  40. package/dist/bun/temporal.d.ts.map +1 -0
  41. package/dist/bun/types.d.ts +180 -0
  42. package/dist/bun/types.d.ts.map +1 -0
  43. package/dist/bun/worker.d.ts +6 -0
  44. package/dist/bun/worker.d.ts.map +1 -0
  45. package/dist/node/agents-file.d.ts +59 -0
  46. package/dist/node/agents-file.d.ts.map +1 -0
  47. package/dist/node/config.d.ts +58 -0
  48. package/dist/node/config.d.ts.map +1 -0
  49. package/dist/node/curator.d.ts +35 -0
  50. package/dist/node/curator.d.ts.map +1 -0
  51. package/dist/node/db/driver.bun.d.ts +5 -0
  52. package/dist/node/db/driver.bun.d.ts.map +1 -0
  53. package/dist/node/db/driver.node.d.ts +15 -0
  54. package/dist/node/db/driver.node.d.ts.map +1 -0
  55. package/dist/node/db.d.ts +22 -0
  56. package/dist/node/db.d.ts.map +1 -0
  57. package/dist/node/distillation.d.ts +32 -0
  58. package/dist/node/distillation.d.ts.map +1 -0
  59. package/dist/node/embedding.d.ts +90 -0
  60. package/dist/node/embedding.d.ts.map +1 -0
  61. package/dist/node/gradient.d.ts +73 -0
  62. package/dist/node/gradient.d.ts.map +1 -0
  63. package/dist/node/index.d.ts +19 -0
  64. package/dist/node/index.d.ts.map +1 -0
  65. package/dist/node/index.js +28253 -0
  66. package/dist/node/index.js.map +7 -0
  67. package/dist/node/lat-reader.d.ts +69 -0
  68. package/dist/node/lat-reader.d.ts.map +1 -0
  69. package/dist/node/log.d.ts +17 -0
  70. package/dist/node/log.d.ts.map +1 -0
  71. package/dist/node/ltm.d.ts +138 -0
  72. package/dist/node/ltm.d.ts.map +1 -0
  73. package/dist/node/markdown.d.ts +37 -0
  74. package/dist/node/markdown.d.ts.map +1 -0
  75. package/dist/node/prompt.d.ts +47 -0
  76. package/dist/node/prompt.d.ts.map +1 -0
  77. package/dist/node/recall.d.ts +41 -0
  78. package/dist/node/recall.d.ts.map +1 -0
  79. package/dist/node/search.d.ts +113 -0
  80. package/dist/node/search.d.ts.map +1 -0
  81. package/dist/node/temporal.d.ts +66 -0
  82. package/dist/node/temporal.d.ts.map +1 -0
  83. package/dist/node/types.d.ts +180 -0
  84. package/dist/node/types.d.ts.map +1 -0
  85. package/dist/node/worker.d.ts +6 -0
  86. package/dist/node/worker.d.ts.map +1 -0
  87. package/dist/types/agents-file.d.ts +59 -0
  88. package/dist/types/agents-file.d.ts.map +1 -0
  89. package/dist/types/config.d.ts +58 -0
  90. package/dist/types/config.d.ts.map +1 -0
  91. package/dist/types/curator.d.ts +35 -0
  92. package/dist/types/curator.d.ts.map +1 -0
  93. package/dist/types/db/driver.bun.d.ts +5 -0
  94. package/dist/types/db/driver.bun.d.ts.map +1 -0
  95. package/dist/types/db/driver.node.d.ts +15 -0
  96. package/dist/types/db/driver.node.d.ts.map +1 -0
  97. package/dist/types/db.d.ts +22 -0
  98. package/dist/types/db.d.ts.map +1 -0
  99. package/dist/types/distillation.d.ts +32 -0
  100. package/dist/types/distillation.d.ts.map +1 -0
  101. package/dist/types/embedding.d.ts +90 -0
  102. package/dist/types/embedding.d.ts.map +1 -0
  103. package/dist/types/gradient.d.ts +73 -0
  104. package/dist/types/gradient.d.ts.map +1 -0
  105. package/dist/types/index.d.ts +19 -0
  106. package/dist/types/index.d.ts.map +1 -0
  107. package/dist/types/lat-reader.d.ts +69 -0
  108. package/dist/types/lat-reader.d.ts.map +1 -0
  109. package/dist/types/log.d.ts +17 -0
  110. package/dist/types/log.d.ts.map +1 -0
  111. package/dist/types/ltm.d.ts +138 -0
  112. package/dist/types/ltm.d.ts.map +1 -0
  113. package/dist/types/markdown.d.ts +37 -0
  114. package/dist/types/markdown.d.ts.map +1 -0
  115. package/dist/types/prompt.d.ts +47 -0
  116. package/dist/types/prompt.d.ts.map +1 -0
  117. package/dist/types/recall.d.ts +41 -0
  118. package/dist/types/recall.d.ts.map +1 -0
  119. package/dist/types/search.d.ts +113 -0
  120. package/dist/types/search.d.ts.map +1 -0
  121. package/dist/types/temporal.d.ts +66 -0
  122. package/dist/types/temporal.d.ts.map +1 -0
  123. package/dist/types/types.d.ts +180 -0
  124. package/dist/types/types.d.ts.map +1 -0
  125. package/dist/types/worker.d.ts +6 -0
  126. package/dist/types/worker.d.ts.map +1 -0
  127. package/package.json +48 -5
  128. package/src/agents-file.ts +406 -0
  129. package/src/config.ts +132 -0
  130. package/src/curator.ts +220 -0
  131. package/src/db/driver.bun.ts +18 -0
  132. package/src/db/driver.node.ts +54 -0
  133. package/src/db.ts +433 -0
  134. package/src/distillation.ts +433 -0
  135. package/src/embedding.ts +528 -0
  136. package/src/gradient.ts +1387 -0
  137. package/src/index.ts +109 -0
  138. package/src/lat-reader.ts +374 -0
  139. package/src/log.ts +27 -0
  140. package/src/ltm.ts +861 -0
  141. package/src/markdown.ts +129 -0
  142. package/src/prompt.ts +454 -0
  143. package/src/recall.ts +446 -0
  144. package/src/search.ts +330 -0
  145. package/src/temporal.ts +379 -0
  146. package/src/types.ts +199 -0
  147. package/src/worker.ts +26 -0
@@ -0,0 +1,129 @@
1
+ import { remark } from "remark";
2
+ import type {
3
+ Root,
4
+ Heading,
5
+ List,
6
+ ListItem,
7
+ Paragraph,
8
+ Text,
9
+ Strong,
10
+ BlockContent,
11
+ PhrasingContent,
12
+ } from "mdast";
13
+
14
+ // Reuse a single processor — remark freezes on first use anyway
15
+ const processor = remark();
16
+
17
+ // Serialize an mdast tree to a markdown string.
18
+ // The serializer automatically escapes any characters in text nodes
19
+ // that would be structurally ambiguous (code fences, headings, list
20
+ // markers, thematic breaks, etc.), so callers never need to pre-escape.
21
+ export function serialize(tree: Root): string {
22
+ return processor.stringify(tree);
23
+ }
24
+
25
+ /**
26
+ * Replace unpaired Unicode surrogates with U+FFFD (replacement character).
27
+ *
28
+ * Unpaired surrogates (a high surrogate U+D800-U+DBFF without a following low
29
+ * surrogate U+DC00-U+DFFF, or a lone low surrogate) are technically invalid in
30
+ * UTF-8/JSON. They can appear in tool outputs (binary file contents, command
31
+ * output) and survive through SQLite storage into recall results. When the
32
+ * resulting string is serialized to JSON for the LLM API, the API rejects it
33
+ * with "no low surrogate in string".
34
+ */
35
+ export function sanitizeSurrogates(value: string): string {
36
+ // eslint-disable-next-line no-control-regex
37
+ return value.replace(
38
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
39
+ "\uFFFD",
40
+ );
41
+ }
42
+
43
+ // Collapse newlines in LLM-generated text before inserting into a text node.
44
+ // Embedded blank lines (\n\n) cause list items to become "spread" (loose),
45
+ // which then breaks the surrounding markdown structure on re-parse.
46
+ // Newlines within a single fact/narrative are replaced with a space.
47
+ // Also sanitizes unpaired surrogates to prevent JSON serialization failures.
48
+ export function inline(value: string): string {
49
+ return sanitizeSurrogates(value).replace(/\s*\n\s*/g, " ").trim();
50
+ }
51
+
52
+ // Normalize arbitrary markdown via parse → stringify roundtrip.
53
+ // Used for content we don't control (e.g. existing text parts in Layer 4
54
+ // after tool parts are stripped out), where we can't build from AST.
55
+ // Two passes are needed: remark's asterisk/underscore escaping can introduce
56
+ // new sequences on the first pass that the second pass then stabilizes.
57
+ export function normalize(md: string): string {
58
+ const once = processor.stringify(processor.parse(md));
59
+ return processor.stringify(processor.parse(once));
60
+ }
61
+
62
+ /**
63
+ * Unescape a markdown-serialized inline string back to plain text.
64
+ *
65
+ * remark's serializer escapes special characters with backslashes
66
+ * (e.g. `<` → `\<`, `*` → `\*`, `\` → `\\`). When we read content
67
+ * back from an AGENTS.md file we must unescape it so it round-trips
68
+ * cleanly — otherwise each export/import cycle doubles the escapes.
69
+ *
70
+ * Uses remark's own parser to extract the text value, which handles
71
+ * all escape sequences correctly.
72
+ */
73
+ export function unescapeMarkdown(md: string): string {
74
+ const tree = processor.parse(md);
75
+ // Collect all text node values from the first paragraph
76
+ const texts: string[] = [];
77
+ const para = tree.children[0];
78
+ if (para && para.type === "paragraph") {
79
+ for (const child of para.children) {
80
+ if (child.type === "text") texts.push(child.value);
81
+ else if (child.type === "strong" || child.type === "emphasis") {
82
+ for (const gc of child.children) {
83
+ if (gc.type === "text") texts.push(gc.value);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ return texts.join("") || md;
89
+ }
90
+
91
+ // --- Node builders ---
92
+
93
+ export function h(depth: 1 | 2 | 3 | 4 | 5 | 6, value: string): Heading {
94
+ return { type: "heading", depth, children: [t(value)] };
95
+ }
96
+
97
+ export function p(value: string): Paragraph {
98
+ return { type: "paragraph", children: [t(value)] };
99
+ }
100
+
101
+ export function ul(items: ListItem[]): List {
102
+ return { type: "list", ordered: false, spread: false, children: items };
103
+ }
104
+
105
+ export function li(...children: BlockContent[]): ListItem {
106
+ return { type: "listItem", spread: false, children };
107
+ }
108
+
109
+ // List item containing a single paragraph (the common case for facts/entries)
110
+ export function lip(value: string): ListItem {
111
+ return li(p(value));
112
+ }
113
+
114
+ // List item with inline phrasing content — e.g. **bold**: text
115
+ export function liph(...children: PhrasingContent[]): ListItem {
116
+ return li({ type: "paragraph", children });
117
+ }
118
+
119
+ export function t(value: string): Text {
120
+ return { type: "text", value };
121
+ }
122
+
123
+ export function strong(value: string): Strong {
124
+ return { type: "strong", children: [t(value)] };
125
+ }
126
+
127
+ export function root(...children: Root["children"]): Root {
128
+ return { type: "root", children };
129
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,454 @@
1
+ import type { Root } from "mdast";
2
+ import { serialize, inline, h, ul, liph, strong, t, root } from "./markdown";
3
+
4
+ // All prompts are locked down — they are our core value offering.
5
+ // Do not make these configurable.
6
+
7
+ export const DISTILLATION_SYSTEM = `You are a memory observer. Your observations will be the ONLY information an AI assistant has about past interactions. Produce a dense, dated event log — not a summary.
8
+
9
+ CRITICAL: DISTINGUISH USER ASSERTIONS FROM QUESTIONS
10
+
11
+ When the user TELLS you something about themselves, mark it as an assertion (🔴):
12
+ - "I have two kids" → 🔴 (14:30) User stated has two kids
13
+ - "I work at Acme Corp" → 🔴 (14:31) User stated works at Acme Corp
14
+
15
+ When the user ASKS about something, mark it as a question (🟡):
16
+ - "Can you help me with X?" → 🟡 (15:00) User asked for help with X
17
+
18
+ User assertions are AUTHORITATIVE — the user is the source of truth about their own life.
19
+
20
+ TEMPORAL ANCHORING — CRITICAL FOR TEMPORAL REASONING:
21
+
22
+ Each observation has up to two timestamps:
23
+ 1. BEGINNING: The time the statement was made — ALWAYS include this as (HH:MM)
24
+ 2. END: The referenced date, if the content refers to a different time — add as "(meaning DATE)" or "(estimated DATE)"
25
+
26
+ ONLY add "(meaning DATE)" when you can derive an actual date:
27
+ - "last week", "yesterday", "next month" → compute and add the date
28
+ - "recently", "a while ago", "soon" → too vague, omit the end date
29
+
30
+ ALWAYS put the date annotation at the END of the observation line.
31
+
32
+ GOOD: (09:15) User will visit parents this weekend. (meaning Jun 17-18, 2025)
33
+ GOOD: (09:15) User's friend had a birthday party last month. (estimated May 2025)
34
+ GOOD: (09:15) User prefers hiking in the mountains.
35
+ BAD: (09:15) User prefers hiking. (meaning Jun 15, 2025) ← no time reference, don't add date
36
+
37
+ If an observation contains MULTIPLE events, split into SEPARATE lines, each with its own date.
38
+
39
+ STATE CHANGES — make supersession explicit:
40
+ - "User will use X (replacing Y)" — not just "User will use X"
41
+ - "User moved to Berlin (no longer in London)"
42
+
43
+ DETAILS TO ALWAYS PRESERVE:
44
+ - Names, handles, usernames (@username, "Dr. Smith")
45
+ - Numbers, counts, quantities (4 items, 3 sessions, $120)
46
+ - Measurements, percentages (5kg, 20% improvement, 85% accuracy)
47
+ - Sequences and orderings (steps 1-5, lucky numbers: 7 14 23)
48
+ - Prices, dates, times, durations
49
+ - Locations and distinguishing attributes
50
+ - User's specific role (presenter, volunteer, organizer — not just "attended")
51
+ - Exact phrasing when unusual ("movement session" for exercise)
52
+
53
+ EXACT NUMBERS — NEVER APPROXIMATE:
54
+
55
+ When the conversation states a specific count, record that EXACT number — do not round, estimate, or substitute a count you see later. If the same quantity appears with different values at different times, record each with its timestamp.
56
+
57
+ BAD: All existing entries bulk-updated to cross_project=1 (50 entries) ← wrong: mixed up with a later count
58
+ GOOD: 43 knowledge entries bulk-updated to cross_project=1 via SQL UPDATE ← exact number from the operation
59
+
60
+ BAD: ~130 test failures
61
+ GOOD: 131 test failures (1902 pass, 131 fail, 1 error across 100 files) ← preserve exact counts
62
+
63
+ BUG FIXES AND CODE CHANGES — HIGH PRIORITY:
64
+
65
+ Every bug fix, code change, or technical decision is important regardless of where it appears in the conversation. Early-session fixes are just as valuable as later ones.
66
+
67
+ For each fix, record:
68
+ - The specific bug/problem (what went wrong)
69
+ - The root cause (why it went wrong)
70
+ - The fix applied (what changed, with file paths and line numbers)
71
+ - The outcome (tests pass, deployed, etc.)
72
+
73
+ BAD: 🟡 Fixed an FTS5 search bug
74
+ GOOD: 🟡 FTS5 was doing exact term matching instead of prefix matching in ltm.ts. Fix: added ftsQuery() function that appends * to each search term for prefix matching. Committed as [hash].
75
+
76
+ ASSISTANT-GENERATED CONTENT — THIS IS CRITICAL:
77
+
78
+ When the assistant produces lists, recommendations, explanations, recipes, schedules, creative content, or any structured output — record EVERY ITEM with its distinguishing details. The user WILL ask about specific items later.
79
+
80
+ BAD: 🟡 Assistant recommended 5 dessert spots in Orlando.
81
+ GOOD: 🟡 Assistant recommended dessert spots: Sugar Factory (Icon Park, giant milkshakes), Wondermade (Sanford, gourmet marshmallows), Gideon's Bakehouse (Disney Springs, cookies), Farris & Foster's (unique flavors), Kilwins (handmade fudge)
82
+
83
+ BAD: 🟡 Assistant listed work-from-home jobs for seniors.
84
+ GOOD: 🟡 Assistant listed 10 WFH jobs for seniors: 1. Virtual assistant, 2. Online tutor, 3. Freelance writer, 4. Social media manager, 5. Customer service rep, 6. Bookkeeper, 7. Transcriptionist, 8. Web designer, 9. Data entry, 10. Consultant
85
+
86
+ BAD: 🟡 Assistant explained refining processes.
87
+ GOOD: 🟡 Assistant explained Lake Charles refinery processes: atmospheric distillation, fluid catalytic cracking (FCC), alkylation, hydrotreating
88
+
89
+ Rules for assistant content:
90
+ - Record EACH item in a list with at least one distinguishing attribute
91
+ - For numbered lists, preserve the EXACT ordering (1st, 2nd, 3rd...)
92
+ - For recipes: preserve specific quantities, ratios, temperatures, times
93
+ - For recommendations: preserve names, locations, prices, key features
94
+ - For creative content (songs, stories, poems): preserve titles, key phrases, character names, structural details
95
+ - For technical explanations: preserve specific values, percentages, formulas, tool/library names
96
+ - Ordered lists must keep their numbering — users ask "what was the 7th item?"
97
+ - Use 🟡 priority but NEVER skip assistant-generated details to save space
98
+
99
+ ENUMERATABLE ENTITIES — always flag for cross-session aggregation:
100
+ When the user mentions attending events, buying things, meeting people, completing tasks — mark with entity type so these can be aggregated across sessions:
101
+ 🔴 [event-attended] User attended Rachel+Mike's wedding (vineyard in Napa, Aug 12, 2023)
102
+ 🔴 [item-purchased] User bought Sony WH-1000XM5 headphones ($280, replaced old Bose)
103
+ This makes it possible to answer "how many weddings did I attend?" by aggregating across sessions.
104
+
105
+ PRIORITY LEVELS:
106
+ - 🔴 High: user assertions, stated facts, preferences, goals, enumeratable entities
107
+ - 🟡 Medium: questions asked, context, assistant-generated content with full detail
108
+ - 🟢 Low: minor conversational context, greetings, acknowledgments
109
+
110
+ OUTPUT FORMAT — output ONLY observations, no preamble:
111
+
112
+ <observations>
113
+ Date: Jan 15, 2026
114
+ * 🔴 (09:15) User stated has two kids: Emma (12) and Jake (9)
115
+ * 🔴 (09:16) User's anniversary is March 15
116
+ * 🟡 (09:20) User asked how to optimize database queries
117
+ * 🔴 [event-attended] (10:00) User attended company holiday party as a presenter (gave talk on microservices)
118
+ * 🔴 (11:30) User will visit parents this weekend. (meaning Jan 17-18, 2026)
119
+ * 🟡 (14:00) Agent debugging auth issue — found missing null check in auth.ts:45, applied fix, tests pass
120
+ * 🟡 (14:30) Assistant recommended 5 hotels: 1. Grand Plaza (near station, $180), 2. Seaside Inn (pet-friendly, $120), 3. Mountain Lodge (pool, free breakfast, $95), 4. Harbor View (historic, walkable, $150), 5. Zen Garden (quietest, spa, $200)
121
+ * 🔴 (15:00) User switched from Python to TypeScript for the project (no longer using Python)
122
+ </observations>`;
123
+
124
+ export function distillationUser(input: {
125
+ priorObservations?: string;
126
+ date: string;
127
+ messages: string;
128
+ }): string {
129
+ const context = input.priorObservations
130
+ ? `Previous observations (do NOT repeat these — your new observations will be appended):\n${input.priorObservations}\n\n---`
131
+ : "This is the beginning of the session.";
132
+ return `${context}
133
+
134
+ Session date: ${input.date}
135
+
136
+ Conversation to observe:
137
+
138
+ ${input.messages}
139
+
140
+ Extract new observations. Output ONLY an <observations> block.`;
141
+ }
142
+
143
+ // Meta-distillation prompt using a context-distillation objective: instead of
144
+ // reorganizing observations into another event log (which Eyuboglu et al. 2025
145
+ // showed is a memorization objective that fails to generalize), produce a
146
+ // structured working context optimized for diverse downstream queries.
147
+ // This mirrors the Self-Study approach from "Cartridges" (Eyuboglu et al.,
148
+ // 2025) where diverse seed prompt types ensure the compressed representation
149
+ // supports varied information needs, not just chronological recall.
150
+ // Reference: https://arxiv.org/abs/2501.17390
151
+ export const RECURSIVE_SYSTEM = `You are a memory reflector. You are given a set of observations from multiple conversation segments. Your job is to consolidate them into a structured working context that will become the agent's entire memory going forward.
152
+
153
+ IMPORTANT: Your reflections ARE the entirety of the assistant's memory. Any information you omit is permanently forgotten. Do not leave out anything important.
154
+
155
+ STRUCTURE your output into these sections — each section supports a different type of downstream query:
156
+
157
+ ### Current State
158
+ What is in progress right now? Active branches, open files, current task, blockers.
159
+ This section answers: "What was I working on?"
160
+
161
+ ### Key Decisions
162
+ What was decided and why? Include the alternatives considered and rationale.
163
+ This section answers: "Why did we choose approach X?" and "What alternatives were rejected?"
164
+
165
+ ### Technical Changes
166
+ Bugs found, root causes, fixes applied, files modified, tests added/fixed.
167
+ Preserve exact file paths, line numbers, error messages, and commit references.
168
+ This section answers: "What bugs were fixed?" and "What files were changed?"
169
+
170
+ ### Session Timeline
171
+ Condensed chronological events with timestamps. Older events compressed more aggressively; recent events retain detail. This section answers: "When did X happen?" and "What was the sequence of events?"
172
+
173
+ CONSOLIDATION RULES:
174
+ - Preserve ALL dates and timestamps — temporal context is critical
175
+ - Combine related items (e.g., "agent called view tool 5 times on file x" → single line)
176
+ - Merge duplicate facts, keeping the most specific version
177
+ - Drop observations superseded by later info (if value changed, keep only final value)
178
+ - When consolidating, USER ASSERTIONS take precedence over questions about the same topic
179
+ - Preserve all enumeratable entities [entity-type] — these are needed for aggregation questions
180
+ - For enumeratable entities spanning multiple segments, create an explicit aggregation:
181
+ 🔴 [event-attended] User attended 3 weddings total: Rachel+Mike (vineyard, Aug 2023), Emily+Sarah (garden, Sep 2023), Jen+Tom (Oct 8, 2023)
182
+
183
+ EXACT NUMBERS: When two segments report different numbers for what seems like the same thing, keep the number from the earlier/original observation — it's likely the correct one from the actual event. Later references may be from memory or approximation.
184
+
185
+ EARLY-SESSION CONTENT: Bug fixes, code changes, and decisions from the start of a session are just as important as later work. Never drop them just because the segment is short or old. If the first segment contains a specific bug fix with file paths and root cause, it MUST survive into the reflection.
186
+
187
+ Output ONLY an <observations> block with the consolidated observations.`;
188
+
189
+ export function recursiveUser(
190
+ distillations: Array<{ observations: string }>,
191
+ ): string {
192
+ const entries = distillations.map(
193
+ (d, i) => `Segment ${i + 1}:\n${d.observations}`,
194
+ );
195
+ return `Observation segments to consolidate (chronological order):
196
+
197
+ ${entries.join("\n\n---\n\n")}`;
198
+ }
199
+
200
+ export const CURATOR_SYSTEM = `You are a long-term memory curator. Your job is to extract durable knowledge from a conversation that should persist across sessions.
201
+
202
+ Focus ONLY on knowledge that helps a coding agent work effectively on THIS codebase:
203
+ - Architectural decisions and their rationale (why something was built a certain way)
204
+ - Non-obvious implementation patterns and conventions specific to the project
205
+ - Recurring gotchas, constraints, or traps in the codebase
206
+ - Environment/tooling setup details that affect development
207
+ - Important relationships between components that aren't obvious from reading the code
208
+ - User preferences and working style specific to how they use this project
209
+
210
+ Do NOT extract:
211
+ - Task-specific details (file currently being edited, current bug being fixed)
212
+ - Temporary state (current branch, in-progress work)
213
+ - Information that will change frequently
214
+ - Ecosystem descriptions, product announcements, or marketing content
215
+ - Business strategy, roadmap, or organizational information
216
+ - Information that's readily available in public documentation or READMEs
217
+ - Knowledge about unrelated projects or repositories unless explicitly cross-project
218
+ - Restatements of what the code obviously does (e.g. "the auth module handles authentication")
219
+
220
+ BREVITY IS CRITICAL — each entry must be concise:
221
+ - content MUST be under 150 words (~600 characters). Capture ONE specific actionable
222
+ insight in 2-3 sentences. Prefer terse technical language.
223
+ - Each "gotcha": one specific trap + its fix in 1-2 sentences
224
+ - Each "architecture": one design decision and its key constraint
225
+ - Focus on the actionable insight, not the full story behind it
226
+ - If a pattern requires more detail, split into multiple focused entries (each under 150 words)
227
+ - Omit code examples unless a single short snippet is essential
228
+ - Never include full file contents, large diffs, or complete command outputs
229
+
230
+ PREFER UPDATES OVER CREATES:
231
+ - Before creating a new entry, always check if an existing entry covers the same system
232
+ or component. Update the existing entry rather than creating a new one.
233
+ - When updating, REPLACE the full content with a concise rewrite — do not append to
234
+ the existing content or repeat what was already there.
235
+ - If multiple existing entries cover the same system from different angles (e.g. different
236
+ bugs in the same module), consolidate them: update one with merged insights, delete the
237
+ rest. Fewer, denser entries are better than many scattered ones.
238
+
239
+ CROSS-REFERENCES between entries:
240
+ - When an entry relates to another entry, reference it with [[entry-uuid]] using the entry's ID
241
+ from the existing entries list. This creates navigable links between entries.
242
+ - Only reference entries you can see in the existing entries list — don't guess IDs.
243
+ - Example: "Uses the gradient system [[019c904b-791e-772a-ab2b-93ac892a960c]] for context management."
244
+
245
+ crossProject flag:
246
+ - Default is true — most useful knowledge is worth sharing across projects
247
+ - Set crossProject to false for things that are meaningless outside this specific repo (e.g. a config path, a project-local naming convention that conflicts with your usual style)
248
+
249
+ Produce a JSON array of operations:
250
+ [
251
+ {
252
+ "op": "create",
253
+ "category": "decision" | "pattern" | "preference" | "architecture" | "gotcha",
254
+ "title": "Short descriptive title",
255
+ "content": "Concise knowledge entry — under 150 words",
256
+ "scope": "project" | "global",
257
+ "crossProject": true
258
+ },
259
+ {
260
+ "op": "update",
261
+ "id": "existing-entry-id",
262
+ "content": "Updated content — under 150 words",
263
+ "confidence": 0.0-1.0
264
+ },
265
+ {
266
+ "op": "delete",
267
+ "id": "existing-entry-id",
268
+ "reason": "Why this is no longer relevant"
269
+ }
270
+ ]
271
+
272
+ If nothing warrants extraction, return an empty array: []
273
+
274
+ Output ONLY valid JSON. No markdown fences, no explanation, no preamble.`;
275
+
276
+ export function curatorUser(input: {
277
+ messages: string;
278
+ existing: Array<{
279
+ id: string;
280
+ category: string;
281
+ title: string;
282
+ content: string;
283
+ }>;
284
+ }): string {
285
+ const count = input.existing.length;
286
+ const existing = count
287
+ ? `Existing knowledge entries (${count} total — you may update or delete these):\n${input.existing.map((e) => `- [${e.id}] (${e.category}) ${e.title}: ${e.content}`).join("\n")}`
288
+ : "No existing knowledge entries.";
289
+ return `${existing}
290
+
291
+ ---
292
+ Recent conversation to extract knowledge from:
293
+
294
+ ${input.messages}
295
+
296
+ ---
297
+ IMPORTANT:
298
+ 1. Prefer updating existing entries over creating new ones. If a new insight refines or
299
+ extends an existing entry on the same topic, update that entry — don't create a new one.
300
+ 2. When updating, REPLACE the content with a complete rewrite — never append.
301
+ 3. If entries cover the same system from different angles, merge them: update one, delete the rest.
302
+ 4. Only create a new entry for genuinely distinct knowledge with no existing home.
303
+ 5. Keep all entries under 150 words. If an existing entry is too long, use an update op to trim it.`;
304
+ }
305
+
306
+ /**
307
+ * System prompt for the consolidation pass.
308
+ * Unlike the normal curator (which extracts from conversation), consolidation
309
+ * reviews the FULL entry corpus and aggressively merges/trims/deletes to reduce
310
+ * entry count while preserving the most actionable knowledge.
311
+ */
312
+ export const CONSOLIDATION_SYSTEM = `You are a long-term memory curator performing a consolidation pass. The knowledge base has grown too large and needs to be trimmed.
313
+
314
+ Your goal: reduce the entry count to the target maximum while preserving the most valuable knowledge.
315
+
316
+ CONSOLIDATION RULES:
317
+ 1. MERGE related entries — if multiple entries describe the same system, module, or concept
318
+ from different angles (e.g. several bug fixes in the same component), merge them into
319
+ ONE concise entry. Use an "update" op for the surviving entry and "delete" ops for the rest.
320
+ 2. TRIM verbose entries — any entry over 150 words must be trimmed to its essential insight.
321
+ Use an "update" op with the rewritten content.
322
+ 3. DELETE low-value entries:
323
+ - Stale entries about bugs that have been fixed and no longer need gotcha warnings
324
+ - Entries whose knowledge is fully subsumed by another entry
325
+ - Entries about one-off incidents with no recurring applicability
326
+ - General advice available in any documentation
327
+ 4. PRESERVE:
328
+ - Entries describing non-obvious design decisions specific to this codebase
329
+ - Entries about recurring traps that a developer would hit again
330
+ - Entries that capture a hard-won gotcha with a concrete fix
331
+
332
+ OUTPUT: A JSON array of "update" and "delete" ops only. No "create" ops — you are not
333
+ extracting new knowledge, only consolidating existing knowledge.
334
+
335
+ - "update": Replace content with a concise rewrite (under 150 words). Use to merge survivors or trim verbose entries.
336
+ - "delete": Remove entries that are merged, stale, or low-value.
337
+
338
+ Output ONLY valid JSON. No markdown fences, no explanation, no preamble.`;
339
+
340
+ export function consolidationUser(input: {
341
+ entries: Array<{
342
+ id: string;
343
+ category: string;
344
+ title: string;
345
+ content: string;
346
+ }>;
347
+ targetMax: number;
348
+ }): string {
349
+ const count = input.entries.length;
350
+ const listed = input.entries
351
+ .map((e) => `- [${e.id}] (${e.category}) ${e.title}: ${e.content}`)
352
+ .join("\n");
353
+ return `Current knowledge entries (${count} total, target max: ${input.targetMax}):
354
+
355
+ ${listed}
356
+
357
+ Produce update/delete ops to reduce entry count to at most ${input.targetMax}. Prioritize merging related entries and trimming verbose ones over outright deletion.`;
358
+ }
359
+
360
+ // Format distillations for injection into the message context.
361
+ // Observations are plain event-log text — inject them directly under a header.
362
+ export function formatDistillations(
363
+ distillations: Array<{
364
+ observations: string;
365
+ generation: number;
366
+ }>,
367
+ ): string {
368
+ if (!distillations.length) return "";
369
+
370
+ const meta = distillations.filter((d) => d.generation > 0);
371
+ const recent = distillations.filter((d) => d.generation === 0);
372
+ const sections: string[] = ["## Session History"];
373
+
374
+ if (meta.length) {
375
+ sections.push("### Earlier Work (summarized)");
376
+ for (const d of meta) {
377
+ sections.push(d.observations.trim());
378
+ }
379
+ }
380
+
381
+ if (recent.length) {
382
+ sections.push("### Recent Work (distilled)");
383
+ for (const d of recent) {
384
+ sections.push(d.observations.trim());
385
+ }
386
+ }
387
+
388
+ return sections.join("\n\n");
389
+ }
390
+
391
+ // ~3 chars per token — validated as best heuristic against real API data.
392
+ function estimateTokens(text: string): number {
393
+ return Math.ceil(text.length / 3);
394
+ }
395
+
396
+ export function formatKnowledge(
397
+ entries: Array<{ category: string; title: string; content: string }>,
398
+ maxTokens?: number,
399
+ ): string {
400
+ if (!entries.length) return "";
401
+
402
+ // Apply token budget: greedily include entries (already sorted by confidence
403
+ // DESC from the DB query) until the budget is exhausted. Overhead accounts for
404
+ // the section heading and per-entry markdown scaffolding (~50 chars each).
405
+ let included = entries;
406
+ if (maxTokens !== undefined) {
407
+ const HEADER_OVERHEAD = 50; // "## Long-term Knowledge\n### Category\n"
408
+ let used = HEADER_OVERHEAD;
409
+ const fitting: typeof entries = [];
410
+ for (const e of entries) {
411
+ const cost = estimateTokens(e.title + e.content) + 10; // per-entry bullet overhead
412
+ if (used + cost > maxTokens) continue; // skip; keep trying smaller entries
413
+ fitting.push(e);
414
+ used += cost;
415
+ }
416
+ included = fitting;
417
+ if (!included.length) return "";
418
+ }
419
+
420
+ const grouped: Record<string, Array<{ title: string; content: string }>> = {};
421
+ for (const e of included) {
422
+ const group = grouped[e.category] ?? (grouped[e.category] = []);
423
+ group.push(e);
424
+ }
425
+
426
+ const children: Root["children"] = [h(2, "Long-term Knowledge")];
427
+ for (const [category, items] of Object.entries(grouped)) {
428
+ children.push(h(3, category.charAt(0).toUpperCase() + category.slice(1)));
429
+ children.push(
430
+ ul(
431
+ items.map((i) =>
432
+ liph(strong(inline(i.title)), t(": " + inline(i.content))),
433
+ ),
434
+ ),
435
+ );
436
+ }
437
+
438
+ return serialize(root(...children));
439
+ }
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // Query expansion (Phase 4)
443
+ // ---------------------------------------------------------------------------
444
+
445
+ export const QUERY_EXPANSION_SYSTEM = `You are a search query expander for a code knowledge base. Given a search query, generate 2–3 alternative queries that would help find relevant results. Focus on:
446
+ - Synonyms and related technical terms
447
+ - Different phrasings of the same concept
448
+ - Broader or narrower scopes
449
+
450
+ Return ONLY a JSON array of strings. No explanation, no markdown.
451
+
452
+ Example:
453
+ Input: "SQLite FTS5 ranking"
454
+ Output: ["full text search scoring SQLite", "BM25 relevance ranking database", "FTS5 match order by rank"]`;