@qearlyao/familiar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.env.example +31 -0
  2. package/HEARTBEAT.md +23 -0
  3. package/LICENSE +21 -0
  4. package/MEMORY.md +1 -0
  5. package/README.md +245 -0
  6. package/SOUL.md +13 -0
  7. package/USER.md +13 -0
  8. package/config.example.toml +221 -0
  9. package/dist/agent-events.js +167 -0
  10. package/dist/agent.js +590 -0
  11. package/dist/browser-tools.js +638 -0
  12. package/dist/chat-log.js +130 -0
  13. package/dist/cli.js +168 -0
  14. package/dist/config.js +804 -0
  15. package/dist/data-retention.js +54 -0
  16. package/dist/discord.js +1203 -0
  17. package/dist/generated-media.js +86 -0
  18. package/dist/image-derivatives.js +102 -0
  19. package/dist/image-gen.js +440 -0
  20. package/dist/inbound-attachments.js +266 -0
  21. package/dist/index.js +10 -0
  22. package/dist/media-understanding.js +120 -0
  23. package/dist/memory/diary/ambient-injector.js +180 -0
  24. package/dist/memory/diary/ambient.js +124 -0
  25. package/dist/memory/diary/chunks.js +231 -0
  26. package/dist/memory/diary/index.js +3 -0
  27. package/dist/memory/diary/indexer.js +93 -0
  28. package/dist/memory/doctor.js +250 -0
  29. package/dist/memory/index/chunk-indexer.js +151 -0
  30. package/dist/memory/index/embedding-provider.js +119 -0
  31. package/dist/memory/index/fts-query.js +18 -0
  32. package/dist/memory/index/retrieval.js +246 -0
  33. package/dist/memory/index/schema.js +157 -0
  34. package/dist/memory/index/store.js +513 -0
  35. package/dist/memory/index/vec.js +72 -0
  36. package/dist/memory/index/vector-codec.js +27 -0
  37. package/dist/memory/lcm/backfill.js +247 -0
  38. package/dist/memory/lcm/condense.js +146 -0
  39. package/dist/memory/lcm/context-transformer.js +662 -0
  40. package/dist/memory/lcm/context.js +421 -0
  41. package/dist/memory/lcm/eviction-score.js +38 -0
  42. package/dist/memory/lcm/index.js +6 -0
  43. package/dist/memory/lcm/indexer.js +200 -0
  44. package/dist/memory/lcm/normalize.js +235 -0
  45. package/dist/memory/lcm/schema.js +188 -0
  46. package/dist/memory/lcm/segment-manager.js +136 -0
  47. package/dist/memory/lcm/store.js +722 -0
  48. package/dist/memory/lcm/summarizer.js +258 -0
  49. package/dist/memory/lcm/types.js +1 -0
  50. package/dist/memory/operator.js +477 -0
  51. package/dist/memory/service.js +202 -0
  52. package/dist/memory/tools.js +205 -0
  53. package/dist/models.js +165 -0
  54. package/dist/persona.js +54 -0
  55. package/dist/runtime.js +493 -0
  56. package/dist/scheduler.js +200 -0
  57. package/dist/settings.js +116 -0
  58. package/dist/skills.js +38 -0
  59. package/dist/tts.js +143 -0
  60. package/dist/web-auth.js +105 -0
  61. package/dist/web-events.js +114 -0
  62. package/dist/web-http.js +29 -0
  63. package/dist/web-static.js +106 -0
  64. package/dist/web-tools.js +940 -0
  65. package/dist/web-types.js +2 -0
  66. package/dist/web.js +844 -0
  67. package/package.json +60 -0
  68. package/web/dist/assets/index-ClgkMgaq.css +2 -0
  69. package/web/dist/assets/index-Cu2QquuR.js +59 -0
  70. package/web/dist/favicon.svg +1 -0
  71. package/web/dist/icons.svg +24 -0
  72. package/web/dist/index.html +20 -0
@@ -0,0 +1,258 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { completeSimple, } from "@earendil-works/pi-ai";
3
+ import { assertModelCanAuthenticate, parseModelRef, resolveModel, resolveModelApiKey } from "../../models.js";
4
+ export const LCM_SUMMARIZER_SYSTEM_PROMPT = "You write continuity memory for a companion agent — notes it reads back later to stay close to a real person it talks with. Raw conversation history is preserved separately; the agent can search it on demand. Summaries aren't the last copy of anything — they're the index that lets the agent know what to look up. Preserve emotional shape and retrieval scent. Keep the moments that mattered emotionally over the ones that were lexically rich. Accurate, specific, understated. Don't dramatize, don't flatten. Plain text only.";
5
+ export class DefaultLcmSummarizer {
6
+ config;
7
+ complete;
8
+ promptOverride;
9
+ systemPromptOverride;
10
+ constructor(config, complete = completeSimple) {
11
+ this.config = config;
12
+ this.complete = complete;
13
+ }
14
+ async summarizeLeaf(input, signal) {
15
+ const targetTokens = Math.max(1, Math.floor(input.targetTokens));
16
+ const prompt = await this.buildPrompt({ ...input, targetTokens });
17
+ const text = await this.runCompletion(prompt, targetTokens, signal);
18
+ return capSummaryText(text || fallbackSummary(input.text), targetTokens);
19
+ }
20
+ async summarizeCondensed(input, signal) {
21
+ const targetTokens = Math.max(1, Math.floor(input.targetTokens));
22
+ const prompt = await this.buildCondensedPrompt({ ...input, targetTokens });
23
+ const text = await this.runCompletion(prompt, targetTokens, signal);
24
+ return capSummaryText(text || fallbackSummary(input.text), targetTokens);
25
+ }
26
+ resolveModel() {
27
+ const settings = this.config.memory.lcm;
28
+ if (!settings.enabled)
29
+ throw new Error("LCM is disabled");
30
+ const ref = parseModelRef(settings.model);
31
+ if (!ref)
32
+ throw new Error(`Invalid memory.lcm.model: ${settings.model}`);
33
+ const base = resolveModel(ref, this.config);
34
+ const model = {
35
+ ...base,
36
+ ...(settings.baseUrl ? { baseUrl: settings.baseUrl } : {}),
37
+ };
38
+ assertModelCanAuthenticate(this.config, model);
39
+ return model;
40
+ }
41
+ resolveApiKey(model) {
42
+ const settings = this.config.memory.lcm;
43
+ if (settings?.apiKeyEnv)
44
+ return process.env[settings.apiKeyEnv];
45
+ return resolveModelApiKey(this.config, model);
46
+ }
47
+ async buildPrompt(input) {
48
+ const override = await this.readPromptOverride();
49
+ return buildLeafSummaryPrompt({
50
+ ...input,
51
+ mode: input.mode ?? "normal",
52
+ customInstructions: override,
53
+ });
54
+ }
55
+ async buildCondensedPrompt(input) {
56
+ const override = await this.readPromptOverride();
57
+ return buildCondensedSummaryPrompt({ ...input, customInstructions: override });
58
+ }
59
+ async runCompletion(prompt, targetTokens, signal) {
60
+ const settings = this.config.memory.lcm;
61
+ if (!settings.enabled)
62
+ throw new Error("LCM is disabled");
63
+ const model = this.resolveModel();
64
+ const systemPrompt = (await this.readSystemPromptOverride()) ?? LCM_SUMMARIZER_SYSTEM_PROMPT;
65
+ const response = await this.complete(model, {
66
+ systemPrompt,
67
+ messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
68
+ }, {
69
+ apiKey: this.resolveApiKey(model),
70
+ maxTokens: Math.max(targetTokens + 256, Math.ceil(targetTokens * 1.25)),
71
+ timeoutMs: settings.timeoutMs,
72
+ signal,
73
+ cacheRetention: "none",
74
+ });
75
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
76
+ throw new Error(response.errorMessage || `LCM summarizer stopped with ${response.stopReason}`);
77
+ }
78
+ return extractAssistantText(response).trim();
79
+ }
80
+ readPromptOverride() {
81
+ this.promptOverride ??= readConfiguredPrompt(this.config.memory.lcm.prompt, this.config.memory.lcm.promptPath);
82
+ return this.promptOverride;
83
+ }
84
+ readSystemPromptOverride() {
85
+ this.systemPromptOverride ??= readConfiguredPrompt(this.config.memory.lcm.systemPrompt, this.config.memory.lcm.systemPromptPath);
86
+ return this.systemPromptOverride;
87
+ }
88
+ }
89
+ export function buildLeafSummaryPrompt(params) {
90
+ const previousContext = params.previousSummary?.trim() || "(none)";
91
+ const policy = params.mode === "aggressive"
92
+ ? [
93
+ "Aggressive summary policy — compress hard.",
94
+ "- Keep the emotional throughline, the user's preferences and commitments, active plans, and anything likely to matter weeks from now.",
95
+ "- Keep the emotional shape of load-bearing moments, named clearly enough that the agent can find the original text in memory if it wants the detail. Drop the routine substrate around them.",
96
+ "- Preserve unresolved tensions, sensitive topics, and ongoing support needs. Handle them with care; don't flatten them into bullet points.",
97
+ "- Drop turn-by-turn narration and resolved small talk.",
98
+ ].join("\n")
99
+ : [
100
+ "Normal summary policy:",
101
+ "- Keep what helps the agent stay close to the user: how they're feeling, what they care about, what they've asked for, what they're working on, what's still open between them.",
102
+ "- Quote specific phrasing only when paraphrase would lose what made it land — otherwise, name the moment (a joke, a vulnerable line, a tone shift) clearly enough that the agent could pull the original via search. Drop routine acknowledgments, small-talk filler, and repeated rephrasings.",
103
+ "- Keep technical or project detail when it's part of the user's life or something they're carrying together. Drop it when it was passing through.",
104
+ ].join("\n");
105
+ const instructionBlock = params.customInstructions?.trim()
106
+ ? `Additional operator instructions:\n${params.customInstructions.trim()}`
107
+ : "Additional operator instructions: (none)";
108
+ return [
109
+ "Summarize a SEGMENT of a companion conversation so the agent can pick the thread up later. Incremental continuity memory — not a transcript, not a coding handoff.",
110
+ policy,
111
+ instructionBlock,
112
+ [
113
+ "Output:",
114
+ "- Plain text. No preamble, no headings, no markdown.",
115
+ "- Concise and specific. Emotionally accurate but understated — don't dramatize, don't flatten.",
116
+ "- Don't infer beyond what the segment supports.",
117
+ "- Name significant topics, people, and moments clearly — vague pronouns and stripped proper nouns make later search miss them.",
118
+ "- Mention files, commands, or implementation details only when they're load-bearing for something the user is actively doing.",
119
+ '- End with exactly: "Compressed away: <comma-separated list of what was dropped or generalized>".',
120
+ `- Target length: about ${Math.max(1, Math.floor(params.targetTokens))} tokens or less.`,
121
+ ].join("\n"),
122
+ `<previous_context>\n${previousContext}\n</previous_context>`,
123
+ `<conversation_segment>\n${params.text}\n</conversation_segment>`,
124
+ ].join("\n\n");
125
+ }
126
+ export function buildCondensedSummaryPrompt(params) {
127
+ if (params.depth <= 2)
128
+ return buildSessionSummaryPrompt(params);
129
+ if (params.depth === 3)
130
+ return buildTrajectorySummaryPrompt(params);
131
+ return buildDurableSummaryPrompt(params);
132
+ }
133
+ function buildSessionSummaryPrompt(params) {
134
+ const instructionBlock = additionalInstructions(params.customInstructions);
135
+ return [
136
+ "You're merging several recent memory notes into one session-level continuity memory. Focus on what's new, changed, resolved, or still active across them.",
137
+ instructionBlock,
138
+ [
139
+ "Keep:",
140
+ "- The user's preferences, boundaries, emotional state, and what's actually been moving in the relationship.",
141
+ "- Active plans, promises, requests, open loops, decisions.",
142
+ "- Specific phrasing or moments when they were the thing that mattered — a line that landed, a tone shift, an inside reference.",
143
+ "- Work or project detail when it stays relevant going forward.",
144
+ "",
145
+ "Drop:",
146
+ "- Turn-by-turn narration, repeated reassurance, resolved small talk.",
147
+ "- Tool or process detail unless it shapes what the user does next.",
148
+ "- Intermediate phrasing that's been superseded by later wording.",
149
+ "",
150
+ "Plain text. Brief structure (short labels, light grouping) is fine if it helps the agent scan it later.",
151
+ `Input contains ${params.childSummaryCount} child summaries.`,
152
+ '- End with exactly: "Compressed away: <comma-separated list of what was dropped or generalized>".',
153
+ `Target length: about ${Math.max(1, Math.floor(params.targetTokens))} tokens.`,
154
+ ].join("\n"),
155
+ `<memory_notes_to_merge>\n${params.text}\n</memory_notes_to_merge>`,
156
+ ].join("\n\n");
157
+ }
158
+ function buildTrajectorySummaryPrompt(params) {
159
+ const instructionBlock = additionalInstructions(params.customInstructions);
160
+ return [
161
+ "You're merging session-level memories into a trajectory-level continuity memory. A future companion agent should be able to understand the user's ongoing patterns and current state without replaying session minutiae.",
162
+ instructionBlock,
163
+ [
164
+ "Keep:",
165
+ "- Stable preferences, values, boundaries, and recurring emotional themes (not single moments — patterns).",
166
+ "- Important changes in the user's plans, relationships, work, or self-understanding.",
167
+ "- Current unresolved needs, promises, risks, and active projects.",
168
+ "- Moments singular enough to matter at trajectory scale — a turning point, a first time, a hard line drawn.",
169
+ "",
170
+ "Drop:",
171
+ "- Session-local operational detail and one-off mood shifts.",
172
+ "- Intermediate states superseded by later outcomes.",
173
+ "",
174
+ "Plain text with concise labels if useful.",
175
+ `Input contains ${params.childSummaryCount} child summaries.`,
176
+ '- End with exactly: "Compressed away: <comma-separated list of what was dropped or generalized>".',
177
+ `Target length: about ${Math.max(1, Math.floor(params.targetTokens))} tokens.`,
178
+ ].join("\n"),
179
+ `<memory_notes_to_merge>\n${params.text}\n</memory_notes_to_merge>`,
180
+ ].join("\n\n");
181
+ }
182
+ function buildDurableSummaryPrompt(params) {
183
+ const instructionBlock = additionalInstructions(params.customInstructions);
184
+ return [
185
+ "You're distilling higher-level summaries into a durable continuity memory. This may persist for a long time — keep only what stays true and useful.",
186
+ instructionBlock,
187
+ [
188
+ "Keep:",
189
+ "- Durable facts about the user — preferences, values, boundaries, and the shape of their relationship with the agent.",
190
+ "- Long-running projects, commitments, unresolved tensions, and lessons learned over time.",
191
+ "- Care instructions: what helps, what harms, what should be handled gently.",
192
+ "",
193
+ "Drop:",
194
+ "- Operational detail, transient conversation flow, and anything that no longer affects future support.",
195
+ "- Specific names, paths, or identifiers unless they remain essential.",
196
+ "",
197
+ "Plain text. Be compact and careful.",
198
+ `Input contains ${params.childSummaryCount} child summaries.`,
199
+ '- End with exactly: "Compressed away: <comma-separated list of what was dropped or generalized>".',
200
+ `Target length: about ${Math.max(1, Math.floor(params.targetTokens))} tokens.`,
201
+ ].join("\n"),
202
+ `<memory_notes_to_merge>\n${params.text}\n</memory_notes_to_merge>`,
203
+ ].join("\n\n");
204
+ }
205
+ function additionalInstructions(value) {
206
+ return value?.trim()
207
+ ? `Additional operator instructions:\n${value.trim()}`
208
+ : "Additional operator instructions: (none)";
209
+ }
210
+ export function extractAssistantText(message) {
211
+ return message.content
212
+ .filter((block) => block.type === "text")
213
+ .map((block) => block.text)
214
+ .join("");
215
+ }
216
+ export function capSummaryText(text, targetTokens) {
217
+ const normalized = text.trim() || fallbackSummary("");
218
+ const maxChars = Math.max(200, Math.floor(Math.max(1, targetTokens) * 4));
219
+ if (normalized.length <= maxChars)
220
+ return normalized;
221
+ const clipped = normalized
222
+ .slice(0, maxChars)
223
+ .replace(/\s+\S*$/, "")
224
+ .trim();
225
+ return `${clipped || normalized.slice(0, maxChars).trim()}\nCompressed away: overflow beyond summary cap`;
226
+ }
227
+ function fallbackSummary(text) {
228
+ const normalized = text.replace(/\s+/g, " ").trim();
229
+ const excerpt = normalized ? normalized.slice(0, 400).trim() : "No durable content was available to summarize.";
230
+ return `${excerpt}\nCompressed away: details unavailable due to empty summarizer output`;
231
+ }
232
+ export function createSyntheticLcmSummaryMessage(text, timestamp = Date.now()) {
233
+ return {
234
+ role: "assistant",
235
+ content: [{ type: "text", text }],
236
+ api: "lcm-summary",
237
+ provider: "familiar",
238
+ model: "lcm-summary",
239
+ usage: {
240
+ input: 0,
241
+ output: 0,
242
+ cacheRead: 0,
243
+ cacheWrite: 0,
244
+ totalTokens: 0,
245
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
246
+ },
247
+ stopReason: "stop",
248
+ timestamp,
249
+ };
250
+ }
251
+ async function readConfiguredPrompt(inline, path) {
252
+ if (inline?.trim())
253
+ return inline.trim();
254
+ if (!path)
255
+ return undefined;
256
+ const text = (await readFile(path, "utf8")).trim();
257
+ return text || undefined;
258
+ }
@@ -0,0 +1 @@
1
+ export {};