@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.
- package/LICENSE +21 -0
- package/README.md +26 -5
- package/dist/bun/agents-file.d.ts +59 -0
- package/dist/bun/agents-file.d.ts.map +1 -0
- package/dist/bun/config.d.ts +58 -0
- package/dist/bun/config.d.ts.map +1 -0
- package/dist/bun/curator.d.ts +35 -0
- package/dist/bun/curator.d.ts.map +1 -0
- package/dist/bun/db/driver.bun.d.ts +5 -0
- package/dist/bun/db/driver.bun.d.ts.map +1 -0
- package/dist/bun/db/driver.node.d.ts +15 -0
- package/dist/bun/db/driver.node.d.ts.map +1 -0
- package/dist/bun/db.d.ts +22 -0
- package/dist/bun/db.d.ts.map +1 -0
- package/dist/bun/distillation.d.ts +32 -0
- package/dist/bun/distillation.d.ts.map +1 -0
- package/dist/bun/embedding.d.ts +90 -0
- package/dist/bun/embedding.d.ts.map +1 -0
- package/dist/bun/gradient.d.ts +73 -0
- package/dist/bun/gradient.d.ts.map +1 -0
- package/dist/bun/index.d.ts +19 -0
- package/dist/bun/index.d.ts.map +1 -0
- package/dist/bun/index.js +28236 -0
- package/dist/bun/index.js.map +7 -0
- package/dist/bun/lat-reader.d.ts +69 -0
- package/dist/bun/lat-reader.d.ts.map +1 -0
- package/dist/bun/log.d.ts +17 -0
- package/dist/bun/log.d.ts.map +1 -0
- package/dist/bun/ltm.d.ts +138 -0
- package/dist/bun/ltm.d.ts.map +1 -0
- package/dist/bun/markdown.d.ts +37 -0
- package/dist/bun/markdown.d.ts.map +1 -0
- package/dist/bun/prompt.d.ts +47 -0
- package/dist/bun/prompt.d.ts.map +1 -0
- package/dist/bun/recall.d.ts +41 -0
- package/dist/bun/recall.d.ts.map +1 -0
- package/dist/bun/search.d.ts +113 -0
- package/dist/bun/search.d.ts.map +1 -0
- package/dist/bun/temporal.d.ts +66 -0
- package/dist/bun/temporal.d.ts.map +1 -0
- package/dist/bun/types.d.ts +180 -0
- package/dist/bun/types.d.ts.map +1 -0
- package/dist/bun/worker.d.ts +6 -0
- package/dist/bun/worker.d.ts.map +1 -0
- package/dist/node/agents-file.d.ts +59 -0
- package/dist/node/agents-file.d.ts.map +1 -0
- package/dist/node/config.d.ts +58 -0
- package/dist/node/config.d.ts.map +1 -0
- package/dist/node/curator.d.ts +35 -0
- package/dist/node/curator.d.ts.map +1 -0
- package/dist/node/db/driver.bun.d.ts +5 -0
- package/dist/node/db/driver.bun.d.ts.map +1 -0
- package/dist/node/db/driver.node.d.ts +15 -0
- package/dist/node/db/driver.node.d.ts.map +1 -0
- package/dist/node/db.d.ts +22 -0
- package/dist/node/db.d.ts.map +1 -0
- package/dist/node/distillation.d.ts +32 -0
- package/dist/node/distillation.d.ts.map +1 -0
- package/dist/node/embedding.d.ts +90 -0
- package/dist/node/embedding.d.ts.map +1 -0
- package/dist/node/gradient.d.ts +73 -0
- package/dist/node/gradient.d.ts.map +1 -0
- package/dist/node/index.d.ts +19 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +28253 -0
- package/dist/node/index.js.map +7 -0
- package/dist/node/lat-reader.d.ts +69 -0
- package/dist/node/lat-reader.d.ts.map +1 -0
- package/dist/node/log.d.ts +17 -0
- package/dist/node/log.d.ts.map +1 -0
- package/dist/node/ltm.d.ts +138 -0
- package/dist/node/ltm.d.ts.map +1 -0
- package/dist/node/markdown.d.ts +37 -0
- package/dist/node/markdown.d.ts.map +1 -0
- package/dist/node/prompt.d.ts +47 -0
- package/dist/node/prompt.d.ts.map +1 -0
- package/dist/node/recall.d.ts +41 -0
- package/dist/node/recall.d.ts.map +1 -0
- package/dist/node/search.d.ts +113 -0
- package/dist/node/search.d.ts.map +1 -0
- package/dist/node/temporal.d.ts +66 -0
- package/dist/node/temporal.d.ts.map +1 -0
- package/dist/node/types.d.ts +180 -0
- package/dist/node/types.d.ts.map +1 -0
- package/dist/node/worker.d.ts +6 -0
- package/dist/node/worker.d.ts.map +1 -0
- package/dist/types/agents-file.d.ts +59 -0
- package/dist/types/agents-file.d.ts.map +1 -0
- package/dist/types/config.d.ts +58 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/curator.d.ts +35 -0
- package/dist/types/curator.d.ts.map +1 -0
- package/dist/types/db/driver.bun.d.ts +5 -0
- package/dist/types/db/driver.bun.d.ts.map +1 -0
- package/dist/types/db/driver.node.d.ts +15 -0
- package/dist/types/db/driver.node.d.ts.map +1 -0
- package/dist/types/db.d.ts +22 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/distillation.d.ts +32 -0
- package/dist/types/distillation.d.ts.map +1 -0
- package/dist/types/embedding.d.ts +90 -0
- package/dist/types/embedding.d.ts.map +1 -0
- package/dist/types/gradient.d.ts +73 -0
- package/dist/types/gradient.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/lat-reader.d.ts +69 -0
- package/dist/types/lat-reader.d.ts.map +1 -0
- package/dist/types/log.d.ts +17 -0
- package/dist/types/log.d.ts.map +1 -0
- package/dist/types/ltm.d.ts +138 -0
- package/dist/types/ltm.d.ts.map +1 -0
- package/dist/types/markdown.d.ts +37 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/prompt.d.ts +47 -0
- package/dist/types/prompt.d.ts.map +1 -0
- package/dist/types/recall.d.ts +41 -0
- package/dist/types/recall.d.ts.map +1 -0
- package/dist/types/search.d.ts +113 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/temporal.d.ts +66 -0
- package/dist/types/temporal.d.ts.map +1 -0
- package/dist/types/types.d.ts +180 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/worker.d.ts +6 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/package.json +48 -5
- package/src/agents-file.ts +406 -0
- package/src/config.ts +132 -0
- package/src/curator.ts +220 -0
- package/src/db/driver.bun.ts +18 -0
- package/src/db/driver.node.ts +54 -0
- package/src/db.ts +433 -0
- package/src/distillation.ts +433 -0
- package/src/embedding.ts +528 -0
- package/src/gradient.ts +1387 -0
- package/src/index.ts +109 -0
- package/src/lat-reader.ts +374 -0
- package/src/log.ts +27 -0
- package/src/ltm.ts +861 -0
- package/src/markdown.ts +129 -0
- package/src/prompt.ts +454 -0
- package/src/recall.ts +446 -0
- package/src/search.ts +330 -0
- package/src/temporal.ts +379 -0
- package/src/types.ts +199 -0
- package/src/worker.ts +26 -0
package/src/markdown.ts
ADDED
|
@@ -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"]`;
|