@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.
Files changed (105) hide show
  1. package/infrastructure/schema/00_workstream.surql +1 -0
  2. package/infrastructure/schema/02_execution_plan.surql +202 -52
  3. package/package.json +4 -87
  4. package/src/ai/index.ts +3 -0
  5. package/src/bifrost/bifrost.ts +94 -25
  6. package/src/bifrost/index.ts +1 -0
  7. package/src/config/agent-defaults.ts +30 -7
  8. package/src/config/constants.ts +0 -9
  9. package/src/config/debug-logger.ts +43 -0
  10. package/src/config/index.ts +5 -0
  11. package/src/config/model-constants.ts +8 -9
  12. package/src/config/workstream-defaults.ts +4 -0
  13. package/src/db/cursor-pagination.ts +2 -2
  14. package/src/db/index.ts +10 -0
  15. package/src/db/memory-store.ts +3 -71
  16. package/src/db/memory.ts +9 -15
  17. package/src/db/service.ts +42 -2
  18. package/src/db/tables.ts +9 -2
  19. package/src/document/index.ts +2 -0
  20. package/src/document/parsing.ts +0 -25
  21. package/src/embeddings/provider.ts +102 -22
  22. package/src/index.ts +15 -499
  23. package/src/queues/index.ts +10 -0
  24. package/src/redis/connection-accessor.ts +26 -0
  25. package/src/redis/connection.ts +1 -1
  26. package/src/redis/index.ts +9 -25
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +1 -1
  29. package/src/redis/stream-context.ts +54 -0
  30. package/src/runtime/agent-runtime-policy.ts +9 -5
  31. package/src/runtime/agent-stream-helpers.ts +6 -3
  32. package/src/runtime/agent-types.ts +1 -5
  33. package/src/runtime/approval-continuation.ts +68 -1
  34. package/src/runtime/chat-attachments.ts +1 -1
  35. package/src/runtime/chat-request-routing.ts +6 -2
  36. package/src/runtime/context-compaction-runtime.ts +2 -2
  37. package/src/runtime/context-compaction.ts +1 -1
  38. package/src/runtime/execution-plan.ts +22 -15
  39. package/src/runtime/index.ts +26 -0
  40. package/src/runtime/indexed-repositories-policy.ts +10 -10
  41. package/src/runtime/memory-pipeline.ts +0 -2
  42. package/src/runtime/runtime-config.ts +238 -0
  43. package/src/runtime/runtime-extensions.ts +3 -2
  44. package/src/runtime/runtime-worker-registry.ts +47 -0
  45. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  46. package/src/runtime/team-consultation-prompts.ts +3 -2
  47. package/src/runtime/turn-lifecycle.ts +13 -5
  48. package/src/runtime/workstream-chat-helpers.ts +0 -54
  49. package/src/runtime/workstream-routing-policy.ts +3 -7
  50. package/src/runtime.ts +387 -0
  51. package/src/services/chat-attachments.service.ts +1 -1
  52. package/src/services/context-compaction.service.ts +1 -1
  53. package/src/services/document-chunk.service.ts +2 -2
  54. package/src/services/execution-plan.service.ts +584 -793
  55. package/src/services/index.ts +14 -0
  56. package/src/services/learned-skill.service.ts +82 -39
  57. package/src/services/memory.service.ts +5 -4
  58. package/src/services/mutating-approval.service.ts +1 -1
  59. package/src/services/organization-member.service.ts +1 -1
  60. package/src/services/organization.service.ts +1 -1
  61. package/src/services/plan-approval.service.ts +83 -0
  62. package/src/services/plan-artifact.service.ts +44 -0
  63. package/src/services/plan-builder.service.ts +61 -0
  64. package/src/services/plan-checkpoint.service.ts +53 -0
  65. package/src/services/plan-compiler.service.ts +81 -0
  66. package/src/services/plan-executor.service.ts +1624 -0
  67. package/src/services/plan-run.service.ts +422 -0
  68. package/src/services/plan-validator.service.ts +760 -0
  69. package/src/services/recent-activity-title.service.ts +1 -1
  70. package/src/services/recent-activity.service.ts +14 -16
  71. package/src/services/user.service.ts +2 -2
  72. package/src/services/workstream-message.service.ts +2 -3
  73. package/src/services/workstream-title.service.ts +1 -1
  74. package/src/services/workstream-turn-preparation.ts +156 -59
  75. package/src/services/workstream-turn.ts +26 -1
  76. package/src/services/workstream.service.ts +35 -9
  77. package/src/services/workstream.types.ts +1 -0
  78. package/src/storage/attachment-parser.ts +1 -1
  79. package/src/storage/attachment-storage.service.ts +11 -10
  80. package/src/storage/generated-document-storage.service.ts +7 -6
  81. package/src/storage/index.ts +10 -0
  82. package/src/system-agents/delegated-agent-factory.ts +78 -29
  83. package/src/system-agents/index.ts +4 -0
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/skill-extractor.agent.ts +1 -1
  87. package/src/system-agents/skill-manager.agent.ts +2 -4
  88. package/src/system-agents/title-generator.agent.ts +2 -2
  89. package/src/tools/execution-plan.tool.ts +22 -48
  90. package/src/tools/firecrawl-client.ts +2 -2
  91. package/src/tools/index.ts +12 -0
  92. package/src/tools/log-hello-world.tool.ts +17 -0
  93. package/src/tools/research-topic.tool.ts +1 -1
  94. package/src/tools/team-think.tool.ts +1 -1
  95. package/src/tools/user-questions.tool.ts +2 -2
  96. package/src/utils/index.ts +6 -0
  97. package/src/workers/bootstrap.ts +8 -16
  98. package/src/workers/index.ts +7 -0
  99. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  100. package/src/workers/skill-extraction.runner.ts +3 -3
  101. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  102. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  103. package/src/workers/utils/repomix-file-sections.ts +42 -0
  104. package/src/config/env-shapes.ts +0 -121
  105. 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: LotaAgentFactoryRegistry
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
- if (config.buildAgentTools) buildAgentTools = config.buildAgentTools
71
- if (config.getAgentRuntimeConfig) getAgentRuntimeConfig = config.getAgentRuntimeConfig
72
- if (config.pluginRuntime !== undefined) pluginRuntime = config.pluginRuntime
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
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export * from './agent-defaults'
2
+ export * from './background-processing'
3
+ export * from './logger'
4
+ export * from './model-constants'
5
+ export * from './workstream-defaults'
@@ -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: 'auto' },
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: 'auto' },
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: 'auto' },
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: 'auto' },
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: 'auto' },
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: 'auto' },
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/runtime/chat-message-metadata'
2
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
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
 
@@ -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'
@@ -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 { createDefaultEmbeddings } from '../embeddings/provider'
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
- const cacheKey = this.getEmbeddingCacheKey(normalized)
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(createDefaultEmbeddings())
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 results = await this.store.search(
95
- query,
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 results = await this.store.hybridSearch(
106
- query,
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: options.limit ?? env.MEMORY_SEARCH_K,
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: options.limit ?? env.MEMORY_SEARCH_K,
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.wrapCreateBuilder(tx.create(this.normalizeTableValue(target)) as unknown as CreateMutationBuilder),
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
- PLAN: 'plan',
11
- PLAN_TASK: 'planTask',
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',
@@ -0,0 +1,2 @@
1
+ export * from './org-document-chunking'
2
+ export * from './parsing'
@@ -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 { env } from '../config/env-shapes'
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
- class ProviderEmbeddings {
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(env.AI_EMBEDDING_MODEL)
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 result = await embed({ model: this.getModel(), value: input, maxRetries: 2 })
81
+ const cached = await this.loadCachedEmbedding(input)
82
+ if (cached) return cached
38
83
 
39
- return result.embedding.map((value) => Number(value))
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 result = await embedMany({
55
- model: this.getModel(),
56
- values: nonEmptyEntries.map((entry) => entry.value),
57
- maxRetries: 2,
58
- })
59
-
60
- const embeddingsByIndex = new Map<number, number[]>()
61
- result.embeddings.forEach((embedding, index) => {
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
- return normalized.map((_, index) => embeddingsByIndex.get(index) ?? [])
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
- export function createDefaultEmbeddings(): ProviderEmbeddings {
75
- return new ProviderEmbeddings()
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
  }