@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,807 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { agentRoster } from '../config/agent-defaults'
|
|
4
|
+
import { env } from '../config/env-shapes'
|
|
5
|
+
import { aiLogger } from '../config/logger'
|
|
6
|
+
import { Memory } from '../db/memory'
|
|
7
|
+
import { isUniqueIndexConflict } from '../db/memory-store.helpers'
|
|
8
|
+
import type {
|
|
9
|
+
AddOptions,
|
|
10
|
+
ExtractedFact,
|
|
11
|
+
MemoryRecord,
|
|
12
|
+
MemorySearchResult,
|
|
13
|
+
MemoryType,
|
|
14
|
+
Message,
|
|
15
|
+
} from '../db/memory-types'
|
|
16
|
+
import { withOrgMemoryLock } from '../redis/org-memory-lock'
|
|
17
|
+
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
18
|
+
import { sanitizeAgentOutputForMemory } from '../runtime/llm-content'
|
|
19
|
+
import { ORG_SCOPE_PREFIX, agentScopeId, scopeId } from '../runtime/memory-scope'
|
|
20
|
+
import {
|
|
21
|
+
countScopedRetrievalCandidates,
|
|
22
|
+
executeScopedRetrieval,
|
|
23
|
+
scopedRetrievalToMap,
|
|
24
|
+
} from '../runtime/retrieval-adapters'
|
|
25
|
+
import { createMemoryRerankerAgent, memoryRerankerPrompt } from '../system-agents/memory-reranker.agent'
|
|
26
|
+
import { createOrgMemoryAgent, orgMemoryPrompt } from '../system-agents/memory.agent'
|
|
27
|
+
import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
|
|
28
|
+
import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory.utils'
|
|
29
|
+
|
|
30
|
+
const ORG_MEMORY_TYPE = 'fact'
|
|
31
|
+
const RERANK_CANDIDATE_MAX_CHARS = 500
|
|
32
|
+
const MAX_MEMORY_RESULTS_PER_SCOPE = 10
|
|
33
|
+
const PRESEEDED_MEMORY_LIMIT = 5
|
|
34
|
+
const PRESEEDED_MEMORY_MAX_CHARS = 300
|
|
35
|
+
const PRESEEDED_MIN_IMPORTANCE = 0.7
|
|
36
|
+
const PRESEEDED_MEMORY_DURABILITY: MemoryRecord['durability'] = 'core'
|
|
37
|
+
const MAX_CONVERSATION_HISTORY_MESSAGES = 24
|
|
38
|
+
const MAX_CONVERSATION_MESSAGE_CHARS = 1_200
|
|
39
|
+
const MAX_CONVERSATION_MEMORY_BLOCK_CHARS = 2_000
|
|
40
|
+
const MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS = 6_000
|
|
41
|
+
const MAX_CONVERSATION_ASSESSMENT_CHARS = 7_000
|
|
42
|
+
const ONBOARDING_MEMORY_MAX_FACTS = 16
|
|
43
|
+
const ONBOARDING_MEMORY_EXTRACTION_PROMPT =
|
|
44
|
+
'Onboarding mode is active. Extract multiple concrete startup facts from user-provided context: company mission, product capabilities, customer segments, pricing, traction, go-to-market plans, roadmap, team composition, technical stack, risks, and referenced URLs. Prefer one fact per concrete claim.'
|
|
45
|
+
|
|
46
|
+
const helperModelRuntime = createHelperModelRuntime()
|
|
47
|
+
|
|
48
|
+
const MemoryRerankOutputSchema = z.object({
|
|
49
|
+
sections: z.array(
|
|
50
|
+
z.object({
|
|
51
|
+
title: z.string(),
|
|
52
|
+
items: z.array(
|
|
53
|
+
z.object({
|
|
54
|
+
id: z.string(),
|
|
55
|
+
relevance: z.string().describe('Short relevance reason. Use empty string when no reason is needed.'),
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export type MemoryRerankOutput = z.infer<typeof MemoryRerankOutputSchema>
|
|
63
|
+
|
|
64
|
+
const isRoutableAgentName = (value?: string): boolean => Boolean(value && agentRoster.includes(value))
|
|
65
|
+
|
|
66
|
+
class MemoryService {
|
|
67
|
+
private orgMemoryCache = new Map<string, Memory>()
|
|
68
|
+
|
|
69
|
+
private getOrCreateMemory(cacheKey: string, cache: Map<string, Memory>): Memory {
|
|
70
|
+
const cached = cache.get(cacheKey)
|
|
71
|
+
if (cached) return cached
|
|
72
|
+
|
|
73
|
+
const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: orgMemoryPrompt })
|
|
74
|
+
|
|
75
|
+
cache.set(cacheKey, memory)
|
|
76
|
+
aiLogger.debug`Memory client created and cached for ${cacheKey}`
|
|
77
|
+
return memory
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private getOrgMemory(orgId: string): Memory {
|
|
81
|
+
const cacheKey = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
82
|
+
return this.getOrCreateMemory(cacheKey, this.orgMemoryCache)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private truncateCandidateText(value: string): string {
|
|
86
|
+
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
87
|
+
if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
|
|
88
|
+
return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private readNumericMetadata(metadata: Record<string, unknown>, key: string): number {
|
|
92
|
+
const value = metadata[key]
|
|
93
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private buildRerankerCandidateText(candidate: MemorySearchResult): string {
|
|
98
|
+
const metadata = candidate.metadata
|
|
99
|
+
const relationCount = this.readNumericMetadata(metadata, 'relationCount')
|
|
100
|
+
const supportCount = this.readNumericMetadata(metadata, 'supportCount')
|
|
101
|
+
const contradictCount = this.readNumericMetadata(metadata, 'contradictCount')
|
|
102
|
+
|
|
103
|
+
const relatedContextRaw = metadata.relatedContext
|
|
104
|
+
const relatedContext = Array.isArray(relatedContextRaw)
|
|
105
|
+
? relatedContextRaw
|
|
106
|
+
.slice(0, 3)
|
|
107
|
+
.map((item) => String(item).trim())
|
|
108
|
+
.filter((item) => item.length > 0)
|
|
109
|
+
: []
|
|
110
|
+
|
|
111
|
+
const contextLines: string[] = []
|
|
112
|
+
if (relatedContext.length > 0) {
|
|
113
|
+
contextLines.push(`Related context: ${relatedContext.join(' | ')}`)
|
|
114
|
+
}
|
|
115
|
+
if (relationCount > 0 || supportCount > 0 || contradictCount > 0) {
|
|
116
|
+
contextLines.push(
|
|
117
|
+
`Graph signals: relations=${relationCount}; supports=${supportCount}; contradicts=${contradictCount}`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (contextLines.length === 0) return candidate.content
|
|
122
|
+
return `${candidate.content}\n\n${contextLines.join('\n')}`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private normalizeConversationText(value: string, maxChars: number): string {
|
|
126
|
+
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
127
|
+
if (!normalized) return ''
|
|
128
|
+
if (normalized.length <= maxChars) return normalized
|
|
129
|
+
return `${normalized.slice(0, maxChars - 3)}...`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private resolveAgentScopeNames(agentName?: string, agentNames: string[] = []): string[] {
|
|
133
|
+
const unique = new Set<string>()
|
|
134
|
+
if (typeof agentName === 'string' && agentName.trim()) {
|
|
135
|
+
unique.add(agentName.trim())
|
|
136
|
+
}
|
|
137
|
+
for (const candidate of agentNames) {
|
|
138
|
+
if (typeof candidate !== 'string') continue
|
|
139
|
+
const normalized = candidate.trim()
|
|
140
|
+
if (!normalized) continue
|
|
141
|
+
unique.add(normalized)
|
|
142
|
+
}
|
|
143
|
+
return [...unique].filter((name) => isRoutableAgentName(name))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private normalizePreSeededMemoryText(value: string): string {
|
|
147
|
+
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
148
|
+
if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
|
|
149
|
+
return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private formatPreSeededMemoriesSection(memories: MemoryRecord[]): string | undefined {
|
|
153
|
+
const lines = memories
|
|
154
|
+
.map((memory) => this.normalizePreSeededMemoryText(memory.content))
|
|
155
|
+
.filter((line) => line.length > 0)
|
|
156
|
+
.map((line) => `- ${line}`)
|
|
157
|
+
if (lines.length === 0) return undefined
|
|
158
|
+
|
|
159
|
+
return ['<pre-seeded-memories>', ...lines, '</pre-seeded-memories>'].join('\n')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private buildConversationMessages(params: {
|
|
163
|
+
input: string
|
|
164
|
+
output: string
|
|
165
|
+
historyMessages: Array<{ role: 'user' | 'agent'; content: string; agentName?: string }>
|
|
166
|
+
memoryBlock?: string
|
|
167
|
+
attachmentContext?: string
|
|
168
|
+
}): { messages: Message[]; normalizedInput: string; sanitizedOutput: string } {
|
|
169
|
+
const normalizedInput = this.normalizeConversationText(params.input, MAX_CONVERSATION_MESSAGE_CHARS)
|
|
170
|
+
const sanitizedOutput = this.normalizeConversationText(
|
|
171
|
+
sanitizeAgentOutputForMemory(params.output),
|
|
172
|
+
MAX_CONVERSATION_MESSAGE_CHARS,
|
|
173
|
+
)
|
|
174
|
+
if (!normalizedInput || !sanitizedOutput) {
|
|
175
|
+
return { messages: [], normalizedInput, sanitizedOutput }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const messages: Message[] = []
|
|
179
|
+
|
|
180
|
+
const normalizedHistory: Message[] = params.historyMessages
|
|
181
|
+
.map((message): Message | null => {
|
|
182
|
+
const role: Message['role'] = message.role === 'agent' ? 'agent' : 'user'
|
|
183
|
+
const normalized = this.normalizeConversationText(
|
|
184
|
+
role === 'agent' ? sanitizeAgentOutputForMemory(message.content) : message.content,
|
|
185
|
+
MAX_CONVERSATION_MESSAGE_CHARS,
|
|
186
|
+
)
|
|
187
|
+
if (!normalized) return null
|
|
188
|
+
return { role, content: normalized }
|
|
189
|
+
})
|
|
190
|
+
.filter((message): message is Message => message !== null)
|
|
191
|
+
.slice(-MAX_CONVERSATION_HISTORY_MESSAGES)
|
|
192
|
+
|
|
193
|
+
messages.push(...normalizedHistory)
|
|
194
|
+
|
|
195
|
+
const normalizedMemoryBlock = this.normalizeConversationText(
|
|
196
|
+
params.memoryBlock ?? '',
|
|
197
|
+
MAX_CONVERSATION_MEMORY_BLOCK_CHARS,
|
|
198
|
+
)
|
|
199
|
+
if (normalizedMemoryBlock) {
|
|
200
|
+
messages.push({ role: 'user', content: `Workstream memory block:\n${normalizedMemoryBlock}` })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const normalizedAttachmentContext = this.normalizeConversationText(
|
|
204
|
+
params.attachmentContext ?? '',
|
|
205
|
+
MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS,
|
|
206
|
+
)
|
|
207
|
+
if (normalizedAttachmentContext) {
|
|
208
|
+
messages.push({ role: 'user', content: `Attachment context:\n${normalizedAttachmentContext}` })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
messages.push({ role: 'user', content: normalizedInput })
|
|
212
|
+
messages.push({ role: 'agent', content: sanitizedOutput })
|
|
213
|
+
|
|
214
|
+
return { messages, normalizedInput, sanitizedOutput }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private buildConversationAssessmentInput(messages: Message[]): string {
|
|
218
|
+
const lines = messages.map((message, index) => `[${message.role.toUpperCase()} #${index + 1}] ${message.content}`)
|
|
219
|
+
return this.normalizeConversationText(lines.join('\n\n'), MAX_CONVERSATION_ASSESSMENT_CHARS)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async rerankCandidates(
|
|
223
|
+
query: string,
|
|
224
|
+
candidates: MemorySearchResult[],
|
|
225
|
+
maxItems: number,
|
|
226
|
+
): Promise<MemoryRerankOutput | null> {
|
|
227
|
+
if (candidates.length === 0) return null
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
return await helperModelRuntime.generateHelperStructured({
|
|
231
|
+
tag: 'memory-reranker',
|
|
232
|
+
createAgent: createMemoryRerankerAgent,
|
|
233
|
+
defaultSystemPrompt: memoryRerankerPrompt,
|
|
234
|
+
messages: [
|
|
235
|
+
{
|
|
236
|
+
role: 'user',
|
|
237
|
+
content: JSON.stringify({
|
|
238
|
+
query,
|
|
239
|
+
maxItems,
|
|
240
|
+
candidates: candidates.map((candidate) => ({
|
|
241
|
+
id: candidate.id,
|
|
242
|
+
text: this.truncateCandidateText(this.buildRerankerCandidateText(candidate)),
|
|
243
|
+
score: candidate.score,
|
|
244
|
+
})),
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
schema: MemoryRerankOutputSchema,
|
|
249
|
+
})
|
|
250
|
+
} catch (error) {
|
|
251
|
+
aiLogger.warn`Memory reranker failed: ${error}`
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async rerankCandidatesMultiScope(
|
|
257
|
+
query: string,
|
|
258
|
+
scopedCandidates: Array<{ scopeTag: string; candidates: MemorySearchResult[] }>,
|
|
259
|
+
maxItems: number,
|
|
260
|
+
): Promise<MemoryRerankOutput | null> {
|
|
261
|
+
const flattened = scopedCandidates.flatMap(({ scopeTag, candidates }) =>
|
|
262
|
+
candidates.map((candidate) => ({ ...candidate, scopeTag })),
|
|
263
|
+
)
|
|
264
|
+
if (flattened.length === 0 || flattened.length <= maxItems) return null
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
return await helperModelRuntime.generateHelperStructured({
|
|
268
|
+
tag: 'memory-reranker-multi-scope',
|
|
269
|
+
createAgent: createMemoryRerankerAgent,
|
|
270
|
+
defaultSystemPrompt: memoryRerankerPrompt,
|
|
271
|
+
messages: [
|
|
272
|
+
{
|
|
273
|
+
role: 'user',
|
|
274
|
+
content: JSON.stringify({
|
|
275
|
+
query,
|
|
276
|
+
maxItems,
|
|
277
|
+
candidates: flattened.map((candidate) => ({
|
|
278
|
+
id: candidate.id,
|
|
279
|
+
text: this.truncateCandidateText(this.buildRerankerCandidateText(candidate)),
|
|
280
|
+
score: candidate.score,
|
|
281
|
+
scope: candidate.scopeTag,
|
|
282
|
+
})),
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
schema: MemoryRerankOutputSchema,
|
|
287
|
+
})
|
|
288
|
+
} catch (error) {
|
|
289
|
+
aiLogger.warn`Multi-scope memory reranker failed: ${error}`
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private async searchMemories({
|
|
295
|
+
query,
|
|
296
|
+
memory,
|
|
297
|
+
scopeId,
|
|
298
|
+
memoryType,
|
|
299
|
+
}: {
|
|
300
|
+
query: string
|
|
301
|
+
memory: Memory
|
|
302
|
+
scopeId: string
|
|
303
|
+
memoryType: MemoryType
|
|
304
|
+
}): Promise<string> {
|
|
305
|
+
const limit = env.MEMORY_SEARCH_K
|
|
306
|
+
const candidateLimit = getCandidateLimit(limit)
|
|
307
|
+
|
|
308
|
+
const candidates = await memory.searchCandidates(query, { scopeId, limit: candidateLimit, memoryType })
|
|
309
|
+
|
|
310
|
+
aiLogger.debug`Memory search candidates (scopeId: ${scopeId}, candidates: ${candidates.length})`
|
|
311
|
+
|
|
312
|
+
if (candidates.length === 0) {
|
|
313
|
+
return 'No stored memories.'
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (candidates.length <= limit) {
|
|
317
|
+
aiLogger.debug`Skipping reranking (candidates: ${candidates.length} <= limit: ${limit})`
|
|
318
|
+
return formatMemoryResults(candidates)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const reranked = await this.rerankCandidates(query, candidates, limit)
|
|
322
|
+
return formatRerankedResults(reranked, candidates, limit)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async searchOrganizationMemories(orgId: string, query: string): Promise<string> {
|
|
326
|
+
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
327
|
+
aiLogger.debug`Organization memory search requested (orgId: ${orgId}, scopeId: ${orgScopeId}, queryLength: ${query.length})`
|
|
328
|
+
const memory = this.getOrgMemory(orgId)
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const results = await this.searchMemories({ query, memory, scopeId: orgScopeId, memoryType: ORG_MEMORY_TYPE })
|
|
332
|
+
aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
333
|
+
return results
|
|
334
|
+
} catch (error: unknown) {
|
|
335
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
336
|
+
aiLogger.error`Organization memory search failed: ${normalizedError}`
|
|
337
|
+
throw normalizedError
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async getStaleMemories(orgId: string): Promise<string> {
|
|
342
|
+
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
343
|
+
const memory = this.getOrgMemory(orgId)
|
|
344
|
+
try {
|
|
345
|
+
const stale = await memory.getStaleMemories(orgScopeId)
|
|
346
|
+
if (stale.length === 0) return ''
|
|
347
|
+
const items = stale.map((m) => `- [NEEDS REVIEW] ${m.content}`).join('\n')
|
|
348
|
+
return `Memories flagged for review (parent fact was superseded):\n${items}`
|
|
349
|
+
} catch {
|
|
350
|
+
return ''
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async searchOrganizationMemoriesRaw(
|
|
355
|
+
orgId: string,
|
|
356
|
+
query: string,
|
|
357
|
+
options?: { fastMode?: boolean; limit?: number },
|
|
358
|
+
): Promise<string> {
|
|
359
|
+
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
360
|
+
aiLogger.info`[MEMORY_DEBUG] searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
|
|
361
|
+
const memory = this.getOrgMemory(orgId)
|
|
362
|
+
const fastMode = options?.fastMode ?? true
|
|
363
|
+
const limit = options?.limit ?? (fastMode ? Math.min(env.MEMORY_SEARCH_K, 4) : env.MEMORY_SEARCH_K)
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const candidates = await memory.searchCandidates(query, {
|
|
367
|
+
scopeId: orgScopeId,
|
|
368
|
+
limit,
|
|
369
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
370
|
+
fastMode,
|
|
371
|
+
includeNeighborContext: !fastMode,
|
|
372
|
+
})
|
|
373
|
+
aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
|
|
374
|
+
return formatMemoryResults(candidates)
|
|
375
|
+
} catch (error: unknown) {
|
|
376
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
377
|
+
aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
|
|
378
|
+
throw normalizedError
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async searchAgentMemories(orgId: string, agentName: string, query: string): Promise<string> {
|
|
383
|
+
if (!isRoutableAgentName(agentName)) {
|
|
384
|
+
aiLogger.debug`Agent memory search skipped - invalid agentName: ${agentName}`
|
|
385
|
+
return 'No stored memories.'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const scoped = agentScopeId(orgId, agentName)
|
|
389
|
+
aiLogger.debug`Agent memory search requested (orgId: ${orgId}, agentName: ${agentName}, scopeId: ${scoped}, queryLength: ${query.length})`
|
|
390
|
+
const memory = this.getOrgMemory(orgId)
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const results = await this.searchMemories({ query, memory, scopeId: scoped, memoryType: ORG_MEMORY_TYPE })
|
|
394
|
+
aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
395
|
+
return results
|
|
396
|
+
} catch (error: unknown) {
|
|
397
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
398
|
+
aiLogger.error`Agent memory search failed: ${normalizedError}`
|
|
399
|
+
throw normalizedError
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async searchOrgMemoriesForAgent(orgId: string, agentName: string, query: string): Promise<string> {
|
|
404
|
+
if (!isRoutableAgentName(agentName)) {
|
|
405
|
+
return await this.searchOrganizationMemories(orgId, query)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const [agentResult, orgResult] = await Promise.all([
|
|
409
|
+
this.searchAgentMemories(orgId, agentName, query),
|
|
410
|
+
this.searchOrganizationMemories(orgId, query),
|
|
411
|
+
])
|
|
412
|
+
|
|
413
|
+
return `Agent memory (${agentName}):\n${agentResult}\n\nGlobal org memory:\n${orgResult}`
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async getTopMemories(params: { orgId: string; agentName?: string; limit?: number }): Promise<string | undefined> {
|
|
417
|
+
const orgMemory = this.getOrgMemory(params.orgId)
|
|
418
|
+
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, params.orgId)
|
|
419
|
+
const requestedLimit = params.limit ?? PRESEEDED_MEMORY_LIMIT
|
|
420
|
+
const limit = Math.max(1, Math.min(requestedLimit, PRESEEDED_MEMORY_LIMIT))
|
|
421
|
+
|
|
422
|
+
const orgRequest = orgMemory.listTopMemories({
|
|
423
|
+
scopeId: orgScopeId,
|
|
424
|
+
limit,
|
|
425
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
426
|
+
durability: PRESEEDED_MEMORY_DURABILITY,
|
|
427
|
+
minImportance: PRESEEDED_MIN_IMPORTANCE,
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
const agentRequest =
|
|
431
|
+
params.agentName && isRoutableAgentName(params.agentName)
|
|
432
|
+
? orgMemory.listTopMemories({
|
|
433
|
+
scopeId: agentScopeId(params.orgId, params.agentName),
|
|
434
|
+
limit,
|
|
435
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
436
|
+
durability: PRESEEDED_MEMORY_DURABILITY,
|
|
437
|
+
minImportance: PRESEEDED_MIN_IMPORTANCE,
|
|
438
|
+
})
|
|
439
|
+
: Promise.resolve([] as MemoryRecord[])
|
|
440
|
+
|
|
441
|
+
const [orgTopMemories, agentTopMemories] = await Promise.all([orgRequest, agentRequest])
|
|
442
|
+
const combined = [...agentTopMemories, ...orgTopMemories].sort((left, right) => {
|
|
443
|
+
if (right.importance !== left.importance) return right.importance - left.importance
|
|
444
|
+
if (right.accessCount !== left.accessCount) return right.accessCount - left.accessCount
|
|
445
|
+
const rightLastAccess = right.lastAccessedAt?.getTime() ?? 0
|
|
446
|
+
const leftLastAccess = left.lastAccessedAt?.getTime() ?? 0
|
|
447
|
+
if (rightLastAccess !== leftLastAccess) return rightLastAccess - leftLastAccess
|
|
448
|
+
return right.createdAt.getTime() - left.createdAt.getTime()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const deduped: MemoryRecord[] = []
|
|
452
|
+
const seen = new Set<string>()
|
|
453
|
+
for (const memory of combined) {
|
|
454
|
+
const normalizedKey = memory.content.replace(/\s+/g, ' ').trim().toLowerCase()
|
|
455
|
+
if (!normalizedKey || seen.has(normalizedKey)) continue
|
|
456
|
+
seen.add(normalizedKey)
|
|
457
|
+
deduped.push(memory)
|
|
458
|
+
if (deduped.length >= limit) break
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return this.formatPreSeededMemoriesSection(deduped)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async searchAllMemoriesBatched({
|
|
465
|
+
orgId,
|
|
466
|
+
agentName,
|
|
467
|
+
query,
|
|
468
|
+
fastMode = false,
|
|
469
|
+
allowMultiScopeRerank = true,
|
|
470
|
+
}: {
|
|
471
|
+
orgId: string
|
|
472
|
+
agentName?: string
|
|
473
|
+
query: string
|
|
474
|
+
fastMode?: boolean
|
|
475
|
+
allowMultiScopeRerank?: boolean
|
|
476
|
+
}): Promise<string> {
|
|
477
|
+
const limit = Math.min(env.MEMORY_SEARCH_K, MAX_MEMORY_RESULTS_PER_SCOPE)
|
|
478
|
+
const candidateLimit = fastMode ? limit : getCandidateLimit(limit)
|
|
479
|
+
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
480
|
+
const orgMemory = this.getOrgMemory(orgId)
|
|
481
|
+
|
|
482
|
+
const retrievalTasks = [
|
|
483
|
+
{
|
|
484
|
+
scopeTag: 'org',
|
|
485
|
+
retrieve: async () =>
|
|
486
|
+
await orgMemory.searchCandidates(query, {
|
|
487
|
+
scopeId: orgScopeId,
|
|
488
|
+
limit: candidateLimit,
|
|
489
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
490
|
+
fastMode,
|
|
491
|
+
includeNeighborContext: !fastMode,
|
|
492
|
+
}),
|
|
493
|
+
},
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
if (isRoutableAgentName(agentName)) {
|
|
497
|
+
const agentScoped = agentScopeId(orgId, agentName as string)
|
|
498
|
+
retrievalTasks.push({
|
|
499
|
+
scopeTag: `agent:${agentName}`,
|
|
500
|
+
retrieve: async () =>
|
|
501
|
+
await orgMemory.searchCandidates(query, {
|
|
502
|
+
scopeId: agentScoped,
|
|
503
|
+
limit: candidateLimit,
|
|
504
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
505
|
+
fastMode,
|
|
506
|
+
includeNeighborContext: !fastMode,
|
|
507
|
+
}),
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const results = await executeScopedRetrieval<MemorySearchResult>(retrievalTasks)
|
|
512
|
+
|
|
513
|
+
const totalCandidates = countScopedRetrievalCandidates(results)
|
|
514
|
+
aiLogger.debug`Batched memory search candidates (scopes: ${results.length}, total: ${totalCandidates})`
|
|
515
|
+
|
|
516
|
+
if (totalCandidates === 0) {
|
|
517
|
+
return 'No stored memories.'
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (fastMode || !allowMultiScopeRerank) {
|
|
521
|
+
const candidatesByScopeTag = scopedRetrievalToMap(results)
|
|
522
|
+
return this.formatBatchedResults(null, candidatesByScopeTag, limit, agentName)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const reranked = await this.rerankCandidatesMultiScope(query, results, limit)
|
|
526
|
+
const candidatesByScopeTag = scopedRetrievalToMap(results)
|
|
527
|
+
|
|
528
|
+
return this.formatBatchedResults(reranked, candidatesByScopeTag, limit, agentName)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private formatBatchedResults(
|
|
532
|
+
reranked: MemoryRerankOutput | null,
|
|
533
|
+
candidatesByScopeTag: Map<string, MemorySearchResult[]>,
|
|
534
|
+
limit: number,
|
|
535
|
+
agentName?: string,
|
|
536
|
+
): string {
|
|
537
|
+
if (reranked && reranked.sections.length > 0) {
|
|
538
|
+
const allCandidates = Array.from(candidatesByScopeTag.values()).flat()
|
|
539
|
+
return formatRerankedResults(reranked, allCandidates, limit)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const sections: string[] = []
|
|
543
|
+
|
|
544
|
+
if (agentName) {
|
|
545
|
+
const agentCandidates = candidatesByScopeTag.get(`agent:${agentName}`) ?? []
|
|
546
|
+
if (agentCandidates.length > 0) {
|
|
547
|
+
sections.push(`Agent memory (${agentName}):\n${formatMemoryResults(agentCandidates.slice(0, limit))}`)
|
|
548
|
+
} else {
|
|
549
|
+
sections.push(`Agent memory (${agentName}):\nNo stored memories.`)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const orgCandidates = candidatesByScopeTag.get('org') ?? []
|
|
554
|
+
if (orgCandidates.length > 0) {
|
|
555
|
+
sections.push(
|
|
556
|
+
`${agentName ? 'Global org memory' : 'Organization memory'}:\n${formatMemoryResults(orgCandidates.slice(0, limit))}`,
|
|
557
|
+
)
|
|
558
|
+
} else {
|
|
559
|
+
sections.push(`${agentName ? 'Global org memory' : 'Organization memory'}:\nNo stored memories.`)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return sections.join('\n\n')
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async createOrganizationMemory({
|
|
566
|
+
orgId,
|
|
567
|
+
content,
|
|
568
|
+
memoryType,
|
|
569
|
+
metadata,
|
|
570
|
+
importance,
|
|
571
|
+
}: {
|
|
572
|
+
orgId: string
|
|
573
|
+
content: string
|
|
574
|
+
memoryType: MemoryType
|
|
575
|
+
metadata?: Record<string, unknown>
|
|
576
|
+
importance?: number
|
|
577
|
+
}): Promise<string> {
|
|
578
|
+
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
579
|
+
aiLogger.info`[MEMORY_DEBUG] createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
|
|
580
|
+
const memory = this.getOrgMemory(orgId)
|
|
581
|
+
try {
|
|
582
|
+
return await memory.insert(content, {
|
|
583
|
+
scopeId: orgScopeId,
|
|
584
|
+
memoryType,
|
|
585
|
+
importance: importance ?? 1,
|
|
586
|
+
metadata: { orgId, ...metadata },
|
|
587
|
+
})
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (isUniqueIndexConflict(error, 'memoryHashIdx')) {
|
|
590
|
+
aiLogger.debug`Organization memory already exists (hash conflict)`
|
|
591
|
+
return ''
|
|
592
|
+
}
|
|
593
|
+
throw error
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async createAgentMemory({
|
|
598
|
+
orgId,
|
|
599
|
+
agentName,
|
|
600
|
+
content,
|
|
601
|
+
memoryType,
|
|
602
|
+
metadata,
|
|
603
|
+
importance,
|
|
604
|
+
}: {
|
|
605
|
+
orgId: string
|
|
606
|
+
agentName: string
|
|
607
|
+
content: string
|
|
608
|
+
memoryType: MemoryType
|
|
609
|
+
metadata?: Record<string, unknown>
|
|
610
|
+
importance?: number
|
|
611
|
+
}): Promise<string> {
|
|
612
|
+
if (!isRoutableAgentName(agentName)) {
|
|
613
|
+
throw new Error(`Invalid agentName for agent memory: ${agentName}`)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const memory = this.getOrgMemory(orgId)
|
|
617
|
+
const scoped = agentScopeId(orgId, agentName)
|
|
618
|
+
try {
|
|
619
|
+
return await memory.insert(content, {
|
|
620
|
+
scopeId: scoped,
|
|
621
|
+
memoryType,
|
|
622
|
+
importance: importance ?? 1,
|
|
623
|
+
metadata: { orgId, agentName, memoryScope: 'agent', ...metadata },
|
|
624
|
+
})
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (isUniqueIndexConflict(error, 'memoryHashIdx')) {
|
|
627
|
+
aiLogger.debug`Agent memory already exists (hash conflict)`
|
|
628
|
+
return ''
|
|
629
|
+
}
|
|
630
|
+
throw error
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async updateOrganizationMemoryById({
|
|
635
|
+
orgId,
|
|
636
|
+
memoryId,
|
|
637
|
+
content,
|
|
638
|
+
}: {
|
|
639
|
+
orgId: string
|
|
640
|
+
memoryId: string
|
|
641
|
+
content: string
|
|
642
|
+
}): Promise<void> {
|
|
643
|
+
const memory = this.getOrgMemory(orgId)
|
|
644
|
+
await memory.updateMemory(memoryId, content)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async addExtractedFactsToScopes(params: {
|
|
648
|
+
orgId: string
|
|
649
|
+
facts: ExtractedFact[]
|
|
650
|
+
source: string
|
|
651
|
+
sourceMetadata?: Record<string, unknown>
|
|
652
|
+
agentNames?: string[]
|
|
653
|
+
acquireLock?: boolean
|
|
654
|
+
}): Promise<void> {
|
|
655
|
+
if (params.facts.length === 0) return
|
|
656
|
+
|
|
657
|
+
const orgMemory = this.getOrgMemory(params.orgId)
|
|
658
|
+
const scopes: AddOptions[] = [
|
|
659
|
+
{
|
|
660
|
+
scopeId: scopeId(ORG_SCOPE_PREFIX, params.orgId),
|
|
661
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
662
|
+
metadata: { orgId: params.orgId, source: params.source, ...params.sourceMetadata },
|
|
663
|
+
},
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
for (const scopedAgentName of this.resolveAgentScopeNames(undefined, params.agentNames ?? [])) {
|
|
667
|
+
scopes.push({
|
|
668
|
+
scopeId: agentScopeId(params.orgId, scopedAgentName),
|
|
669
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
670
|
+
metadata: {
|
|
671
|
+
orgId: params.orgId,
|
|
672
|
+
source: params.source,
|
|
673
|
+
...params.sourceMetadata,
|
|
674
|
+
agentName: scopedAgentName,
|
|
675
|
+
memoryScope: 'agent',
|
|
676
|
+
},
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const preparedUpdates = await orgMemory.prepareFactsToScopes(params.facts, scopes)
|
|
681
|
+
if (preparedUpdates.length === 0) return
|
|
682
|
+
|
|
683
|
+
if (params.acquireLock === false) {
|
|
684
|
+
await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
|
|
685
|
+
} else {
|
|
686
|
+
await withOrgMemoryLock(params.orgId, async () => {
|
|
687
|
+
await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async addConversationMemories({
|
|
693
|
+
orgId,
|
|
694
|
+
input,
|
|
695
|
+
output,
|
|
696
|
+
sourceId,
|
|
697
|
+
onboardStatus,
|
|
698
|
+
agentName,
|
|
699
|
+
historyMessages = [],
|
|
700
|
+
memoryBlock,
|
|
701
|
+
attachmentContext,
|
|
702
|
+
agentNames = [],
|
|
703
|
+
}: {
|
|
704
|
+
orgId: string
|
|
705
|
+
input: string
|
|
706
|
+
output: string
|
|
707
|
+
sourceId?: string
|
|
708
|
+
onboardStatus?: string
|
|
709
|
+
agentName?: string
|
|
710
|
+
historyMessages?: Array<{ role: 'user' | 'agent'; content: string; agentName?: string }>
|
|
711
|
+
memoryBlock?: string
|
|
712
|
+
attachmentContext?: string
|
|
713
|
+
agentNames?: string[]
|
|
714
|
+
}): Promise<void> {
|
|
715
|
+
const { messages, normalizedInput, sanitizedOutput } = this.buildConversationMessages({
|
|
716
|
+
input,
|
|
717
|
+
output,
|
|
718
|
+
historyMessages,
|
|
719
|
+
memoryBlock,
|
|
720
|
+
attachmentContext,
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
if (!normalizedInput || !sanitizedOutput || messages.length === 0) {
|
|
724
|
+
aiLogger.debug`Skipping memory add - empty input or output`
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
729
|
+
aiLogger.info`[MEMORY_DEBUG] addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
|
|
730
|
+
|
|
731
|
+
const orgMemory = this.getOrgMemory(orgId)
|
|
732
|
+
let assessedImportance: number | undefined
|
|
733
|
+
let assessmentMetadata: Record<string, unknown> | undefined
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const assessmentInput = this.buildConversationAssessmentInput(messages)
|
|
737
|
+
if (assessmentInput) {
|
|
738
|
+
const assessment = await assessMemoryImportance({
|
|
739
|
+
content: assessmentInput,
|
|
740
|
+
targetScope: 'global',
|
|
741
|
+
tag: 'memory-conversation-assessment',
|
|
742
|
+
})
|
|
743
|
+
if (assessment.classification === 'transient' && assessment.durability === 'ephemeral') {
|
|
744
|
+
aiLogger.debug`Skipping transient conversation memory`
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
assessedImportance = Math.round(clampMemoryImportance(assessment.importance) * 100) / 100
|
|
748
|
+
assessmentMetadata = {
|
|
749
|
+
importanceSource: 'model_assessed',
|
|
750
|
+
durability: assessment.durability,
|
|
751
|
+
classification: assessment.classification,
|
|
752
|
+
rationale: assessment.rationale,
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} catch (error) {
|
|
756
|
+
aiLogger.warn`Conversation memory assessment failed; continuing with default scoring: ${error}`
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const scopes: AddOptions[] = [
|
|
760
|
+
{
|
|
761
|
+
scopeId: orgScopeId,
|
|
762
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
763
|
+
...(typeof assessedImportance === 'number' ? { importance: assessedImportance } : {}),
|
|
764
|
+
metadata: { orgId, source: 'chat', ...(sourceId ? { sourceId } : {}), ...assessmentMetadata },
|
|
765
|
+
},
|
|
766
|
+
]
|
|
767
|
+
|
|
768
|
+
for (const scopedAgentName of this.resolveAgentScopeNames(agentName, agentNames)) {
|
|
769
|
+
const agentId = agentScopeId(orgId, scopedAgentName)
|
|
770
|
+
scopes.push({
|
|
771
|
+
scopeId: agentId,
|
|
772
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
773
|
+
...(typeof assessedImportance === 'number' ? { importance: assessedImportance } : {}),
|
|
774
|
+
metadata: {
|
|
775
|
+
orgId,
|
|
776
|
+
agentName: scopedAgentName,
|
|
777
|
+
memoryScope: 'agent',
|
|
778
|
+
source: 'chat',
|
|
779
|
+
...(sourceId ? { sourceId } : {}),
|
|
780
|
+
...assessmentMetadata,
|
|
781
|
+
},
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const onboardingActive = onboardStatus !== undefined && onboardStatus !== 'completed'
|
|
786
|
+
const extractionConfig = onboardingActive
|
|
787
|
+
? { maxFacts: ONBOARDING_MEMORY_MAX_FACTS, customPrompt: ONBOARDING_MEMORY_EXTRACTION_PROMPT }
|
|
788
|
+
: undefined
|
|
789
|
+
const extractedFacts = await orgMemory.extractFactsFromMessages(messages, extractionConfig)
|
|
790
|
+
if (extractedFacts.length === 0) return
|
|
791
|
+
const preparedUpdates = await orgMemory.prepareFactsToScopes(extractedFacts, scopes)
|
|
792
|
+
if (preparedUpdates.length === 0) return
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
await withOrgMemoryLock(orgId, async () => {
|
|
796
|
+
await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
|
|
797
|
+
})
|
|
798
|
+
aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
|
|
799
|
+
} catch (error: unknown) {
|
|
800
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
801
|
+
aiLogger.error`Memory write failed: ${normalizedError}`
|
|
802
|
+
throw normalizedError
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export const memoryService = new MemoryService()
|