@lota-sdk/core 0.1.9 → 0.1.12
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 +1 -0
- package/infrastructure/schema/02_execution_plan.surql +202 -52
- package/package.json +4 -87
- package/src/ai/index.ts +3 -0
- package/src/bifrost/bifrost.ts +94 -25
- package/src/bifrost/index.ts +1 -0
- package/src/config/agent-defaults.ts +30 -7
- package/src/config/constants.ts +0 -9
- package/src/config/debug-logger.ts +43 -0
- package/src/config/index.ts +5 -0
- package/src/config/model-constants.ts +8 -9
- package/src/config/workstream-defaults.ts +4 -0
- package/src/db/cursor-pagination.ts +2 -2
- package/src/db/index.ts +10 -0
- package/src/db/memory-store.ts +3 -71
- package/src/db/memory.ts +9 -15
- package/src/db/service.ts +42 -2
- package/src/db/tables.ts +9 -2
- package/src/document/index.ts +2 -0
- package/src/document/parsing.ts +0 -25
- package/src/embeddings/provider.ts +102 -22
- package/src/index.ts +15 -499
- package/src/queues/index.ts +10 -0
- package/src/redis/connection-accessor.ts +26 -0
- package/src/redis/connection.ts +1 -1
- package/src/redis/index.ts +9 -25
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +1 -1
- package/src/redis/stream-context.ts +54 -0
- package/src/runtime/agent-runtime-policy.ts +9 -5
- package/src/runtime/agent-stream-helpers.ts +6 -3
- package/src/runtime/agent-types.ts +1 -5
- package/src/runtime/approval-continuation.ts +68 -1
- package/src/runtime/chat-attachments.ts +1 -1
- package/src/runtime/chat-request-routing.ts +6 -2
- package/src/runtime/context-compaction-runtime.ts +2 -2
- package/src/runtime/context-compaction.ts +1 -1
- package/src/runtime/execution-plan.ts +22 -15
- package/src/runtime/index.ts +26 -0
- package/src/runtime/indexed-repositories-policy.ts +10 -10
- package/src/runtime/memory-pipeline.ts +0 -2
- package/src/runtime/runtime-config.ts +238 -0
- package/src/runtime/runtime-extensions.ts +3 -2
- package/src/runtime/runtime-worker-registry.ts +47 -0
- package/src/runtime/team-consultation-orchestrator.ts +9 -6
- package/src/runtime/team-consultation-prompts.ts +3 -2
- package/src/runtime/turn-lifecycle.ts +13 -5
- package/src/runtime/workstream-chat-helpers.ts +0 -54
- package/src/runtime/workstream-routing-policy.ts +3 -7
- package/src/runtime.ts +387 -0
- package/src/services/chat-attachments.service.ts +1 -1
- package/src/services/context-compaction.service.ts +1 -1
- package/src/services/document-chunk.service.ts +2 -2
- package/src/services/execution-plan.service.ts +584 -793
- package/src/services/index.ts +14 -0
- package/src/services/learned-skill.service.ts +82 -39
- package/src/services/memory.service.ts +5 -4
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/organization.service.ts +1 -1
- package/src/services/plan-approval.service.ts +83 -0
- package/src/services/plan-artifact.service.ts +44 -0
- package/src/services/plan-builder.service.ts +61 -0
- package/src/services/plan-checkpoint.service.ts +53 -0
- package/src/services/plan-compiler.service.ts +81 -0
- package/src/services/plan-executor.service.ts +1624 -0
- package/src/services/plan-run.service.ts +422 -0
- package/src/services/plan-validator.service.ts +760 -0
- package/src/services/recent-activity-title.service.ts +1 -1
- package/src/services/recent-activity.service.ts +14 -16
- package/src/services/user.service.ts +2 -2
- package/src/services/workstream-message.service.ts +2 -3
- package/src/services/workstream-title.service.ts +1 -1
- package/src/services/workstream-turn-preparation.ts +156 -59
- package/src/services/workstream-turn.ts +26 -1
- package/src/services/workstream.service.ts +35 -9
- package/src/services/workstream.types.ts +1 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-storage.service.ts +11 -10
- package/src/storage/generated-document-storage.service.ts +7 -6
- package/src/storage/index.ts +10 -0
- package/src/system-agents/delegated-agent-factory.ts +78 -29
- package/src/system-agents/index.ts +4 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/skill-extractor.agent.ts +1 -1
- package/src/system-agents/skill-manager.agent.ts +2 -4
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +22 -48
- package/src/tools/firecrawl-client.ts +2 -2
- package/src/tools/index.ts +12 -0
- package/src/tools/log-hello-world.tool.ts +17 -0
- package/src/tools/research-topic.tool.ts +1 -1
- package/src/tools/team-think.tool.ts +1 -1
- package/src/tools/user-questions.tool.ts +2 -2
- package/src/utils/index.ts +6 -0
- package/src/workers/bootstrap.ts +8 -16
- package/src/workers/index.ts +7 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
- package/src/workers/skill-extraction.runner.ts +3 -3
- package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
- package/src/workers/utils/repo-structure-extractor.ts +2 -5
- package/src/workers/utils/repomix-file-sections.ts +42 -0
- package/src/config/env-shapes.ts +0 -121
- package/src/runtime/agent-contract.ts +0 -1
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
type LotaAgentFactoryRegistry = Record<string, (...args: unknown[]) => unknown>
|
|
2
2
|
|
|
3
|
+
function defaultBuildAgentTools(): Record<string, never> {
|
|
4
|
+
return {}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function defaultGetAgentRuntimeConfig(): Record<string, never> {
|
|
8
|
+
return {}
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
// Agent configuration — these are defaults that consumers override via createLotaRuntime config
|
|
4
12
|
export let agentDisplayNames: Record<string, string> = {}
|
|
5
13
|
export let agentShortDisplayNames: Record<string, string> = {}
|
|
6
14
|
export let agentRoster: readonly string[] = []
|
|
15
|
+
export let leadAgentId = ''
|
|
7
16
|
export let teamConsultParticipants: readonly string[] = []
|
|
8
17
|
|
|
9
18
|
export interface CoreWorkstreamProfile {
|
|
@@ -22,12 +31,18 @@ export let getCoreWorkstreamProfile: (coreType: string) => CoreWorkstreamProfile
|
|
|
22
31
|
|
|
23
32
|
export function configureAgents(config: {
|
|
24
33
|
roster: readonly string[]
|
|
34
|
+
leadAgentId: string
|
|
25
35
|
displayNames: Record<string, string>
|
|
26
36
|
shortDisplayNames?: Record<string, string>
|
|
27
37
|
teamConsultParticipants: readonly string[]
|
|
28
38
|
getCoreWorkstreamProfile?: (coreType: string) => CoreWorkstreamProfile
|
|
29
39
|
}): void {
|
|
40
|
+
if (!config.roster.includes(config.leadAgentId)) {
|
|
41
|
+
throw new Error(`Lead agent "${config.leadAgentId}" must be present in the configured roster.`)
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
agentRoster = config.roster
|
|
45
|
+
leadAgentId = config.leadAgentId
|
|
31
46
|
agentDisplayNames = config.displayNames
|
|
32
47
|
agentShortDisplayNames = config.shortDisplayNames ?? {}
|
|
33
48
|
teamConsultParticipants = config.teamConsultParticipants
|
|
@@ -40,6 +55,14 @@ export function isAgentName(value: unknown): boolean {
|
|
|
40
55
|
return typeof value === 'string' && new Set(agentRoster).has(value)
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
export function getLeadAgentId(): string {
|
|
59
|
+
return leadAgentId
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getLeadAgentDisplayName(): string {
|
|
63
|
+
return agentDisplayNames[leadAgentId] ?? leadAgentId
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
export function resolveAgentNameAlias(value: unknown): string | undefined {
|
|
44
67
|
if (typeof value !== 'string') return undefined
|
|
45
68
|
const lowered = value.trim().toLowerCase()
|
|
@@ -56,20 +79,20 @@ export function resolveAgentNameAlias(value: unknown): string | undefined {
|
|
|
56
79
|
|
|
57
80
|
export let createAgent: LotaAgentFactoryRegistry = {}
|
|
58
81
|
|
|
59
|
-
export let buildAgentTools: (...args: unknown[]) => unknown =
|
|
60
|
-
export let getAgentRuntimeConfig: (...args: unknown[]) => unknown =
|
|
82
|
+
export let buildAgentTools: (...args: unknown[]) => unknown = defaultBuildAgentTools
|
|
83
|
+
export let getAgentRuntimeConfig: (...args: unknown[]) => unknown = defaultGetAgentRuntimeConfig
|
|
61
84
|
export let pluginRuntime: unknown = undefined
|
|
62
85
|
|
|
63
86
|
export function configureAgentFactory(config: {
|
|
64
|
-
createAgent
|
|
87
|
+
createAgent?: LotaAgentFactoryRegistry
|
|
65
88
|
buildAgentTools?: (...args: unknown[]) => unknown
|
|
66
89
|
getAgentRuntimeConfig?: (...args: unknown[]) => unknown
|
|
67
90
|
pluginRuntime?: unknown
|
|
68
91
|
}): void {
|
|
69
|
-
createAgent = config.createAgent
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
createAgent = config.createAgent ?? {}
|
|
93
|
+
buildAgentTools = config.buildAgentTools ?? defaultBuildAgentTools
|
|
94
|
+
getAgentRuntimeConfig = config.getAgentRuntimeConfig ?? defaultGetAgentRuntimeConfig
|
|
95
|
+
pluginRuntime = config.pluginRuntime
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
const AGENT_MENTION_REGEX = /(^|[^\w])@([a-z][a-z0-9_-]*)\b/gi
|
package/src/config/constants.ts
CHANGED
|
@@ -22,12 +22,3 @@ export const MEMORY = {
|
|
|
22
22
|
export function validateKnnLimit(limit: unknown): number {
|
|
23
23
|
return z.number().int().positive().max(MEMORY.MAX_KNN_LIMIT).parse(limit)
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Creates a KNN query string with validated limit
|
|
28
|
-
* Example: createKnnQuery(10) returns "<|10|>"
|
|
29
|
-
*/
|
|
30
|
-
export function createKnnQuery(limit: unknown): string {
|
|
31
|
-
const validatedLimit = validateKnnLimit(limit)
|
|
32
|
-
return `<|${validatedLimit}|>`
|
|
33
|
-
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { chatLogger } from './logger'
|
|
2
|
+
|
|
3
|
+
const isDebug = () => process.env.LOG_LEVEL === 'debug' || process.env.LOTA_DEBUG === '1'
|
|
4
|
+
|
|
5
|
+
interface DebugTimer {
|
|
6
|
+
step(name: string): void
|
|
7
|
+
elapsed(): number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const NOOP_TIMER: DebugTimer = { step() {}, elapsed: () => 0 }
|
|
11
|
+
|
|
12
|
+
function createTimer(label: string): DebugTimer {
|
|
13
|
+
const start = performance.now()
|
|
14
|
+
let lastStep = start
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
step(name: string) {
|
|
18
|
+
const now = performance.now()
|
|
19
|
+
const stepMs = now - lastStep
|
|
20
|
+
const totalMs = now - start
|
|
21
|
+
chatLogger.debug`[ttft:${label}] ${name}: ${stepMs.toFixed(1)}ms (elapsed: ${totalMs.toFixed(1)}ms)`
|
|
22
|
+
lastStep = now
|
|
23
|
+
},
|
|
24
|
+
elapsed() {
|
|
25
|
+
return performance.now() - start
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const lotaDebugLogger = {
|
|
31
|
+
get enabled() {
|
|
32
|
+
return isDebug()
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
timer(label: string): DebugTimer {
|
|
36
|
+
return isDebug() ? createTimer(label) : NOOP_TIMER
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
step(name: string) {
|
|
40
|
+
if (!isDebug()) return
|
|
41
|
+
chatLogger.debug`[ttft] ${name}`
|
|
42
|
+
},
|
|
43
|
+
}
|
|
@@ -4,32 +4,31 @@ export const OPENROUTER_TEAM_AGENT_MODEL_ID = 'openrouter/google/gemini-3.1-pro-
|
|
|
4
4
|
export const OPENROUTER_STRUCTURED_HELPER_MODEL_ID = 'openrouter/google/gemini-3-flash-preview:exacto' as const
|
|
5
5
|
export const OPENROUTER_DELEGATED_REASONING_MODEL_ID = 'openrouter/google/gemini-3-flash-preview:exacto' as const
|
|
6
6
|
export const OPENROUTER_WEB_RESEARCH_MODEL_ID = 'openrouter/stepfun/step-3.5-flash' as const
|
|
7
|
-
export const OPENROUTER_ARTIFACT_GENERATOR_MODEL_ID = 'openrouter/qwen/qwen3.5-flash-02-23' as const
|
|
8
|
-
export const OPENROUTER_REPO_INDEXER_MODEL_ID = 'openrouter/qwen/qwen3.5-flash-02-23:nitro' as const
|
|
9
|
-
export const OPENROUTER_CODE_ANALYSIS_MODEL_ID = 'openrouter/xiaomi/mimo-v2-flash' as const
|
|
10
7
|
export const OPENROUTER_FAST_REASONING_MODEL_ID = 'openrouter/openai/gpt-oss-120b:nitro' as const
|
|
11
8
|
export const OPENROUTER_STRUCTURED_REASONING_MODEL_ID = 'openrouter/openai/gpt-oss-120b:exacto' as const
|
|
12
9
|
|
|
10
|
+
export const BIFROST_REASONING_SUMMARY_LEVEL = 'detailed' as const
|
|
11
|
+
|
|
13
12
|
export const OPENAI_HIGH_REASONING_PROVIDER_OPTIONS = {
|
|
14
|
-
openai: { forceReasoning: true, reasoningEffort: 'high', reasoningSummary:
|
|
13
|
+
openai: { forceReasoning: true, reasoningEffort: 'high', reasoningSummary: BIFROST_REASONING_SUMMARY_LEVEL },
|
|
15
14
|
} as const
|
|
16
15
|
|
|
17
16
|
export const OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS = {
|
|
18
|
-
openai: { forceReasoning: true, reasoningEffort: 'high', reasoningSummary:
|
|
17
|
+
openai: { forceReasoning: true, reasoningEffort: 'high', reasoningSummary: BIFROST_REASONING_SUMMARY_LEVEL },
|
|
19
18
|
} as const
|
|
20
19
|
|
|
21
20
|
export const OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS = {
|
|
22
|
-
openai: { forceReasoning: true, reasoningEffort: 'xhigh', reasoningSummary:
|
|
21
|
+
openai: { forceReasoning: true, reasoningEffort: 'xhigh', reasoningSummary: BIFROST_REASONING_SUMMARY_LEVEL },
|
|
23
22
|
} as const
|
|
24
23
|
|
|
25
24
|
export const OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS = {
|
|
26
|
-
openai: { forceReasoning: true, reasoningEffort: 'medium', reasoningSummary:
|
|
25
|
+
openai: { forceReasoning: true, reasoningEffort: 'medium', reasoningSummary: BIFROST_REASONING_SUMMARY_LEVEL },
|
|
27
26
|
} as const
|
|
28
27
|
|
|
29
28
|
export const OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS = {
|
|
30
|
-
openai: { forceReasoning: true, reasoningEffort: 'low', reasoningSummary:
|
|
29
|
+
openai: { forceReasoning: true, reasoningEffort: 'low', reasoningSummary: BIFROST_REASONING_SUMMARY_LEVEL },
|
|
31
30
|
} as const
|
|
32
31
|
|
|
33
32
|
export const OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS = {
|
|
34
|
-
openai: { forceReasoning: true, reasoningEffort: 'minimal', reasoningSummary:
|
|
33
|
+
openai: { forceReasoning: true, reasoningEffort: 'minimal', reasoningSummary: BIFROST_REASONING_SUMMARY_LEVEL },
|
|
35
34
|
} as const
|
|
@@ -66,3 +66,7 @@ export function configureWorkstreams(params: { agentRoster: readonly string[]; c
|
|
|
66
66
|
export function getWorkstreamBootstrapConfig(): ResolvedWorkstreamBootstrapConfig {
|
|
67
67
|
return resolvedWorkstreamBootstrapConfig
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
export function resolveOnboardingOwnerAgentId(defaultLeadAgentId: string): string {
|
|
71
|
+
return resolvedWorkstreamBootstrapConfig.onboardingWelcome?.directAgentId ?? defaultLeadAgentId
|
|
72
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { toTimestamp } from '@lota-sdk/shared
|
|
2
|
-
import type { ChatMessage } from '@lota-sdk/shared
|
|
1
|
+
import { toTimestamp } from '@lota-sdk/shared'
|
|
2
|
+
import type { ChatMessage } from '@lota-sdk/shared'
|
|
3
3
|
import type { BoundQuery, RecordId } from 'surrealdb'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './cursor-pagination'
|
|
2
|
+
export * from './memory'
|
|
3
|
+
export * from './memory-store'
|
|
4
|
+
export * from './memory-store.helpers'
|
|
5
|
+
export * from './memory-types'
|
|
6
|
+
export * from './record-id'
|
|
7
|
+
export * from './sdk-database'
|
|
8
|
+
export * from './service'
|
|
9
|
+
export * from './startup'
|
|
10
|
+
export * from './tables'
|
package/src/db/memory-store.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { BoundQuery, eq, inside } from 'surrealdb'
|
|
2
2
|
|
|
3
|
-
import { getEmbeddingCache } from '../ai/embedding-cache'
|
|
4
|
-
import { env } from '../config/env-shapes'
|
|
5
3
|
import { aiLogger } from '../config/logger'
|
|
6
4
|
import { DEFAULT_MEMORY_SEARCH_LIMIT } from '../config/search'
|
|
7
|
-
import {
|
|
5
|
+
import { getDefaultEmbeddings } from '../embeddings/provider'
|
|
8
6
|
import { memoryQueryBuilder } from './memory-query-builder'
|
|
9
7
|
import type { RelationCounts } from './memory-store.helpers'
|
|
10
8
|
import { hashContent, mapRowToMemoryRecord, processGraphAwareRows } from './memory-store.helpers'
|
|
@@ -25,8 +23,6 @@ import { TABLES } from './tables'
|
|
|
25
23
|
const MEMORY_TABLE = TABLES.MEMORY
|
|
26
24
|
const MEMORY_HISTORY_TABLE = TABLES.MEMORY_HISTORY
|
|
27
25
|
const MEMORY_RELATION_TABLE = TABLES.MEMORY_RELATION
|
|
28
|
-
const EMBEDDING_CACHE_TTL_MS = 5 * 60 * 1000
|
|
29
|
-
const EMBEDDING_CACHE_MAX_ENTRIES = 64
|
|
30
26
|
const MIN_RELEVANCE_SCORE = 0.25
|
|
31
27
|
const TOUCH_MEMORIES_MAX_ATTEMPTS = 4
|
|
32
28
|
const TOUCH_MEMORIES_RETRY_BASE_DELAY_MS = 25
|
|
@@ -37,9 +33,6 @@ interface EmbeddingClient {
|
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
export class SurrealMemoryStore {
|
|
40
|
-
private embeddingCache = new Map<string, { embedding: number[]; ts: number }>()
|
|
41
|
-
private embeddingInFlight = new Map<string, Promise<number[]>>()
|
|
42
|
-
|
|
43
36
|
constructor(private embeddings: EmbeddingClient) {}
|
|
44
37
|
|
|
45
38
|
private toMetadataFieldPath(key: string): string {
|
|
@@ -183,72 +176,11 @@ export class SurrealMemoryStore {
|
|
|
183
176
|
}))
|
|
184
177
|
}
|
|
185
178
|
|
|
186
|
-
private getEmbeddingCacheKey(content: string): string {
|
|
187
|
-
return content.trim().toLowerCase()
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private pruneEmbeddingCache(now: number): void {
|
|
191
|
-
for (const [cacheKey, entry] of this.embeddingCache.entries()) {
|
|
192
|
-
if (now - entry.ts > EMBEDDING_CACHE_TTL_MS) {
|
|
193
|
-
this.embeddingCache.delete(cacheKey)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
while (this.embeddingCache.size > EMBEDDING_CACHE_MAX_ENTRIES) {
|
|
198
|
-
const oldest = this.embeddingCache.keys().next().value
|
|
199
|
-
if (!oldest) break
|
|
200
|
-
this.embeddingCache.delete(oldest)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
179
|
private async generateEmbedding(content: string): Promise<number[]> {
|
|
205
180
|
const normalized = content.trim()
|
|
206
181
|
if (!normalized) return []
|
|
207
182
|
|
|
208
|
-
|
|
209
|
-
const now = Date.now()
|
|
210
|
-
this.pruneEmbeddingCache(now)
|
|
211
|
-
|
|
212
|
-
// L1: in-memory cache
|
|
213
|
-
const cached = this.embeddingCache.get(cacheKey)
|
|
214
|
-
if (cached && now - cached.ts <= EMBEDDING_CACHE_TTL_MS) {
|
|
215
|
-
return cached.embedding
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const inFlight = this.embeddingInFlight.get(cacheKey)
|
|
219
|
-
if (inFlight) {
|
|
220
|
-
return await inFlight
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const request = (async () => {
|
|
224
|
-
// L2: Redis cache
|
|
225
|
-
const redisCache = getEmbeddingCache()
|
|
226
|
-
if (redisCache) {
|
|
227
|
-
const model = env.AI_EMBEDDING_MODEL
|
|
228
|
-
const redisCached = await redisCache.get(model, normalized)
|
|
229
|
-
if (redisCached) {
|
|
230
|
-
this.embeddingCache.set(cacheKey, { embedding: redisCached, ts: Date.now() })
|
|
231
|
-
return redisCached
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// L3: API call
|
|
236
|
-
const embedding = await this.embeddings.embedQuery(normalized)
|
|
237
|
-
this.embeddingCache.set(cacheKey, { embedding, ts: Date.now() })
|
|
238
|
-
this.pruneEmbeddingCache(Date.now())
|
|
239
|
-
|
|
240
|
-
// Backfill Redis
|
|
241
|
-
if (redisCache) {
|
|
242
|
-
void redisCache.set(env.AI_EMBEDDING_MODEL, normalized, embedding)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return embedding
|
|
246
|
-
})().finally(() => {
|
|
247
|
-
this.embeddingInFlight.delete(cacheKey)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
this.embeddingInFlight.set(cacheKey, request)
|
|
251
|
-
return await request
|
|
183
|
+
return await this.embeddings.embedQuery(normalized)
|
|
252
184
|
}
|
|
253
185
|
|
|
254
186
|
async warmEmbedding(content: string): Promise<void> {
|
|
@@ -1059,7 +991,7 @@ let defaultMemoryStore: SurrealMemoryStore | null = null
|
|
|
1059
991
|
|
|
1060
992
|
export function getDefaultMemoryStore(): SurrealMemoryStore {
|
|
1061
993
|
if (!defaultMemoryStore) {
|
|
1062
|
-
defaultMemoryStore = new SurrealMemoryStore(
|
|
994
|
+
defaultMemoryStore = new SurrealMemoryStore(getDefaultEmbeddings())
|
|
1063
995
|
}
|
|
1064
996
|
return defaultMemoryStore
|
|
1065
997
|
}
|
package/src/db/memory.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { env } from '../config/env-shapes'
|
|
2
1
|
import { aiLogger } from '../config/logger'
|
|
3
2
|
import type { CreateHelperAgentFn } from '../runtime/helper-model'
|
|
4
3
|
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
import { getFactRetrievalMessages } from '../runtime/memory-prompts-fact'
|
|
13
12
|
import { parseMessages } from '../runtime/memory-prompts-parse'
|
|
14
13
|
import { getClassifyMemoryDeltaPrompt } from '../runtime/memory-prompts-update'
|
|
14
|
+
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
15
15
|
import type { SurrealMemoryStore } from './memory-store'
|
|
16
16
|
import { getDefaultMemoryStore } from './memory-store'
|
|
17
17
|
import { hashContent, isUniqueIndexConflict } from './memory-store.helpers'
|
|
@@ -91,23 +91,15 @@ export class Memory {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
async search(query: string, options: SearchOptions): Promise<string> {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
options.scopeId,
|
|
97
|
-
options.limit ?? env.MEMORY_SEARCH_K,
|
|
98
|
-
options.memoryType,
|
|
99
|
-
)
|
|
94
|
+
const limit = options.limit ?? getRuntimeConfig().memory.searchK
|
|
95
|
+
const results = await this.store.search(query, options.scopeId, limit, options.memoryType)
|
|
100
96
|
|
|
101
97
|
return formatResults(results)
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
async hybridSearch(query: string, options: SearchOptions): Promise<string> {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
options.scopeId,
|
|
108
|
-
options.limit ?? env.MEMORY_SEARCH_K,
|
|
109
|
-
options.memoryType,
|
|
110
|
-
)
|
|
101
|
+
const limit = options.limit ?? getRuntimeConfig().memory.searchK
|
|
102
|
+
const results = await this.store.hybridSearch(query, options.scopeId, limit, options.memoryType)
|
|
111
103
|
|
|
112
104
|
return formatResults(results)
|
|
113
105
|
}
|
|
@@ -116,9 +108,10 @@ export class Memory {
|
|
|
116
108
|
query: string,
|
|
117
109
|
options: SearchOptions & { weights?: [number, number]; normalization?: 'minmax' | 'zscore' },
|
|
118
110
|
): Promise<string> {
|
|
111
|
+
const limit = options.limit ?? getRuntimeConfig().memory.searchK
|
|
119
112
|
const results = await this.store.hybridSearchWeighted(query, {
|
|
120
113
|
scopeId: options.scopeId,
|
|
121
|
-
limit
|
|
114
|
+
limit,
|
|
122
115
|
memoryType: options.memoryType,
|
|
123
116
|
weights: options.weights,
|
|
124
117
|
normalization: options.normalization,
|
|
@@ -128,9 +121,10 @@ export class Memory {
|
|
|
128
121
|
}
|
|
129
122
|
|
|
130
123
|
async searchCandidates(query: string, options: WeightedSearchOptions): Promise<MemorySearchResult[]> {
|
|
124
|
+
const limit = options.limit ?? getRuntimeConfig().memory.searchK
|
|
131
125
|
const results = await this.store.hybridSearchWeighted(query, {
|
|
132
126
|
scopeId: options.scopeId,
|
|
133
|
-
limit
|
|
127
|
+
limit,
|
|
134
128
|
memoryType: options.memoryType,
|
|
135
129
|
weights: options.weights,
|
|
136
130
|
normalization: options.normalization,
|
package/src/db/service.ts
CHANGED
|
@@ -66,6 +66,8 @@ export type CreateMutationBuilder = {
|
|
|
66
66
|
export interface DatabaseTransaction {
|
|
67
67
|
query: (query: unknown) => Promise<unknown>
|
|
68
68
|
create: (target: unknown) => CreateMutationBuilder
|
|
69
|
+
update: (target: unknown) => MutationBuilder
|
|
70
|
+
delete: (target: unknown) => Promise<unknown>
|
|
69
71
|
relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) => Promise<unknown>
|
|
70
72
|
commit: () => Promise<void>
|
|
71
73
|
cancel: () => Promise<void>
|
|
@@ -458,6 +460,38 @@ export class SurrealDBService {
|
|
|
458
460
|
throw new SurrealDBError('Invalid table value')
|
|
459
461
|
}
|
|
460
462
|
|
|
463
|
+
private isRecordIdLike(value: unknown): boolean {
|
|
464
|
+
if (value instanceof RecordId || value instanceof StringRecordId) {
|
|
465
|
+
return true
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (typeof value === 'string') {
|
|
469
|
+
return /^[a-zA-Z][a-zA-Z0-9_]*:/.test(value)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (value && typeof value === 'object') {
|
|
473
|
+
const record = value as { tb?: unknown; id?: unknown }
|
|
474
|
+
if (typeof record.tb === 'string' && record.id !== undefined) {
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const stringValue = toStringLikeValue(value)
|
|
479
|
+
if (stringValue) {
|
|
480
|
+
return /^[a-zA-Z][a-zA-Z0-9_]*:/.test(stringValue)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return false
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private normalizeCreateTarget(value: unknown): Table | RecordId {
|
|
488
|
+
if (this.isRecordIdLike(value)) {
|
|
489
|
+
return ensureRecordId(value as RecordIdInput)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return this.normalizeTableValue(value)
|
|
493
|
+
}
|
|
494
|
+
|
|
461
495
|
private wrapMutationBuilder(builder: MutationBuilder): MutationBuilder {
|
|
462
496
|
return {
|
|
463
497
|
content: (data) => this.wrapMutationBuilder(builder.content(this.normalizeMutationData(data))),
|
|
@@ -478,8 +512,14 @@ export class SurrealDBService {
|
|
|
478
512
|
private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
|
|
479
513
|
return {
|
|
480
514
|
query: (query: unknown) => tx.query(this.normalizeBoundQuery(query as BoundQuery | BoundQueryLike)),
|
|
481
|
-
create: (target: unknown) =>
|
|
482
|
-
this.
|
|
515
|
+
create: (target: unknown) => {
|
|
516
|
+
const normalizedTarget = this.normalizeCreateTarget(target)
|
|
517
|
+
const builder = normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
|
|
518
|
+
return this.wrapCreateBuilder(builder as unknown as CreateMutationBuilder)
|
|
519
|
+
},
|
|
520
|
+
update: (target: unknown) =>
|
|
521
|
+
this.wrapMutationBuilder(tx.update(ensureRecordId(target as RecordIdInput)) as unknown as MutationBuilder),
|
|
522
|
+
delete: (target: unknown) => tx.delete(ensureRecordId(target as RecordIdInput)),
|
|
483
523
|
relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
|
|
484
524
|
tx.relate(
|
|
485
525
|
ensureRecordId(from as RecordIdInput),
|
package/src/db/tables.ts
CHANGED
|
@@ -7,8 +7,15 @@ export const TABLES = {
|
|
|
7
7
|
MEMORY_RELATION: 'memoryRelation',
|
|
8
8
|
MEMORY_HISTORY: 'memoryHistory',
|
|
9
9
|
LEARNED_SKILL: 'learnedSkill',
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
PLAN_SPEC: 'planSpec',
|
|
11
|
+
PLAN_NODE_SPEC: 'planNodeSpec',
|
|
12
|
+
PLAN_RUN: 'planRun',
|
|
13
|
+
PLAN_NODE_RUN: 'planNodeRun',
|
|
14
|
+
PLAN_NODE_ATTEMPT: 'planNodeAttempt',
|
|
15
|
+
PLAN_ARTIFACT: 'planArtifact',
|
|
16
|
+
PLAN_APPROVAL: 'planApproval',
|
|
17
|
+
PLAN_CHECKPOINT: 'planCheckpoint',
|
|
18
|
+
PLAN_VALIDATION_ISSUE: 'planValidationIssue',
|
|
12
19
|
PLAN_EVENT: 'planEvent',
|
|
13
20
|
ORGANIZATION: 'organization',
|
|
14
21
|
ORGANIZATION_MEMBER: 'organizationMember',
|
package/src/document/parsing.ts
CHANGED
|
@@ -13,28 +13,3 @@ export function normalizeKey(value: string): string {
|
|
|
13
13
|
.replace(/\s+/g, '-')
|
|
14
14
|
.slice(0, 120)
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
export function makeMemoryKey(kind: string, rawKey: string): string {
|
|
18
|
-
const normalized = normalizeKey(rawKey)
|
|
19
|
-
return normalized ? `${kind}:${normalized}` : `${kind}:item`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function truncateForModel(value: string, maxChars: number): string {
|
|
23
|
-
if (value.length <= maxChars) return value
|
|
24
|
-
return `${value.slice(0, maxChars)}\n\n[...truncated due to size...]`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function dedupeStrings(items: string[], limit: number): string[] {
|
|
28
|
-
const out: string[] = []
|
|
29
|
-
const seen = new Set<string>()
|
|
30
|
-
for (const raw of items) {
|
|
31
|
-
const value = normalizeWhitespace(raw)
|
|
32
|
-
if (!value) continue
|
|
33
|
-
const key = value.toLowerCase()
|
|
34
|
-
if (seen.has(key)) continue
|
|
35
|
-
seen.add(key)
|
|
36
|
-
out.push(value)
|
|
37
|
-
if (out.length >= limit) break
|
|
38
|
-
}
|
|
39
|
-
return out
|
|
40
|
-
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { embed, embedMany } from 'ai'
|
|
2
2
|
|
|
3
|
+
import { getEmbeddingCache } from '../ai/embedding-cache'
|
|
3
4
|
import { bifrostEmbeddingModel } from '../bifrost/bifrost'
|
|
4
|
-
import {
|
|
5
|
+
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
5
6
|
|
|
6
7
|
const SUPPORTED_EMBEDDING_PREFIXES = ['openai/', 'openrouter/'] as const
|
|
7
8
|
|
|
9
|
+
type SharedEmbeddingCache = {
|
|
10
|
+
get(model: string, text: string): Promise<number[] | null>
|
|
11
|
+
set(model: string, text: string, embedding: number[]): Promise<void>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ProviderEmbeddingsOptions = {
|
|
15
|
+
embedFn?: typeof embed
|
|
16
|
+
embedManyFn?: typeof embedMany
|
|
17
|
+
getCache?: () => SharedEmbeddingCache | null
|
|
18
|
+
modelId?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
function resolveEmbeddingModel(modelId: string) {
|
|
9
22
|
const normalized = modelId.trim()
|
|
10
23
|
if (!normalized) {
|
|
@@ -20,23 +33,63 @@ function resolveEmbeddingModel(modelId: string) {
|
|
|
20
33
|
return bifrostEmbeddingModel(normalized)
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
|
|
36
|
+
function normalizeEmbedding(embedding: readonly number[]): number[] {
|
|
37
|
+
return embedding.map((value) => Number(value))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ProviderEmbeddings {
|
|
41
|
+
private readonly embedFn: typeof embed
|
|
42
|
+
private readonly embedManyFn: typeof embedMany
|
|
43
|
+
private readonly getCache: () => SharedEmbeddingCache | null
|
|
44
|
+
private readonly configuredModelId?: string
|
|
45
|
+
private resolvedModelId: string | null = null
|
|
24
46
|
private _model: ReturnType<typeof resolveEmbeddingModel> | null = null
|
|
25
47
|
|
|
48
|
+
constructor(options: ProviderEmbeddingsOptions = {}) {
|
|
49
|
+
this.embedFn = options.embedFn ?? embed
|
|
50
|
+
this.embedManyFn = options.embedManyFn ?? embedMany
|
|
51
|
+
this.getCache = options.getCache ?? getEmbeddingCache
|
|
52
|
+
this.configuredModelId = options.modelId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private getModelId(): string {
|
|
56
|
+
if (!this.resolvedModelId) {
|
|
57
|
+
this.resolvedModelId = this.configuredModelId ?? getRuntimeConfig().aiGateway.embeddingModel
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this.resolvedModelId
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
private getModel() {
|
|
27
64
|
if (!this._model) {
|
|
28
|
-
this._model = resolveEmbeddingModel(
|
|
65
|
+
this._model = resolveEmbeddingModel(this.getModelId())
|
|
29
66
|
}
|
|
30
67
|
return this._model
|
|
31
68
|
}
|
|
32
69
|
|
|
70
|
+
private async loadCachedEmbedding(text: string): Promise<number[] | null> {
|
|
71
|
+
const redisCache = this.getCache()
|
|
72
|
+
if (!redisCache) return null
|
|
73
|
+
|
|
74
|
+
return await redisCache.get(this.getModelId(), text)
|
|
75
|
+
}
|
|
76
|
+
|
|
33
77
|
async embedQuery(text: string): Promise<number[]> {
|
|
34
78
|
const input = text.trim()
|
|
35
79
|
if (!input) return []
|
|
36
80
|
|
|
37
|
-
const
|
|
81
|
+
const cached = await this.loadCachedEmbedding(input)
|
|
82
|
+
if (cached) return cached
|
|
38
83
|
|
|
39
|
-
|
|
84
|
+
const result = await this.embedFn({ model: this.getModel(), value: input, maxRetries: 2 })
|
|
85
|
+
const embedding = normalizeEmbedding(result.embedding)
|
|
86
|
+
|
|
87
|
+
const redisCache = this.getCache()
|
|
88
|
+
if (redisCache) {
|
|
89
|
+
void redisCache.set(this.getModelId(), input, embedding)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return embedding
|
|
40
93
|
}
|
|
41
94
|
|
|
42
95
|
async embedDocuments(values: string[]): Promise<number[][]> {
|
|
@@ -51,26 +104,53 @@ class ProviderEmbeddings {
|
|
|
51
104
|
return normalized.map(() => [])
|
|
52
105
|
}
|
|
53
106
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const entry = nonEmptyEntries.at(index)
|
|
63
|
-
if (!entry) return
|
|
64
|
-
embeddingsByIndex.set(
|
|
65
|
-
entry.index,
|
|
66
|
-
embedding.map((value) => Number(value)),
|
|
107
|
+
const uniqueTexts = [...new Set(nonEmptyEntries.map((entry) => entry.value))]
|
|
108
|
+
const embeddingsByText = new Map<string, number[]>()
|
|
109
|
+
let missingTexts = [...uniqueTexts]
|
|
110
|
+
|
|
111
|
+
const redisCache = this.getCache()
|
|
112
|
+
if (redisCache && missingTexts.length > 0) {
|
|
113
|
+
const redisResults = await Promise.all(
|
|
114
|
+
missingTexts.map(async (text) => ({ text, embedding: await redisCache.get(this.getModelId(), text) })),
|
|
67
115
|
)
|
|
68
|
-
})
|
|
69
116
|
|
|
70
|
-
|
|
117
|
+
missingTexts = []
|
|
118
|
+
for (const result of redisResults) {
|
|
119
|
+
if (!result.embedding) {
|
|
120
|
+
missingTexts.push(result.text)
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
embeddingsByText.set(result.text, result.embedding)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (missingTexts.length > 0) {
|
|
129
|
+
const result = await this.embedManyFn({ model: this.getModel(), values: missingTexts, maxRetries: 2 })
|
|
130
|
+
|
|
131
|
+
missingTexts.forEach((text, index) => {
|
|
132
|
+
const embedding = normalizeEmbedding(result.embeddings[index] ?? [])
|
|
133
|
+
embeddingsByText.set(text, embedding)
|
|
134
|
+
if (redisCache) {
|
|
135
|
+
void redisCache.set(this.getModelId(), text, embedding)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return normalized.map((text) => (text ? (embeddingsByText.get(text) ?? []) : []))
|
|
71
141
|
}
|
|
72
142
|
}
|
|
73
143
|
|
|
74
|
-
|
|
75
|
-
|
|
144
|
+
let defaultEmbeddings: ProviderEmbeddings | null = null
|
|
145
|
+
|
|
146
|
+
export function getDefaultEmbeddings(): ProviderEmbeddings {
|
|
147
|
+
if (!defaultEmbeddings) {
|
|
148
|
+
defaultEmbeddings = new ProviderEmbeddings()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return defaultEmbeddings
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function resetDefaultEmbeddingsForTests(): void {
|
|
155
|
+
defaultEmbeddings = null
|
|
76
156
|
}
|