@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,570 @@
1
+ export { agentScopeId, ORG_SCOPE_PREFIX, scopeId } from './memory-scope'
2
+
3
+ interface MemoryFactInput {
4
+ content: string
5
+ confidence: number
6
+ durability?: string
7
+ type?: string
8
+ }
9
+
10
+ interface PostProcessMemoryFactsOptions {
11
+ maxFacts?: number
12
+ minChars?: number
13
+ maxChars?: number
14
+ }
15
+
16
+ interface MemoryImportanceParams {
17
+ text: string
18
+ memoryType: string
19
+ explicitImportance?: number
20
+ confidenceByKey: Map<string, number>
21
+ durabilityByKey: Map<string, string>
22
+ categoryByKey: Map<string, string | undefined>
23
+ defaultImportance?: number
24
+ }
25
+
26
+ interface MemoryFactMaps {
27
+ confidenceByKey: Map<string, number>
28
+ durabilityByKey: Map<string, string>
29
+ categoryByKey: Map<string, string | undefined>
30
+ }
31
+
32
+ interface MemoryUpdateRelationLike<TRelation extends string = string> {
33
+ memoryId: string
34
+ relation: TRelation
35
+ }
36
+
37
+ interface MemoryUpdateItemLike<TRelation extends string = string> {
38
+ id: string
39
+ text: string
40
+ event: 'ADD' | 'UPDATE' | 'DELETE' | 'NONE'
41
+ oldMemory?: string
42
+ relatesTo?: MemoryUpdateRelationLike<TRelation>[]
43
+ }
44
+
45
+ interface MemoryUpdateOutputLike<TRelation extends string = string> {
46
+ memory: MemoryUpdateItemLike<TRelation>[]
47
+ }
48
+
49
+ interface MemoryDeltaItemLike {
50
+ fact: string
51
+ classification: 'new' | 'supersedes' | 'contradicts' | 'enriches' | 'duplicate'
52
+ targetMemoryIds?: string[]
53
+ invalidateTargetIds?: string[]
54
+ relations?: MemoryDeltaRelationLike[]
55
+ rationale?: string
56
+ }
57
+
58
+ interface MemoryDeltaOutputLike {
59
+ deltas: MemoryDeltaItemLike[]
60
+ }
61
+
62
+ interface MemoryDeltaRelationLike<TRelation extends string = string> {
63
+ relation: TRelation
64
+ targetMemoryId?: string
65
+ targetFactIndex?: number
66
+ }
67
+
68
+ interface MemoryActionAdd {
69
+ type: 'add'
70
+ refId: string
71
+ text: string
72
+ normalizedText: string
73
+ importance: number
74
+ durability: string
75
+ category: string
76
+ }
77
+
78
+ interface MemoryActionUpdate {
79
+ type: 'update'
80
+ refId: string
81
+ text: string
82
+ normalizedText: string
83
+ importance: number
84
+ durability?: string
85
+ category?: string
86
+ }
87
+
88
+ interface MemoryActionDelete {
89
+ type: 'delete'
90
+ refId: string
91
+ }
92
+
93
+ interface MemoryActionRelation<TRelation extends string = string> {
94
+ fromRefId: string
95
+ toRefId: string
96
+ relation: TRelation
97
+ }
98
+
99
+ interface MemoryActionPlan<TRelation extends string = string> {
100
+ actions: Array<MemoryActionAdd | MemoryActionUpdate | MemoryActionDelete>
101
+ relations: MemoryActionRelation<TRelation>[]
102
+ }
103
+
104
+ function clampImportance(value: number): number {
105
+ if (!Number.isFinite(value)) return 0
106
+ return Math.max(0, Math.min(1, value))
107
+ }
108
+
109
+ function normalizeMemoryKey(text: string): string {
110
+ return text
111
+ .toLowerCase()
112
+ .replace(/[^\w\s$%]/g, ' ')
113
+ .replace(/\s+/g, ' ')
114
+ .trim()
115
+ }
116
+
117
+ function scoreFact<T extends MemoryFactInput>(fact: T): number {
118
+ const confidence = clampImportance(typeof fact.confidence === 'number' ? fact.confidence : 0)
119
+ const durability = fact.durability ?? 'standard'
120
+ const type = fact.type ?? 'fact'
121
+
122
+ const durabilityWeight = durability === 'core' ? 0.35 : durability === 'standard' ? 0.2 : 0.05
123
+ const typeWeight = type === 'decision' ? 0.25 : type === 'fact' ? 0.18 : 0.1
124
+ const lengthWeight = Math.min(fact.content.length, 120) / 120 / 10
125
+
126
+ return confidence + durabilityWeight + typeWeight + lengthWeight
127
+ }
128
+
129
+ function resolveBestFact<T extends MemoryFactInput>(current: T, candidate: T): T {
130
+ const currentScore = scoreFact(current)
131
+ const candidateScore = scoreFact(candidate)
132
+ if (candidateScore > currentScore) return candidate
133
+ if (candidateScore < currentScore) return current
134
+ return candidate.content.length > current.content.length ? candidate : current
135
+ }
136
+
137
+ function normalizeFactForDedupe(text: string): string {
138
+ return normalizeMemoryKey(text)
139
+ .replace(/\b(the|a|an|is|are|was|were|to|of|for|and)\b/g, ' ')
140
+ .replace(/\s+/g, ' ')
141
+ .trim()
142
+ }
143
+
144
+ function resolveAdaptiveFactCap(rawFactCount: number): number {
145
+ const baseCap = 8
146
+ const maxCap = 18
147
+ if (!Number.isFinite(rawFactCount) || rawFactCount <= baseCap) return baseCap
148
+
149
+ const scaledCap = Math.round(rawFactCount * 0.7)
150
+ return Math.max(baseCap, Math.min(maxCap, scaledCap))
151
+ }
152
+
153
+ export function postProcessMemoryFacts<T extends MemoryFactInput>(
154
+ rawFacts: T[],
155
+ options: PostProcessMemoryFactsOptions = {},
156
+ ): T[] {
157
+ const maxFacts = options.maxFacts ?? resolveAdaptiveFactCap(rawFacts.length)
158
+ const minChars = options.minChars ?? 4
159
+ const maxChars = options.maxChars ?? 420
160
+ if (!Array.isArray(rawFacts) || rawFacts.length === 0) return []
161
+
162
+ const deduped = new Map<string, T>()
163
+
164
+ for (const fact of rawFacts) {
165
+ const content = typeof fact.content === 'string' ? fact.content.replace(/\s+/g, ' ').trim() : ''
166
+ if (!content || content.length < minChars || content.length > maxChars) continue
167
+ const normalizedFact = { ...fact, content }
168
+ const key = normalizeFactForDedupe(content)
169
+ if (!key) continue
170
+
171
+ const existing = deduped.get(key)
172
+ if (!existing) {
173
+ deduped.set(key, normalizedFact)
174
+ continue
175
+ }
176
+
177
+ deduped.set(key, resolveBestFact(existing, normalizedFact))
178
+ }
179
+
180
+ return [...deduped.values()].sort((left, right) => scoreFact(right) - scoreFact(left)).slice(0, maxFacts)
181
+ }
182
+
183
+ export function buildMemoryFactMaps<T extends MemoryFactInput>(facts: T[]): MemoryFactMaps {
184
+ const confidenceByKey = new Map<string, number>()
185
+ const durabilityByKey = new Map<string, string>()
186
+ const categoryByKey = new Map<string, string | undefined>()
187
+
188
+ for (const fact of facts) {
189
+ const key = normalizeMemoryKey(fact.content)
190
+ confidenceByKey.set(key, fact.confidence)
191
+ durabilityByKey.set(key, fact.durability ?? 'standard')
192
+ categoryByKey.set(key, fact.type)
193
+ }
194
+
195
+ return { confidenceByKey, durabilityByKey, categoryByKey }
196
+ }
197
+
198
+ function toUniqueOrderedIds(value: string[] | undefined, validIds: Set<string>): string[] {
199
+ if (!value || value.length === 0) return []
200
+ const seen = new Set<string>()
201
+ const ordered: string[] = []
202
+ for (const rawId of value) {
203
+ const id = rawId.trim()
204
+ if (!id || seen.has(id) || !validIds.has(id)) continue
205
+ seen.add(id)
206
+ ordered.push(id)
207
+ }
208
+ return ordered
209
+ }
210
+
211
+ export function compileMemoryUpdatesFromDelta(params: {
212
+ existingMemories: Array<{ id: string; text: string }>
213
+ newFacts: string[]
214
+ delta: MemoryDeltaOutputLike
215
+ }): MemoryUpdateOutputLike {
216
+ const existingById = new Map(params.existingMemories.map((memory) => [memory.id, memory.text]))
217
+ const validExistingIds = new Set(existingById.keys())
218
+ const updates: MemoryUpdateItemLike[] = []
219
+ const deleteSet = new Set<string>()
220
+ let nextAddIndex = 0
221
+
222
+ const findDeltaForFact = (fact: string, index: number): MemoryDeltaItemLike | null => {
223
+ const direct = params.delta.deltas.at(index)
224
+ if (direct?.fact === fact) return direct
225
+ return params.delta.deltas.find((delta) => delta.fact === fact) ?? null
226
+ }
227
+
228
+ const nextAddId = () => {
229
+ const next = `new_${nextAddIndex}`
230
+ nextAddIndex += 1
231
+ return next
232
+ }
233
+
234
+ const prepared = params.newFacts.map((rawFact, index) => {
235
+ const fact = rawFact.trim()
236
+ const delta = fact ? findDeltaForFact(fact, index) : null
237
+ const classification = delta?.classification ?? 'new'
238
+ const targetMemoryIds = toUniqueOrderedIds(delta?.targetMemoryIds, validExistingIds)
239
+ const invalidateTargetIds = toUniqueOrderedIds(delta?.invalidateTargetIds, validExistingIds).filter((id) =>
240
+ targetMemoryIds.includes(id),
241
+ )
242
+
243
+ return { index, fact, delta, classification, targetMemoryIds, invalidateTargetIds }
244
+ })
245
+
246
+ const addIdByFactIndex = new Map<number, string>()
247
+ for (const item of prepared) {
248
+ if (!item.fact) continue
249
+
250
+ const shouldAdd =
251
+ item.classification === 'new' ||
252
+ item.classification === 'supersedes' ||
253
+ item.classification === 'contradicts' ||
254
+ (item.classification === 'enriches' && item.targetMemoryIds.length === 0)
255
+
256
+ if (!shouldAdd) continue
257
+ addIdByFactIndex.set(item.index, nextAddId())
258
+ }
259
+
260
+ const buildRelations = (
261
+ item: (typeof prepared)[number],
262
+ fallbackClassification: 'supersedes' | 'contradicts' | 'new',
263
+ ): MemoryUpdateRelationLike[] => {
264
+ const relations: MemoryUpdateRelationLike[] = []
265
+ const relationKeys = new Set<string>()
266
+ const pushRelation = (memoryId: string, relation: string): void => {
267
+ const id = memoryId.trim()
268
+ if (!id || !relation) return
269
+ const key = `${id}|${relation}`
270
+ if (relationKeys.has(key)) return
271
+ relationKeys.add(key)
272
+ relations.push({ memoryId: id, relation })
273
+ }
274
+
275
+ let explicitRelationCount = 0
276
+ for (const relation of item.delta?.relations ?? []) {
277
+ if (typeof relation.relation !== 'string') continue
278
+
279
+ if (typeof relation.targetMemoryId === 'string') {
280
+ const targetMemoryId = relation.targetMemoryId.trim()
281
+ if (targetMemoryId && validExistingIds.has(targetMemoryId)) {
282
+ pushRelation(targetMemoryId, relation.relation)
283
+ explicitRelationCount += 1
284
+ }
285
+ }
286
+
287
+ if (typeof relation.targetFactIndex === 'number' && Number.isInteger(relation.targetFactIndex)) {
288
+ if (relation.targetFactIndex === item.index) continue
289
+ const targetFactIndex = relation.targetFactIndex
290
+ const targetFactAddId = addIdByFactIndex.get(targetFactIndex)
291
+ if (targetFactAddId) {
292
+ pushRelation(targetFactAddId, relation.relation)
293
+ explicitRelationCount += 1
294
+ }
295
+ }
296
+ }
297
+
298
+ if (fallbackClassification === 'supersedes' || fallbackClassification === 'contradicts') {
299
+ for (const targetMemoryId of item.targetMemoryIds) {
300
+ pushRelation(targetMemoryId, fallbackClassification)
301
+ }
302
+ } else if (item.targetMemoryIds.length > 0 && explicitRelationCount === 0) {
303
+ for (const targetMemoryId of item.targetMemoryIds) {
304
+ pushRelation(targetMemoryId, 'supports')
305
+ }
306
+ }
307
+
308
+ return relations
309
+ }
310
+
311
+ for (const item of prepared) {
312
+ if (!item.fact) continue
313
+
314
+ for (const invalidateId of item.invalidateTargetIds) {
315
+ deleteSet.add(invalidateId)
316
+ }
317
+
318
+ if (item.classification === 'duplicate') {
319
+ const targetId = item.targetMemoryIds[0]
320
+ if (targetId) {
321
+ updates.push({ id: targetId, text: existingById.get(targetId) ?? item.fact, event: 'NONE' })
322
+ } else {
323
+ updates.push({ id: `noop_${item.index}`, text: item.fact, event: 'NONE' })
324
+ }
325
+ continue
326
+ }
327
+
328
+ if (item.classification === 'enriches') {
329
+ const targetId = item.targetMemoryIds[0]
330
+ if (targetId) {
331
+ const relations = buildRelations(item, 'new').filter((relation) => relation.memoryId !== targetId)
332
+ updates.push({
333
+ id: targetId,
334
+ text: item.fact,
335
+ event: 'UPDATE',
336
+ oldMemory: existingById.get(targetId),
337
+ ...(relations.length > 0 ? { relatesTo: relations } : {}),
338
+ })
339
+ continue
340
+ }
341
+
342
+ const addId = addIdByFactIndex.get(item.index) ?? nextAddId()
343
+ const relations = buildRelations(item, 'new')
344
+ updates.push({
345
+ id: addId,
346
+ text: item.fact,
347
+ event: 'ADD',
348
+ ...(relations.length > 0 ? { relatesTo: relations } : {}),
349
+ })
350
+ continue
351
+ }
352
+
353
+ if (item.classification === 'supersedes' || item.classification === 'contradicts') {
354
+ const addId = addIdByFactIndex.get(item.index) ?? nextAddId()
355
+ const relations = buildRelations(item, item.classification)
356
+ updates.push({
357
+ id: addId,
358
+ text: item.fact,
359
+ event: 'ADD',
360
+ ...(relations.length > 0 ? { relatesTo: relations } : {}),
361
+ })
362
+ continue
363
+ }
364
+
365
+ const addId = addIdByFactIndex.get(item.index) ?? nextAddId()
366
+ const relations = buildRelations(item, 'new')
367
+ updates.push({
368
+ id: addId,
369
+ text: item.fact,
370
+ event: 'ADD',
371
+ ...(relations.length > 0 ? { relatesTo: relations } : {}),
372
+ })
373
+ }
374
+
375
+ const preservedRelationTargetIds = new Set<string>()
376
+ for (const update of updates) {
377
+ if (!update.relatesTo?.length) continue
378
+ for (const relation of update.relatesTo) {
379
+ preservedRelationTargetIds.add(relation.memoryId)
380
+ }
381
+ }
382
+
383
+ for (const deleteId of deleteSet) {
384
+ if (preservedRelationTargetIds.has(deleteId)) continue
385
+ updates.push({ id: deleteId, text: existingById.get(deleteId) ?? '', event: 'DELETE' })
386
+ }
387
+
388
+ return { memory: updates }
389
+ }
390
+
391
+ function resolveMemoryImportance(params: MemoryImportanceParams): number {
392
+ if (typeof params.explicitImportance === 'number') {
393
+ return clampImportance(params.explicitImportance)
394
+ }
395
+
396
+ if (params.memoryType === 'user_request') {
397
+ return 0.9
398
+ }
399
+
400
+ const normalizedText = normalizeMemoryKey(params.text)
401
+ const aiConfidence = params.confidenceByKey.get(normalizedText)
402
+ if (aiConfidence !== undefined) {
403
+ const durability = params.durabilityByKey.get(normalizedText) ?? 'standard'
404
+ const category = params.categoryByKey.get(normalizedText) ?? 'fact'
405
+
406
+ const durabilityCap = durability === 'core' ? 0.95 : durability === 'standard' ? 0.85 : 0.7
407
+ const categoryCap = category === 'decision' ? 0.95 : category === 'fact' ? 0.85 : 0.7
408
+ return clampImportance(Math.min(aiConfidence, durabilityCap, categoryCap))
409
+ }
410
+
411
+ return params.defaultImportance ?? 0.65
412
+ }
413
+
414
+ export function createMemoryActionPlan<TRelation extends string = string>(params: {
415
+ updates: MemoryUpdateOutputLike<TRelation>
416
+ memoryType: string
417
+ explicitImportance?: number
418
+ confidenceByKey: Map<string, number>
419
+ durabilityByKey: Map<string, string>
420
+ categoryByKey: Map<string, string | undefined>
421
+ existingMemories?: Array<{ id: string; text: string }>
422
+ }): MemoryActionPlan<TRelation> {
423
+ const actions: Array<MemoryActionAdd | MemoryActionUpdate | MemoryActionDelete> = []
424
+ const relations: MemoryActionRelation<TRelation>[] = []
425
+
426
+ const actionKeys = new Set<string>()
427
+ const relationKeys = new Set<string>()
428
+ const existingIds = new Set((params.existingMemories ?? []).map((memory) => memory.id))
429
+ const usedRefIds = new Set<string>()
430
+ const addRefByIndex = new Map<number, string>()
431
+ const addIdAliasMap = new Map<string, string>()
432
+
433
+ for (const item of params.updates.memory) {
434
+ if (typeof item.id === 'string' && item.id.trim().length > 0) {
435
+ usedRefIds.add(item.id.trim())
436
+ }
437
+ }
438
+
439
+ let generatedCounter = 0
440
+ const nextGeneratedAddRefId = (): string => {
441
+ while (usedRefIds.has(`new_generated_${generatedCounter}`)) {
442
+ generatedCounter += 1
443
+ }
444
+ const refId = `new_generated_${generatedCounter}`
445
+ usedRefIds.add(refId)
446
+ generatedCounter += 1
447
+ return refId
448
+ }
449
+
450
+ const pushAction = (action: MemoryActionAdd | MemoryActionUpdate | MemoryActionDelete): void => {
451
+ const key = `${action.type}|${action.refId}`
452
+ if (actionKeys.has(key)) return
453
+ actionKeys.add(key)
454
+ actions.push(action)
455
+ }
456
+
457
+ const pushRelation = (relation: MemoryActionRelation<TRelation>): void => {
458
+ const fromRefId = relation.fromRefId.trim()
459
+ const toRefId = relation.toRefId.trim()
460
+ if (!fromRefId || !toRefId || fromRefId === toRefId) return
461
+
462
+ const key = `${fromRefId}|${toRefId}|${String(relation.relation)}`
463
+ if (relationKeys.has(key)) return
464
+ relationKeys.add(key)
465
+ relations.push({ ...relation, fromRefId, toRefId })
466
+ }
467
+
468
+ for (const [index, item] of params.updates.memory.entries()) {
469
+ const text = typeof item.text === 'string' ? item.text.replace(/\s+/g, ' ').trim() : ''
470
+ const itemId = typeof item.id === 'string' ? item.id.trim() : ''
471
+
472
+ switch (item.event) {
473
+ case 'ADD': {
474
+ if (!text) break
475
+ const refId = !itemId || existingIds.has(itemId) ? nextGeneratedAddRefId() : itemId
476
+ addRefByIndex.set(index, refId)
477
+ if (itemId && itemId !== refId) {
478
+ addIdAliasMap.set(itemId, refId)
479
+ }
480
+
481
+ const normalizedText = normalizeMemoryKey(text)
482
+ pushAction({
483
+ type: 'add',
484
+ refId,
485
+ text,
486
+ normalizedText,
487
+ importance: resolveMemoryImportance({
488
+ text,
489
+ memoryType: params.memoryType,
490
+ explicitImportance: params.explicitImportance,
491
+ confidenceByKey: params.confidenceByKey,
492
+ durabilityByKey: params.durabilityByKey,
493
+ categoryByKey: params.categoryByKey,
494
+ }),
495
+ durability: params.durabilityByKey.get(normalizedText) ?? 'standard',
496
+ category: params.categoryByKey.get(normalizedText) ?? 'fact',
497
+ })
498
+ break
499
+ }
500
+
501
+ case 'UPDATE': {
502
+ if (!itemId || itemId.startsWith('new_') || !text) break
503
+ const normalizedText = normalizeMemoryKey(text)
504
+ pushAction({
505
+ type: 'update',
506
+ refId: itemId,
507
+ text,
508
+ normalizedText,
509
+ importance: resolveMemoryImportance({
510
+ text,
511
+ memoryType: params.memoryType,
512
+ explicitImportance: params.explicitImportance,
513
+ confidenceByKey: params.confidenceByKey,
514
+ durabilityByKey: params.durabilityByKey,
515
+ categoryByKey: params.categoryByKey,
516
+ }),
517
+ durability: params.durabilityByKey.get(normalizedText),
518
+ category: params.categoryByKey.get(normalizedText),
519
+ })
520
+ break
521
+ }
522
+
523
+ case 'DELETE': {
524
+ if (!itemId || itemId.startsWith('new_')) break
525
+ pushAction({ type: 'delete', refId: itemId })
526
+ break
527
+ }
528
+
529
+ case 'NONE':
530
+ break
531
+ }
532
+ }
533
+
534
+ for (const [index, item] of params.updates.memory.entries()) {
535
+ if (item.event === 'DELETE' || item.event === 'NONE' || !item.relatesTo?.length) continue
536
+
537
+ const fromRefId =
538
+ item.event === 'ADD' ? addRefByIndex.get(index) : typeof item.id === 'string' ? item.id.trim() : ''
539
+ if (!fromRefId) continue
540
+
541
+ for (const relation of item.relatesTo) {
542
+ if (typeof relation.memoryId !== 'string' || !relation.memoryId.trim()) continue
543
+ const rawToRefId = relation.memoryId.trim()
544
+ const toRefId = addIdAliasMap.get(rawToRefId) ?? rawToRefId
545
+ pushRelation({ fromRefId, toRefId, relation: relation.relation })
546
+ }
547
+ }
548
+
549
+ const knownIds = new Set<string>((params.existingMemories ?? []).map((memory) => memory.id))
550
+ for (const action of actions) {
551
+ knownIds.add(action.refId)
552
+ }
553
+
554
+ const filteredRelations = relations.filter((relation) => {
555
+ return knownIds.has(relation.fromRefId) && knownIds.has(relation.toRefId)
556
+ })
557
+
558
+ if (filteredRelations.length !== relations.length) {
559
+ relationKeys.clear()
560
+ relations.length = 0
561
+ for (const relation of filteredRelations) {
562
+ const key = `${relation.fromRefId}|${relation.toRefId}|${String(relation.relation)}`
563
+ if (relationKeys.has(key)) continue
564
+ relationKeys.add(key)
565
+ relations.push(relation)
566
+ }
567
+ }
568
+
569
+ return { actions, relations }
570
+ }
@@ -0,0 +1,47 @@
1
+ export function getFactRetrievalMessages(
2
+ parsedMessages: string,
3
+ customPrompt?: string,
4
+ maxFacts = 8,
5
+ ): [string, string] {
6
+ const baseInstructions = customPrompt ? `\n\nAdditional instructions: ${customPrompt}` : ''
7
+
8
+ const systemPrompt = `You extract durable memories from conversations.
9
+
10
+ Goal:
11
+ - Return a short list of stable, long-term facts, preferences, or decisions that were explicitly stated.
12
+
13
+ Classification:
14
+ - "fact": Objective information (tools, architecture, team structure, processes).
15
+ - "preference": Subjective preference or style choice.
16
+ - "decision": Explicit decision, requirement, or policy.
17
+
18
+ Confidence scoring:
19
+ - 1.0: Explicit, confirmed statements ("We use TypeScript", "I prefer dark mode", "We decided to migrate").
20
+ - 0.8: Strong implications or clearly implied facts.
21
+ - 0.6-0.7: Reasonable inferences with some ambiguity.
22
+ - Below 0.6: Do NOT extract. Skip entirely.
23
+
24
+ Durability classification:
25
+ - "core": Business decisions, technical architecture choices, confirmed requirements, strategic direction. These are foundational and rarely change. Examples: "We use SurrealDB", "We decided to adopt microservices", "Revenue target is $1M ARR".
26
+ - "standard": General facts, moderate inferences, process details. These may evolve over time. Examples: "Team has 5 engineers", "Sprint cycle is 2 weeks", "Using Jest for testing".
27
+ - "ephemeral": Personal preferences, one-off interactions, formatting choices, style opinions. These are least important long-term. Examples: "User prefers bullet points", "Prefers dark mode", "Likes concise answers".
28
+
29
+ Hard rules:
30
+ - Only extract information the user states as true, as a decision, as a requirement, or as a stable preference.
31
+ - Do NOT extract transient tasks, questions, research requests, or one-off conversation details.
32
+ - Do NOT turn suggestions, brainstorming, or hypotheticals into facts. If something is not confirmed, skip it.
33
+ - Do NOT trust new facts introduced by the agent unless the user explicitly confirms them.
34
+ - Preserve concrete numeric literals and units exactly when present (examples: "6", "50k", "$79", "15%", "Q2 2026").
35
+ - If a statement explicitly marks an older policy/value as deprecated or obsolete, capture both:
36
+ 1) the current truth
37
+ 2) the deprecation/obsolescence fact
38
+ - Prefer returning fewer items. If uncertain, return an empty list.
39
+ - Max ${maxFacts} facts.
40
+
41
+ Today's date is ${new Date().toISOString().split('T')[0]}.
42
+ ${baseInstructions}`
43
+
44
+ const userPrompt = `Conversation:\n${parsedMessages}`
45
+
46
+ return [systemPrompt, userPrompt]
47
+ }
@@ -0,0 +1,3 @@
1
+ export function parseMessages(messages: { role: 'user' | 'agent'; content: string }[]): string {
2
+ return messages.map((m) => `${m.role === 'user' ? 'User' : 'Agent'}: ${m.content}`).join('\n')
3
+ }
@@ -0,0 +1,37 @@
1
+ export function getClassifyMemoryDeltaPrompt(params: {
2
+ existingMemories: { id: string; text: string }[]
3
+ newFacts: string[]
4
+ }): { systemPrompt: string; userPrompt: string } {
5
+ const systemPrompt = `You classify how each new fact relates to existing memory.
6
+
7
+ Decide one classification per fact:
8
+ - new: fact is durable and not materially covered.
9
+ - supersedes: newer truth replacing older truth.
10
+ - contradicts: explicit negation/removal of prior statement or policy.
11
+ - enriches: same durable meaning, better details; usually update one target.
12
+ - duplicate: already represented.
13
+
14
+ Rules:
15
+ - Return exactly one delta item per new fact, preserving order.
16
+ - fact must match the corresponding newFacts entry verbatim.
17
+ - targetMemoryIds and invalidateTargetIds may only contain ids from existingMemories.
18
+ - invalidateTargetIds must be a subset of targetMemoryIds.
19
+ - In each relation item, set exactly one target: targetMemoryId or targetFactIndex.
20
+ - targetFactIndex must be valid, must not point to the same fact index, and is only for relations to other new facts.
21
+ - For supersedes/contradicts, include target memories when evidence exists.
22
+ - If uncertain, prefer conservative output: classify as new with no targets.
23
+
24
+ The caller enforces a structured output schema. Return only schema fields.`
25
+
26
+ const userPrompt = JSON.stringify(
27
+ {
28
+ task: 'Classify memory deltas for new facts.',
29
+ existingMemories: params.existingMemories,
30
+ newFacts: params.newFacts,
31
+ },
32
+ null,
33
+ 2,
34
+ )
35
+
36
+ return { systemPrompt, userPrompt }
37
+ }
@@ -0,0 +1,43 @@
1
+ export const ORG_SCOPE_PREFIX = 'org'
2
+
3
+ const SCOPE_ID_MAX_LENGTH = 255
4
+ const SCOPE_PREFIX_REGEX = /^[a-z][a-z0-9_]*$/
5
+
6
+ function stripRecordPrefix(id: string): string {
7
+ if (typeof id !== 'string' || id.length === 0) {
8
+ throw new TypeError('id must be a non-empty string')
9
+ }
10
+ const [, ...rest] = id.split(':')
11
+ return rest.length > 0 ? rest.join(':') : id
12
+ }
13
+
14
+ export function scopeId(prefix: string, id: string): string {
15
+ if (typeof prefix !== 'string' || prefix.length === 0) {
16
+ throw new TypeError('prefix must be a non-empty string')
17
+ }
18
+ if (!SCOPE_PREFIX_REGEX.test(prefix)) {
19
+ throw new Error(`Invalid scope prefix: ${prefix}`)
20
+ }
21
+
22
+ const stripped = stripRecordPrefix(id)
23
+ const scoped = `${prefix}:${stripped}`
24
+ if (scoped.length > SCOPE_ID_MAX_LENGTH) {
25
+ throw new Error('scopeId exceeds maximum length')
26
+ }
27
+
28
+ return scoped
29
+ }
30
+
31
+ export function agentScopeId(orgId: string, agentName: string): string {
32
+ if (typeof agentName !== 'string' || agentName.trim().length === 0) {
33
+ throw new TypeError('agentName must be a non-empty string')
34
+ }
35
+ const strippedOrgId = stripRecordPrefix(orgId)
36
+ const scoped = `agent:${strippedOrgId}:${agentName.trim()}`
37
+
38
+ if (scoped.length > SCOPE_ID_MAX_LENGTH) {
39
+ throw new Error('scopeId exceeds maximum length')
40
+ }
41
+
42
+ return scoped
43
+ }