@lota-sdk/core 0.1.13 → 0.1.15
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/package.json +5 -5
- package/src/ai/embedding-cache.ts +7 -6
- package/src/ai/index.ts +1 -0
- package/src/bifrost/bifrost.ts +12 -7
- package/src/config/agent-defaults.ts +1 -1
- package/src/config/logger.ts +7 -9
- package/src/{runtime.ts → create-runtime.ts} +6 -6
- package/src/db/cursor-pagination.ts +1 -1
- package/src/db/memory-store.ts +10 -6
- package/src/db/memory.ts +6 -4
- package/src/db/schema-fingerprint.ts +1 -0
- package/src/db/service.ts +45 -51
- package/src/db/startup.ts +3 -3
- package/src/index.ts +1 -1
- package/src/queues/context-compaction.queue.ts +4 -8
- package/src/queues/document-processor.queue.ts +7 -7
- package/src/queues/memory-consolidation.queue.ts +7 -8
- package/src/queues/post-chat-memory.queue.ts +2 -6
- package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
- package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
- package/src/queues/skill-extraction.queue.ts +4 -7
- package/src/queues/workstream-title-generation.queue.ts +2 -6
- package/src/redis/connection.ts +6 -3
- package/src/redis/index.ts +1 -0
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +41 -8
- package/src/runtime/agent-stream-helpers.ts +2 -1
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction-runtime.ts +6 -4
- package/src/runtime/context-compaction.ts +19 -38
- package/src/runtime/execution-plan.ts +2 -2
- package/src/runtime/helper-model.ts +3 -1
- package/src/runtime/index.ts +12 -1
- package/src/runtime/memory-block.ts +3 -2
- package/src/runtime/memory-pipeline.ts +24 -5
- package/src/runtime/plugin-types.ts +1 -1
- package/src/runtime/runtime-extensions.ts +89 -13
- package/src/runtime/title-helpers.ts +11 -2
- package/src/runtime/workstream-chat-helpers.ts +5 -6
- package/src/runtime/workstream-routing-policy.ts +0 -30
- package/src/runtime/workstream-state.ts +17 -7
- package/src/services/attachment.service.ts +1 -1
- package/src/services/context-compaction.service.ts +3 -3
- package/src/services/document-chunk.service.ts +37 -32
- package/src/services/execution-plan.service.ts +2 -0
- package/src/services/learned-skill.service.ts +6 -10
- package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
- package/src/services/memory.service.ts +21 -18
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/plan-artifact.service.ts +1 -0
- package/src/services/plan-executor.service.ts +2 -18
- package/src/services/plan-helpers.ts +15 -0
- package/src/services/plan-validator.service.ts +3 -18
- package/src/services/recent-activity-title.service.ts +3 -10
- package/src/services/recent-activity.service.ts +6 -12
- package/src/services/workstream-message.service.ts +26 -16
- package/src/services/workstream-title.service.ts +1 -9
- package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
- package/src/services/workstream-turn.ts +2 -2
- package/src/services/workstream.service.ts +22 -10
- package/src/services/workstream.types.ts +7 -16
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
- package/src/storage/index.ts +2 -2
- package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
- package/src/system-agents/delegated-agent-factory.ts +3 -2
- package/src/system-agents/index.ts +8 -0
- package/src/system-agents/memory-reranker.agent.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +6 -2
- package/src/tools/fetch-webpage.tool.ts +20 -18
- package/src/tools/index.ts +2 -2
- package/src/tools/read-file-parts.tool.ts +1 -1
- package/src/tools/search-web.tool.ts +18 -15
- package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
- package/src/tools/team-think.tool.ts +9 -5
- package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
- package/src/utils/async.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/hono-error-handler.ts +1 -2
- package/src/utils/index.ts +10 -2
- package/src/utils/string.ts +14 -0
- package/src/workers/bootstrap.ts +2 -2
- package/src/workers/memory-consolidation.worker.ts +12 -12
- package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
- package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
- package/src/workers/skill-extraction.runner.ts +7 -101
- package/src/workers/utils/file-section-chunker.ts +5 -3
- package/src/workers/utils/workstream-message-query.ts +106 -0
- package/src/workers/worker-utils.ts +4 -0
- package/src/runtime/retrieval-pipeline.ts +0 -3
- package/src/utils/error.ts +0 -10
- /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
- /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
|
@@ -36,36 +36,6 @@ export function uniqueMentionOrder(message: string): string[] {
|
|
|
36
36
|
return ordered
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const GTM_STRONG_INTENT_PATTERNS: RegExp[] = [
|
|
40
|
-
/\bgo[-\s]?to[-\s]?market\b/i,
|
|
41
|
-
/\bgtm\b/i,
|
|
42
|
-
/\bcontent\s+marketing\b/i,
|
|
43
|
-
/\bcontent\s+strategy\b/i,
|
|
44
|
-
/\bdemand\s+generation\b/i,
|
|
45
|
-
/\bdistribution\s+strategy\b/i,
|
|
46
|
-
/\bpositioning\s+strategy\b/i,
|
|
47
|
-
/\blaunch\s+strategy\b/i,
|
|
48
|
-
/\blaunch\s+plan\b/i,
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
const GTM_WEAK_INTENT_PATTERNS: RegExp[] = [/\blaunch\b/i, /\bcommunity\b/i, /\bdistribution\b/i, /\bpositioning\b/i]
|
|
52
|
-
|
|
53
|
-
export function isGtmIntentMessage(message: string): boolean {
|
|
54
|
-
const text = message.trim()
|
|
55
|
-
if (!text) return false
|
|
56
|
-
|
|
57
|
-
if (GTM_STRONG_INTENT_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
58
|
-
return true
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let weakMatches = 0
|
|
62
|
-
for (const pattern of GTM_WEAK_INTENT_PATTERNS) {
|
|
63
|
-
if (pattern.test(text)) weakMatches += 1
|
|
64
|
-
if (weakMatches >= 2) return true
|
|
65
|
-
}
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
68
|
-
|
|
69
39
|
const HIGH_IMPACT_CLASS_PATTERNS: Array<{ className: HighImpactResponseClass; patterns: RegExp[] }> = [
|
|
70
40
|
{
|
|
71
41
|
className: 'architecture-recommendation',
|
|
@@ -238,13 +238,23 @@ export interface CompactionOutput {
|
|
|
238
238
|
stateDelta: WorkstreamStateDelta
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
const WORKSTREAM_STATE_MAX_KEY_DECISIONS = 8
|
|
242
|
+
const WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS = 6
|
|
243
|
+
const WORKSTREAM_STATE_MAX_TASKS = 10
|
|
244
|
+
const WORKSTREAM_STATE_MAX_OPEN_QUESTIONS = 5
|
|
245
|
+
const WORKSTREAM_STATE_MAX_RISKS = 5
|
|
246
|
+
const WORKSTREAM_STATE_MAX_ARTIFACTS = 10
|
|
247
|
+
const WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS = 6
|
|
248
|
+
|
|
249
|
+
export function applyWorkstreamStateCaps(state: WorkstreamState): void {
|
|
250
|
+
state.keyDecisions = state.keyDecisions.slice(-WORKSTREAM_STATE_MAX_KEY_DECISIONS)
|
|
251
|
+
state.activeConstraints = state.activeConstraints.slice(-WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS)
|
|
252
|
+
state.tasks = state.tasks.slice(-WORKSTREAM_STATE_MAX_TASKS)
|
|
253
|
+
state.openQuestions = state.openQuestions.slice(-WORKSTREAM_STATE_MAX_OPEN_QUESTIONS)
|
|
254
|
+
state.risks = state.risks.slice(-WORKSTREAM_STATE_MAX_RISKS)
|
|
255
|
+
state.artifacts = state.artifacts.slice(-WORKSTREAM_STATE_MAX_ARTIFACTS)
|
|
256
|
+
state.agentContributions = state.agentContributions.slice(-WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS)
|
|
257
|
+
}
|
|
248
258
|
|
|
249
259
|
export function createEmptyWorkstreamState(now = Date.now()): WorkstreamState {
|
|
250
260
|
return {
|
|
@@ -3,7 +3,7 @@ import { recordIdToString } from '../db/record-id'
|
|
|
3
3
|
import { TABLES } from '../db/tables'
|
|
4
4
|
import { attachmentStorageService } from '../storage/attachment-storage.service'
|
|
5
5
|
import type { UploadedWorkstreamAttachment as SdkUploadedWorkstreamAttachment } from '../storage/attachment-storage.service'
|
|
6
|
-
import type { MessagePartLike, ReadableUploadMetadata as SdkReadableUploadMetadata } from '../storage/
|
|
6
|
+
import type { MessagePartLike, ReadableUploadMetadata as SdkReadableUploadMetadata } from '../storage/attachment-types'
|
|
7
7
|
|
|
8
8
|
export type ReadableUploadMetadata = SdkReadableUploadMetadata
|
|
9
9
|
|
|
@@ -6,9 +6,9 @@ import { recordIdToString } from '../db/record-id'
|
|
|
6
6
|
import { databaseService } from '../db/service'
|
|
7
7
|
import { TABLES } from '../db/tables'
|
|
8
8
|
import { parseWorkstreamState, toStateFieldsUpdated } from '../runtime/context-compaction'
|
|
9
|
-
import {
|
|
9
|
+
import { CONTEXT_WINDOW_TOKENS, WORKSTREAM_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
|
|
10
10
|
import type { WorkstreamState } from '../runtime/workstream-state'
|
|
11
|
-
import { compactMemoryBlockSummary, contextCompactionRuntime } from './context-compaction-runtime'
|
|
11
|
+
import { compactMemoryBlockSummary, contextCompactionRuntime } from './context-compaction-runtime.singleton'
|
|
12
12
|
import { workstreamMessageService } from './workstream-message.service'
|
|
13
13
|
import { WorkstreamSchema } from './workstream.types'
|
|
14
14
|
|
|
@@ -35,7 +35,7 @@ class ContextCompactionService {
|
|
|
35
35
|
return contextCompactionRuntime.formatWorkstreamStateForPrompt(state)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
estimateThreshold(contextSize =
|
|
38
|
+
estimateThreshold(contextSize = CONTEXT_WINDOW_TOKENS): number {
|
|
39
39
|
return contextCompactionRuntime.estimateThreshold(contextSize)
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto'
|
|
|
3
3
|
import { chunkMarkdownDocument, chunkPagedDocument, chunkPlainTextDocument } from '../document/org-document-chunking'
|
|
4
4
|
import type { ParsedDocumentChunk } from '../document/org-document-chunking'
|
|
5
5
|
import { getDefaultEmbeddings } from '../embeddings/provider'
|
|
6
|
+
import { CHARS_PER_TOKEN_ESTIMATE } from '../utils/string'
|
|
6
7
|
|
|
7
8
|
type DocumentChunkEmbeddings = {
|
|
8
9
|
embedDocuments(documents: string[]): Promise<number[][]>
|
|
@@ -58,8 +59,10 @@ export class DocumentChunkService {
|
|
|
58
59
|
return createHash('sha256').update(content).digest('hex')
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
// Uses 4 chars/token (conservative estimate for document content which tends
|
|
63
|
+
// to have longer words than conversational text where 3 chars/token is used).
|
|
61
64
|
estimateTokenCount(content: string): number {
|
|
62
|
-
return Math.max(1, Math.ceil(content.length /
|
|
65
|
+
return Math.max(1, Math.ceil(content.length / (CHARS_PER_TOKEN_ESTIMATE + 1)))
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
async embedQuery(query: string): Promise<number[]> {
|
|
@@ -95,37 +98,39 @@ export class DocumentChunkService {
|
|
|
95
98
|
|
|
96
99
|
await params.archive(staleVersionRows)
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
await Promise.all(
|
|
102
|
+
params.chunks.map(async (chunk, index) => {
|
|
103
|
+
const contentHash = this.hashContent(chunk.content)
|
|
104
|
+
const existingRow = existingByChunkKey.get(chunk.chunkKey)
|
|
105
|
+
const payload = params.buildPayload({
|
|
106
|
+
chunk,
|
|
107
|
+
embedding: embeddings[index] ?? [],
|
|
108
|
+
contentHash,
|
|
109
|
+
tokenEstimate: this.estimateTokenCount(chunk.content),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
seenChunkKeys.add(chunk.chunkKey)
|
|
113
|
+
|
|
114
|
+
if (!existingRow) {
|
|
115
|
+
await params.create(payload)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const current = params.selectShape(existingRow)
|
|
120
|
+
const hasChanged =
|
|
121
|
+
current.contentHash !== contentHash ||
|
|
122
|
+
current.chunkIndex !== chunk.chunkIndex ||
|
|
123
|
+
(current.sectionPath ?? null) !== (chunk.sectionPath ?? null) ||
|
|
124
|
+
(current.pageStart ?? null) !== (chunk.pageStart ?? null) ||
|
|
125
|
+
(current.pageEnd ?? null) !== (chunk.pageEnd ?? null)
|
|
126
|
+
|
|
127
|
+
if (!hasChanged) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await params.update(existingRow, payload)
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
129
134
|
|
|
130
135
|
const removedCurrentVersionRows = existingRows.filter((row) => {
|
|
131
136
|
const current = params.selectShape(row)
|
|
@@ -612,6 +612,7 @@ class ExecutionPlanService {
|
|
|
612
612
|
): Promise<PlanNodeSpecRecord[]> {
|
|
613
613
|
const createdRecords: PlanNodeSpecRecord[] = []
|
|
614
614
|
|
|
615
|
+
// Sequential: SurrealDB transactions require ordered operations
|
|
615
616
|
for (const compiledNode of nodes) {
|
|
616
617
|
const nodeSpecId = new RecordId(TABLES.PLAN_NODE_SPEC, Bun.randomUUIDv7())
|
|
617
618
|
const created = await tx
|
|
@@ -657,6 +658,7 @@ class ExecutionPlanService {
|
|
|
657
658
|
nodeSpecs: PlanNodeSpecRecord[],
|
|
658
659
|
): Promise<PlanNodeRunRecord[]> {
|
|
659
660
|
const createdNodeRuns: PlanNodeRunRecord[] = []
|
|
661
|
+
// Sequential: SurrealDB transactions require ordered operations
|
|
660
662
|
for (const nodeSpec of nodeSpecs) {
|
|
661
663
|
const nodeRunId = new RecordId(TABLES.PLAN_NODE_RUN, Bun.randomUUIDv7())
|
|
662
664
|
const created = await tx
|
|
@@ -16,6 +16,7 @@ const embeddings = getDefaultEmbeddings()
|
|
|
16
16
|
const PROMOTION_MIN_USES = 5
|
|
17
17
|
const PROMOTION_MIN_SUCCESS_RATE = 0.6
|
|
18
18
|
|
|
19
|
+
const ACTIVE_SKILL_FILTER = "AND status IN ['learned', 'verified', 'promoted'] AND archivedAt IS NONE"
|
|
19
20
|
const SKILL_EXISTS_TTL_SECONDS = 120
|
|
20
21
|
const SKILL_EXISTS_KEY_PREFIX = 'skill-exists'
|
|
21
22
|
|
|
@@ -176,8 +177,7 @@ class LearnedSkillService {
|
|
|
176
177
|
new BoundQuery(
|
|
177
178
|
`SELECT id FROM ${TABLES.LEARNED_SKILL}
|
|
178
179
|
WHERE organizationId = $orgRef
|
|
179
|
-
|
|
180
|
-
AND archivedAt IS NONE
|
|
180
|
+
${ACTIVE_SKILL_FILTER}
|
|
181
181
|
AND (agentId IS NONE OR agentId = $agentId)
|
|
182
182
|
LIMIT 1`,
|
|
183
183
|
{ orgRef, agentId },
|
|
@@ -227,8 +227,7 @@ class LearnedSkillService {
|
|
|
227
227
|
vector::similarity::cosine(embedding, $embedding) AS similarity
|
|
228
228
|
FROM ${TABLES.LEARNED_SKILL}
|
|
229
229
|
WHERE organizationId = $organizationId
|
|
230
|
-
|
|
231
|
-
AND archivedAt IS NONE
|
|
230
|
+
${ACTIVE_SKILL_FILTER}
|
|
232
231
|
AND confidence >= $minConfidence
|
|
233
232
|
AND (agentId IS NONE OR agentId = $agentId)
|
|
234
233
|
AND embedding <|${params.limit}|> $embedding
|
|
@@ -311,8 +310,7 @@ class LearnedSkillService {
|
|
|
311
310
|
vector::similarity::cosine(embedding, $embedding) AS similarity
|
|
312
311
|
FROM ${TABLES.LEARNED_SKILL}
|
|
313
312
|
WHERE organizationId = $organizationId
|
|
314
|
-
|
|
315
|
-
AND archivedAt IS NONE
|
|
313
|
+
${ACTIVE_SKILL_FILTER}
|
|
316
314
|
AND embedding <|3|> $embedding
|
|
317
315
|
ORDER BY similarity DESC
|
|
318
316
|
LIMIT 1
|
|
@@ -333,8 +331,7 @@ class LearnedSkillService {
|
|
|
333
331
|
`SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
|
|
334
332
|
FROM ${TABLES.LEARNED_SKILL}
|
|
335
333
|
WHERE organizationId = $organizationId
|
|
336
|
-
|
|
337
|
-
AND archivedAt IS NONE
|
|
334
|
+
${ACTIVE_SKILL_FILTER}
|
|
338
335
|
ORDER BY createdAt DESC`,
|
|
339
336
|
{ organizationId: orgRef },
|
|
340
337
|
),
|
|
@@ -357,8 +354,7 @@ class LearnedSkillService {
|
|
|
357
354
|
FROM ${TABLES.LEARNED_SKILL}
|
|
358
355
|
WHERE organizationId = $organizationId
|
|
359
356
|
AND hash = $hash
|
|
360
|
-
|
|
361
|
-
AND archivedAt IS NONE
|
|
357
|
+
${ACTIVE_SKILL_FILTER}
|
|
362
358
|
LIMIT 1`,
|
|
363
359
|
{ organizationId: orgRef, hash },
|
|
364
360
|
),
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { MEMORY } from '../config/constants'
|
|
2
2
|
import { VECTOR_SEARCH_OVERFETCH_MULTIPLIER } from '../config/search'
|
|
3
3
|
import type { MemorySearchResult } from '../db/memory-types'
|
|
4
|
-
import {
|
|
4
|
+
import { compactWhitespace } from '../utils/string'
|
|
5
5
|
import type { MemoryRerankOutput } from './memory.service'
|
|
6
6
|
|
|
7
7
|
export function getCandidateLimit(limit: number): number {
|
|
8
|
-
return
|
|
9
|
-
limit,
|
|
10
|
-
multiplier: VECTOR_SEARCH_OVERFETCH_MULTIPLIER,
|
|
11
|
-
minimum: MEMORY.DEFAULT_CANDIDATE_LIMIT,
|
|
12
|
-
})
|
|
8
|
+
return Math.max(limit * VECTOR_SEARCH_OVERFETCH_MULTIPLIER, MEMORY.DEFAULT_CANDIDATE_LIMIT)
|
|
13
9
|
}
|
|
14
10
|
|
|
15
11
|
export function formatMemoryResults(results: MemorySearchResult[]): string {
|
|
16
12
|
if (results.length === 0) return 'No stored memories.'
|
|
17
13
|
|
|
18
14
|
const normalize = (value: string) => {
|
|
19
|
-
const trimmed = value
|
|
15
|
+
const trimmed = compactWhitespace(value)
|
|
20
16
|
if (trimmed.length <= 400) return trimmed
|
|
21
17
|
return `${trimmed.slice(0, 400)}...`
|
|
22
18
|
}
|
|
@@ -65,7 +61,7 @@ export function formatRerankedResults(
|
|
|
65
61
|
used.add(candidate.id)
|
|
66
62
|
total += 1
|
|
67
63
|
const reason = item.relevance ? ` — ${item.relevance}` : ''
|
|
68
|
-
const trimmed = candidate.content
|
|
64
|
+
const trimmed = compactWhitespace(candidate.content)
|
|
69
65
|
const content = trimmed.length <= 400 ? trimmed : `${trimmed.slice(0, 400)}...`
|
|
70
66
|
lines.push(`- ${content}${reason}`)
|
|
71
67
|
if (total >= limit) break
|
|
@@ -24,10 +24,12 @@ import {
|
|
|
24
24
|
scopedRetrievalToMap,
|
|
25
25
|
} from '../runtime/retrieval-adapters'
|
|
26
26
|
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
27
|
-
import { createMemoryRerankerAgent,
|
|
28
|
-
import { createOrgMemoryAgent,
|
|
27
|
+
import { createMemoryRerankerAgent, MEMORY_RERANKER_PROMPT } from '../system-agents/memory-reranker.agent'
|
|
28
|
+
import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../system-agents/memory.agent'
|
|
29
|
+
import { toError } from '../utils/errors'
|
|
30
|
+
import { compactWhitespace } from '../utils/string'
|
|
29
31
|
import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
|
|
30
|
-
import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory
|
|
32
|
+
import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory-utils'
|
|
31
33
|
|
|
32
34
|
const ORG_MEMORY_TYPE = 'fact'
|
|
33
35
|
const RERANK_CANDIDATE_MAX_CHARS = 500
|
|
@@ -72,7 +74,7 @@ class MemoryService {
|
|
|
72
74
|
const cached = cache.get(cacheKey)
|
|
73
75
|
if (cached) return cached
|
|
74
76
|
|
|
75
|
-
const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt:
|
|
77
|
+
const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: ORG_MEMORY_PROMPT })
|
|
76
78
|
|
|
77
79
|
cache.set(cacheKey, memory)
|
|
78
80
|
aiLogger.debug`Memory client created and cached for ${cacheKey}`
|
|
@@ -85,7 +87,7 @@ class MemoryService {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
private truncateCandidateText(value: string): string {
|
|
88
|
-
const normalized = value
|
|
90
|
+
const normalized = compactWhitespace(value)
|
|
89
91
|
if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
|
|
90
92
|
return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
|
|
91
93
|
}
|
|
@@ -125,7 +127,7 @@ class MemoryService {
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
private normalizeConversationText(value: string, maxChars: number): string {
|
|
128
|
-
const normalized = value
|
|
130
|
+
const normalized = compactWhitespace(value)
|
|
129
131
|
if (!normalized) return ''
|
|
130
132
|
if (normalized.length <= maxChars) return normalized
|
|
131
133
|
return `${normalized.slice(0, maxChars - 3)}...`
|
|
@@ -146,7 +148,7 @@ class MemoryService {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
private normalizePreSeededMemoryText(value: string): string {
|
|
149
|
-
const normalized = value
|
|
151
|
+
const normalized = compactWhitespace(value)
|
|
150
152
|
if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
|
|
151
153
|
return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
|
|
152
154
|
}
|
|
@@ -232,7 +234,7 @@ class MemoryService {
|
|
|
232
234
|
return await helperModelRuntime.generateHelperStructured({
|
|
233
235
|
tag: 'memory-reranker',
|
|
234
236
|
createAgent: createMemoryRerankerAgent,
|
|
235
|
-
defaultSystemPrompt:
|
|
237
|
+
defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
|
|
236
238
|
messages: [
|
|
237
239
|
{
|
|
238
240
|
role: 'user',
|
|
@@ -269,7 +271,7 @@ class MemoryService {
|
|
|
269
271
|
return await helperModelRuntime.generateHelperStructured({
|
|
270
272
|
tag: 'memory-reranker-multi-scope',
|
|
271
273
|
createAgent: createMemoryRerankerAgent,
|
|
272
|
-
defaultSystemPrompt:
|
|
274
|
+
defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
|
|
273
275
|
messages: [
|
|
274
276
|
{
|
|
275
277
|
role: 'user',
|
|
@@ -334,7 +336,7 @@ class MemoryService {
|
|
|
334
336
|
aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
335
337
|
return results
|
|
336
338
|
} catch (error: unknown) {
|
|
337
|
-
const normalizedError =
|
|
339
|
+
const normalizedError = toError(error)
|
|
338
340
|
aiLogger.error`Organization memory search failed: ${normalizedError}`
|
|
339
341
|
throw normalizedError
|
|
340
342
|
}
|
|
@@ -348,7 +350,8 @@ class MemoryService {
|
|
|
348
350
|
if (stale.length === 0) return ''
|
|
349
351
|
const items = stale.map((m) => `- [NEEDS REVIEW] ${m.content}`).join('\n')
|
|
350
352
|
return `Memories flagged for review (parent fact was superseded):\n${items}`
|
|
351
|
-
} catch {
|
|
353
|
+
} catch (error) {
|
|
354
|
+
aiLogger.warn`Failed to get stale memories: ${error}`
|
|
352
355
|
return ''
|
|
353
356
|
}
|
|
354
357
|
}
|
|
@@ -359,7 +362,7 @@ class MemoryService {
|
|
|
359
362
|
options?: { fastMode?: boolean; limit?: number },
|
|
360
363
|
): Promise<string> {
|
|
361
364
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
362
|
-
aiLogger.
|
|
365
|
+
aiLogger.debug`searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
|
|
363
366
|
const memory = this.getOrgMemory(orgId)
|
|
364
367
|
const fastMode = options?.fastMode ?? true
|
|
365
368
|
const searchK = getRuntimeConfig().memory.searchK
|
|
@@ -376,7 +379,7 @@ class MemoryService {
|
|
|
376
379
|
aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
|
|
377
380
|
return formatMemoryResults(candidates)
|
|
378
381
|
} catch (error: unknown) {
|
|
379
|
-
const normalizedError =
|
|
382
|
+
const normalizedError = toError(error)
|
|
380
383
|
aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
|
|
381
384
|
throw normalizedError
|
|
382
385
|
}
|
|
@@ -397,7 +400,7 @@ class MemoryService {
|
|
|
397
400
|
aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
398
401
|
return results
|
|
399
402
|
} catch (error: unknown) {
|
|
400
|
-
const normalizedError =
|
|
403
|
+
const normalizedError = toError(error)
|
|
401
404
|
aiLogger.error`Agent memory search failed: ${normalizedError}`
|
|
402
405
|
throw normalizedError
|
|
403
406
|
}
|
|
@@ -467,7 +470,7 @@ class MemoryService {
|
|
|
467
470
|
const deduped: MemoryRecord[] = []
|
|
468
471
|
const seen = new Set<string>()
|
|
469
472
|
for (const memory of combined) {
|
|
470
|
-
const normalizedKey = memory.content
|
|
473
|
+
const normalizedKey = compactWhitespace(memory.content).toLowerCase()
|
|
471
474
|
if (!normalizedKey || seen.has(normalizedKey)) continue
|
|
472
475
|
seen.add(normalizedKey)
|
|
473
476
|
deduped.push(memory)
|
|
@@ -594,7 +597,7 @@ class MemoryService {
|
|
|
594
597
|
durability?: MemoryRecord['durability']
|
|
595
598
|
}): Promise<string> {
|
|
596
599
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
597
|
-
aiLogger.
|
|
600
|
+
aiLogger.debug`createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
|
|
598
601
|
const memory = this.getOrgMemory(orgId)
|
|
599
602
|
try {
|
|
600
603
|
return await memory.insert(content, {
|
|
@@ -762,7 +765,7 @@ class MemoryService {
|
|
|
762
765
|
}
|
|
763
766
|
|
|
764
767
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
765
|
-
aiLogger.
|
|
768
|
+
aiLogger.debug`addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
|
|
766
769
|
|
|
767
770
|
const orgMemory = this.getOrgMemory(orgId)
|
|
768
771
|
let assessedImportance: number | undefined
|
|
@@ -833,7 +836,7 @@ class MemoryService {
|
|
|
833
836
|
})
|
|
834
837
|
aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
|
|
835
838
|
} catch (error: unknown) {
|
|
836
|
-
const normalizedError =
|
|
839
|
+
const normalizedError = toError(error)
|
|
837
840
|
aiLogger.error`Memory write failed: ${normalizedError}`
|
|
838
841
|
throw normalizedError
|
|
839
842
|
}
|
|
@@ -23,7 +23,7 @@ const sdkOrganizationMemberSchema = z.object({
|
|
|
23
23
|
createdAt: z.iso.datetime(),
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
type SdkOrganizationMemberRecord = z.infer<typeof organizationMemberRecordSchema>
|
|
27
27
|
export type SdkOrganizationMember = z.infer<typeof sdkOrganizationMemberSchema>
|
|
28
28
|
|
|
29
29
|
class OrganizationMemberService extends BaseService<typeof organizationMemberRecordSchema> {
|
|
@@ -17,6 +17,7 @@ class PlanArtifactService {
|
|
|
17
17
|
}): Promise<PlanArtifactRecord[]> {
|
|
18
18
|
const records: PlanArtifactRecord[] = []
|
|
19
19
|
|
|
20
|
+
// Sequential: SurrealDB transactions require ordered operations
|
|
20
21
|
for (const artifact of params.artifacts) {
|
|
21
22
|
const artifactId = new RecordId(TABLES.PLAN_ARTIFACT, Bun.randomUUIDv7())
|
|
22
23
|
const created = await params.tx
|
|
@@ -26,9 +26,11 @@ import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
|
26
26
|
import { databaseService } from '../db/service'
|
|
27
27
|
import type { DatabaseTransaction } from '../db/service'
|
|
28
28
|
import { TABLES } from '../db/tables'
|
|
29
|
+
import { isRecord } from '../utils/string'
|
|
29
30
|
import { planApprovalService } from './plan-approval.service'
|
|
30
31
|
import { planArtifactService } from './plan-artifact.service'
|
|
31
32
|
import { planCheckpointService } from './plan-checkpoint.service'
|
|
33
|
+
import { readPathValue } from './plan-helpers'
|
|
32
34
|
import { planRunService } from './plan-run.service'
|
|
33
35
|
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
34
36
|
import { planValidatorService } from './plan-validator.service'
|
|
@@ -37,24 +39,6 @@ const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skip
|
|
|
37
39
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
38
40
|
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
|
|
39
41
|
|
|
40
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readPathValue(source: unknown, path: string): unknown {
|
|
45
|
-
if (!path.trim()) return source
|
|
46
|
-
|
|
47
|
-
let current: unknown = source
|
|
48
|
-
for (const segment of path
|
|
49
|
-
.split('.')
|
|
50
|
-
.map((part) => part.trim())
|
|
51
|
-
.filter(Boolean)) {
|
|
52
|
-
if (!isRecord(current)) return undefined
|
|
53
|
-
current = current[segment]
|
|
54
|
-
}
|
|
55
|
-
return current
|
|
56
|
-
}
|
|
57
|
-
|
|
58
42
|
function setPathValue(target: Record<string, unknown>, path: string, value: unknown) {
|
|
59
43
|
const segments = path
|
|
60
44
|
.split('.')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isRecord } from '../utils/string'
|
|
2
|
+
|
|
3
|
+
export function readPathValue(source: unknown, path: string): unknown {
|
|
4
|
+
if (!path.trim()) return source
|
|
5
|
+
|
|
6
|
+
let current: unknown = source
|
|
7
|
+
for (const segment of path
|
|
8
|
+
.split('.')
|
|
9
|
+
.map((part) => part.trim())
|
|
10
|
+
.filter(Boolean)) {
|
|
11
|
+
if (!isRecord(current)) return undefined
|
|
12
|
+
current = current[segment]
|
|
13
|
+
}
|
|
14
|
+
return current
|
|
15
|
+
}
|
|
@@ -8,6 +8,9 @@ import type {
|
|
|
8
8
|
PlanValidationIssueSeverity,
|
|
9
9
|
} from '@lota-sdk/shared'
|
|
10
10
|
|
|
11
|
+
import { isRecord } from '../utils/string'
|
|
12
|
+
import { readPathValue } from './plan-helpers'
|
|
13
|
+
|
|
11
14
|
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
|
|
12
15
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
13
16
|
|
|
@@ -46,24 +49,6 @@ function createIssue(params: {
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function readPathValue(source: unknown, path: string): unknown {
|
|
54
|
-
if (!path.trim()) return source
|
|
55
|
-
|
|
56
|
-
let current: unknown = source
|
|
57
|
-
for (const segment of path
|
|
58
|
-
.split('.')
|
|
59
|
-
.map((part) => part.trim())
|
|
60
|
-
.filter(Boolean)) {
|
|
61
|
-
if (!isRecord(current)) return undefined
|
|
62
|
-
current = current[segment]
|
|
63
|
-
}
|
|
64
|
-
return current
|
|
65
|
-
}
|
|
66
|
-
|
|
67
52
|
function hasAllFields(value: unknown, fields: string[]): boolean {
|
|
68
53
|
if (!isRecord(value)) return false
|
|
69
54
|
return fields.every((field) => readPathValue(value, field) !== undefined)
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
2
|
+
import { normalizeTitle } from '../runtime/title-helpers'
|
|
2
3
|
import {
|
|
3
4
|
createRecentActivityTitleRefinerAgent,
|
|
4
|
-
|
|
5
|
+
RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
|
|
5
6
|
} from '../system-agents/recent-activity-title-refiner.agent'
|
|
6
|
-
import { compactWhitespace } from '../utils/string'
|
|
7
7
|
import { recentActivityService } from './recent-activity.service'
|
|
8
8
|
|
|
9
9
|
const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
|
|
10
10
|
|
|
11
|
-
function normalizeTitle(value: string): string {
|
|
12
|
-
const normalized = compactWhitespace(value)
|
|
13
|
-
.replace(/^["'`]+|["'`]+$/g, '')
|
|
14
|
-
.replace(/[.!?,;:]+$/g, '')
|
|
15
|
-
return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
function buildRefinementPromptInput(
|
|
19
12
|
candidate: Awaited<ReturnType<typeof recentActivityService.getRefinementCandidate>>,
|
|
20
13
|
) {
|
|
@@ -52,7 +45,7 @@ class RecentActivityTitleService {
|
|
|
52
45
|
await this.helperRuntime.generateHelperText({
|
|
53
46
|
tag: 'recent-activity-title-refinement',
|
|
54
47
|
createAgent: createRecentActivityTitleRefinerAgent,
|
|
55
|
-
defaultSystemPrompt:
|
|
48
|
+
defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
|
|
56
49
|
timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
|
|
57
50
|
messages: [{ role: 'user', content: promptInput }],
|
|
58
51
|
}),
|
|
@@ -166,18 +166,12 @@ class RecentActivityService {
|
|
|
166
166
|
}): Promise<RecentActivity[]> {
|
|
167
167
|
await databaseService.connect()
|
|
168
168
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
event: candidate,
|
|
176
|
-
})
|
|
177
|
-
items.push(recorded.item)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return items
|
|
169
|
+
const results = await Promise.all(
|
|
170
|
+
params.events.map((candidate) =>
|
|
171
|
+
this.recordEvent({ orgId: params.orgId, userId: params.userId, source: params.source, event: candidate }),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
return results.map((r) => r.item)
|
|
181
175
|
}
|
|
182
176
|
|
|
183
177
|
async recordEvent(params: {
|