@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.
- package/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- 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,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
|
+
}
|