@lota-sdk/core 0.1.5 → 0.1.7

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 (36) hide show
  1. package/infrastructure/schema/00_identity.surql +26 -0
  2. package/infrastructure/schema/00_workstream.surql +8 -0
  3. package/infrastructure/schema/05_recent_activity.surql +48 -0
  4. package/package.json +4 -3
  5. package/src/ai/embedding-cache.ts +48 -0
  6. package/src/config/background-processing.ts +33 -0
  7. package/src/config/env-shapes.ts +0 -1
  8. package/src/config/model-constants.ts +4 -0
  9. package/src/db/memory-store.ts +110 -19
  10. package/src/db/memory-types.ts +11 -0
  11. package/src/db/memory.ts +11 -1
  12. package/src/db/schema-fingerprint.ts +21 -0
  13. package/src/db/sdk-database.ts +1 -0
  14. package/src/db/service.ts +0 -4
  15. package/src/db/tables.ts +1 -1
  16. package/src/index.ts +207 -10
  17. package/src/queues/memory-consolidation.queue.ts +6 -0
  18. package/src/queues/workstream-title-generation.queue.ts +69 -0
  19. package/src/runtime/agent-types.ts +5 -22
  20. package/src/runtime/helper-model.ts +9 -2
  21. package/src/runtime/memory-digest-policy.ts +30 -2
  22. package/src/runtime/skill-extraction-policy.ts +9 -2
  23. package/src/services/memory.service.ts +35 -0
  24. package/src/services/organization-member.service.ts +114 -0
  25. package/src/services/organization.service.ts +117 -0
  26. package/src/services/user.service.ts +56 -0
  27. package/src/services/workstream-title.service.ts +25 -35
  28. package/src/services/workstream-turn-preparation.ts +37 -10
  29. package/src/services/workstream-turn.ts +2 -0
  30. package/src/services/workstream.service.ts +61 -1
  31. package/src/services/workstream.types.ts +3 -0
  32. package/src/system-agents/title-generator.agent.ts +5 -5
  33. package/src/tools/research-topic.tool.ts +5 -1
  34. package/src/utils/sse-keepalive.ts +40 -0
  35. package/src/workers/bootstrap.ts +26 -1
  36. package/src/workers/memory-consolidation.worker.ts +1 -9
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { CoreWorkstreamProfile } from './config/agent-defaults'
2
2
  import type { LotaWorkstreamConfig } from './config/workstream-defaults'
3
+ import { ensureRecordId } from './db/record-id'
3
4
  import type { SurrealDBService } from './db/service'
5
+ import { TABLES } from './db/tables'
4
6
  import type { startContextCompactionWorker } from './queues/context-compaction.queue'
5
7
  import type {
6
8
  scheduleRecurringConsolidation,
@@ -10,16 +12,22 @@ import type { startPostChatMemoryWorker } from './queues/post-chat-memory.queue'
10
12
  import type { startRecentActivityTitleRefinementWorker } from './queues/recent-activity-title-refinement.queue'
11
13
  import type { startRegularChatMemoryDigestWorker } from './queues/regular-chat-memory-digest.queue'
12
14
  import type { startSkillExtractionWorker } from './queues/skill-extraction.queue'
15
+ import type { startWorkstreamTitleGenerationWorker } from './queues/workstream-title-generation.queue'
13
16
  import type { RedisConnectionManager } from './redis/connection'
14
17
  import type { isApprovalContinuationRequest } from './runtime/approval-continuation'
18
+ import type { routeWorkstreamChatMessages } from './runtime/chat-request-routing'
15
19
  import type { LotaPlugin } from './runtime/plugin-types'
16
20
  import type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from './runtime/runtime-extensions'
17
21
  import type { attachmentService } from './services/attachment.service'
22
+ import type { documentChunkService } from './services/document-chunk.service'
18
23
  import type { executionPlanService } from './services/execution-plan.service'
19
24
  import type { memoryService } from './services/memory.service'
20
25
  import type { verifyMutatingApproval } from './services/mutating-approval.service'
26
+ import type { organizationMemberService } from './services/organization-member.service'
27
+ import type { organizationService } from './services/organization.service'
21
28
  import type { recentActivityTitleService } from './services/recent-activity-title.service'
22
29
  import type { recentActivityService } from './services/recent-activity.service'
30
+ import type { userService } from './services/user.service'
23
31
  import type { workstreamMessageService } from './services/workstream-message.service'
24
32
  import type { workstreamTitleService } from './services/workstream-title.service'
25
33
  import type {
@@ -39,11 +47,22 @@ interface LotaRuntimeBuiltInWorkers {
39
47
  startRecentActivityTitleRefinementWorker: typeof startRecentActivityTitleRefinementWorker
40
48
  startRegularChatMemoryDigestWorker: typeof startRegularChatMemoryDigestWorker
41
49
  startSkillExtractionWorker: typeof startSkillExtractionWorker
50
+ startWorkstreamTitleGenerationWorker: typeof startWorkstreamTitleGenerationWorker
42
51
  scheduleRecurringConsolidation: typeof scheduleRecurringConsolidation
43
52
  }
44
53
 
54
+ type ArchiveSdkWorkstream = (
55
+ workstreamId: Parameters<typeof workstreamService.updateStatus>[0],
56
+ status?: 'archived',
57
+ ) => ReturnType<typeof workstreamService.updateStatus>
58
+
59
+ type UnarchiveSdkWorkstream = (
60
+ workstreamId: Parameters<typeof workstreamService.updateStatus>[0],
61
+ status?: 'regular',
62
+ ) => ReturnType<typeof workstreamService.updateStatus>
63
+
45
64
  export interface LotaRuntimeConfig {
46
- database: { url: string; namespace: string; database: string; username: string; password: string }
65
+ database: { url: string; namespace: string; username: string; password: string }
47
66
  redis: { url: string }
48
67
  aiGateway: { url: string; key: string; admin?: string; pass?: string; embeddingModel?: string }
49
68
  s3: {
@@ -56,8 +75,14 @@ export interface LotaRuntimeConfig {
56
75
  }
57
76
  firecrawl: { apiKey: string; apiBaseUrl?: string }
58
77
  logging?: { level?: 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal' }
59
- memory?: { searchK?: number }
78
+ memory?: { searchK?: number; embeddingCacheTtlSeconds?: number }
60
79
  workstreams?: LotaWorkstreamConfig
80
+ backgroundProcessing?: {
81
+ memoryExtractionFrequency?: number
82
+ skillExtractionFrequency?: number
83
+ memoryDigestFrequency?: number
84
+ memoryConsolidationFrequency?: number
85
+ }
61
86
 
62
87
  agents: {
63
88
  roster: readonly string[]
@@ -85,9 +110,13 @@ export interface LotaRuntime {
85
110
  redis: RedisConnectionManager
86
111
  closeRedisConnection: () => Promise<void>
87
112
  attachmentService: typeof attachmentService
113
+ documentChunkService: typeof documentChunkService
88
114
  generatedDocumentStorageService: typeof generatedDocumentStorageService
89
115
  memoryService: typeof memoryService
90
116
  verifyMutatingApproval: typeof verifyMutatingApproval
117
+ organizationService: typeof organizationService
118
+ organizationMemberService: typeof organizationMemberService
119
+ userService: typeof userService
91
120
  recentActivityService: typeof recentActivityService
92
121
  recentActivityTitleService: typeof recentActivityTitleService
93
122
  executionPlanService: typeof executionPlanService
@@ -99,6 +128,57 @@ export interface LotaRuntime {
99
128
  isApprovalContinuationRequest: typeof isApprovalContinuationRequest
100
129
  runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
101
130
  }
131
+ lota: {
132
+ organizations: {
133
+ create: typeof organizationService.createOrganization
134
+ upsert: typeof organizationService.upsertOrganization
135
+ get: typeof organizationService.getOrganization
136
+ list: typeof organizationService.listOrganizations
137
+ update: typeof organizationService.updateOrganization
138
+ delete: typeof organizationService.deleteOrganization
139
+ }
140
+ users: {
141
+ upsert: typeof userService.upsertUser
142
+ get: typeof userService.getUser
143
+ list: typeof userService.listUsers
144
+ update: typeof userService.updateUser
145
+ delete: typeof userService.deleteUser
146
+ }
147
+ memberships: {
148
+ add: typeof organizationMemberService.addMembership
149
+ listForOrganization: typeof organizationMemberService.listMembershipsForOrganization
150
+ listForUser: typeof organizationMemberService.listMembershipsForUser
151
+ remove: typeof organizationMemberService.removeMembership
152
+ isMember: typeof organizationMemberService.isMember
153
+ }
154
+ workstreams: {
155
+ create: typeof workstreamService.createWorkstream
156
+ list: typeof workstreamService.listWorkstreams
157
+ get: typeof workstreamService.getWorkstream
158
+ update: typeof workstreamService.updateTitle
159
+ archive: ArchiveSdkWorkstream
160
+ unarchive: UnarchiveSdkWorkstream
161
+ delete: typeof workstreamService.deleteWorkstream
162
+ stop: typeof workstreamService.stopActiveRun
163
+ listMessages: typeof workstreamMessageService.listMessageHistoryPage
164
+ getMessage: (params: { workstreamId: string; messageId: string }) => Promise<unknown>
165
+ sendMessage: (params: {
166
+ workstreamId: string
167
+ organizationId: string
168
+ userId: string
169
+ userName: string
170
+ messages: Parameters<typeof routeWorkstreamChatMessages>[0]
171
+ }) => Promise<Awaited<ReturnType<typeof createWorkstreamTurnStream>>>
172
+ continueApproval: (params: {
173
+ workstreamId: string
174
+ organizationId: string
175
+ userId: string
176
+ userName: string
177
+ messages: Parameters<typeof routeWorkstreamChatMessages>[0]
178
+ }) => Promise<Awaited<ReturnType<typeof createWorkstreamApprovalContinuationStream>>>
179
+ uploadAttachment: typeof attachmentService.uploadWorkstreamAttachment
180
+ }
181
+ }
102
182
  redis: {
103
183
  manager: RedisConnectionManager
104
184
  getConnection: () => ReturnType<RedisConnectionManager['getConnection']>
@@ -118,12 +198,30 @@ export interface LotaRuntime {
118
198
  export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<LotaRuntime> {
119
199
  const { lotaSdkEnvKeys, setEnv } = await import('./config/env-shapes')
120
200
  const { configureLogger } = await import('./config/logger')
201
+ const { publishDatabaseBootstrap } = await import('./db/startup')
202
+ const { computeSchemaFingerprint } = await import('./db/schema-fingerprint')
203
+ const { LOTA_SDK_DATABASE_NAME } = await import('./db/sdk-database')
121
204
  const { SurrealDBService: SurrealDBServiceClass, setDatabaseService } = await import('./db/service')
122
205
  const { createRedisConnectionManager } = await import('./redis/connection')
123
206
  const { setRedisConnectionManager } = await import('./redis/index')
124
207
  const { configureAgents, configureAgentFactory } = await import('./config/agent-defaults')
125
208
  const { configureWorkstreams } = await import('./config/workstream-defaults')
126
209
  const { configureRuntimeExtensions } = await import('./runtime/runtime-extensions')
210
+ const { routeWorkstreamChatMessages } = await import('./runtime/chat-request-routing')
211
+ const { configureBackgroundProcessing } = await import('./config/background-processing')
212
+ const { configureEmbeddingCache } = await import('./ai/embedding-cache')
213
+
214
+ // Resolve config defaults
215
+ const memory = {
216
+ searchK: config.memory?.searchK ?? 6,
217
+ embeddingCacheTtlSeconds: config.memory?.embeddingCacheTtlSeconds ?? 3600,
218
+ }
219
+ const backgroundProcessing = {
220
+ memoryExtractionFrequency: config.backgroundProcessing?.memoryExtractionFrequency ?? 3,
221
+ skillExtractionFrequency: config.backgroundProcessing?.skillExtractionFrequency ?? 5,
222
+ memoryDigestFrequency: config.backgroundProcessing?.memoryDigestFrequency ?? 1,
223
+ memoryConsolidationFrequency: config.backgroundProcessing?.memoryConsolidationFrequency ?? 10,
224
+ }
127
225
 
128
226
  setEnv({
129
227
  AI_GATEWAY_URL: config.aiGateway.url,
@@ -141,7 +239,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
141
239
  ATTACHMENT_URL_EXPIRES_IN: config.s3.attachmentUrlExpiresIn ?? 1800,
142
240
  FIRECRAWL_API_KEY: config.firecrawl.apiKey,
143
241
  FIRECRAWL_API_BASE_URL: config.firecrawl.apiBaseUrl,
144
- MEMORY_SEARCH_K: config.memory?.searchK ?? 6,
242
+ MEMORY_SEARCH_K: memory.searchK,
145
243
  })
146
244
 
147
245
  await configureLogger(config.logging?.level ?? 'info')
@@ -149,7 +247,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
149
247
  const db = new SurrealDBServiceClass({
150
248
  url: config.database.url,
151
249
  namespace: config.database.namespace,
152
- database: config.database.database,
250
+ database: LOTA_SDK_DATABASE_NAME,
153
251
  username: config.database.username,
154
252
  password: config.database.password,
155
253
  })
@@ -157,6 +255,8 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
157
255
 
158
256
  const redisManager = createRedisConnectionManager({ url: config.redis.url })
159
257
  setRedisConnectionManager(redisManager)
258
+ configureEmbeddingCache(redisManager.getConnection(), memory.embeddingCacheTtlSeconds)
259
+ configureBackgroundProcessing(backgroundProcessing)
160
260
 
161
261
  configureAgents({
162
262
  roster: config.agents.roster,
@@ -177,11 +277,15 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
177
277
  }
178
278
 
179
279
  const { attachmentService } = await import('./services/attachment.service')
280
+ const { documentChunkService } = await import('./services/document-chunk.service')
180
281
  const { recentActivityService } = await import('./services/recent-activity.service')
181
282
  const { recentActivityTitleService } = await import('./services/recent-activity-title.service')
182
283
  const { executionPlanService } = await import('./services/execution-plan.service')
183
284
  const { memoryService } = await import('./services/memory.service')
184
285
  const { verifyMutatingApproval } = await import('./services/mutating-approval.service')
286
+ const { organizationMemberService } = await import('./services/organization-member.service')
287
+ const { organizationService } = await import('./services/organization.service')
288
+ const { userService } = await import('./services/user.service')
185
289
  const { workstreamMessageService } = await import('./services/workstream-message.service')
186
290
  const { workstreamService } = await import('./services/workstream.service')
187
291
  const { workstreamTitleService } = await import('./services/workstream-title.service')
@@ -199,6 +303,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
199
303
  const { startRecentActivityTitleRefinementWorker } = await import('./queues/recent-activity-title-refinement.queue')
200
304
  const { startRegularChatMemoryDigestWorker } = await import('./queues/regular-chat-memory-digest.queue')
201
305
  const { startSkillExtractionWorker } = await import('./queues/skill-extraction.queue')
306
+ const { startWorkstreamTitleGenerationWorker } = await import('./queues/workstream-title-generation.queue')
202
307
 
203
308
  configureRuntimeExtensions({
204
309
  adapters: config.runtimeAdapters,
@@ -209,11 +314,8 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
209
314
 
210
315
  const pluginRuntime = config.pluginRuntime ?? {}
211
316
  const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
212
- const schemaFiles = [
213
- ...getBuiltInSchemaFiles(),
214
- ...pluginContributions.flatMap((plugin) => plugin.schemaFiles),
215
- ...(config.extraSchemaFiles ?? []),
216
- ]
317
+ const schemaFiles = [...getBuiltInSchemaFiles(), ...(config.extraSchemaFiles ?? [])]
318
+ const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
217
319
  const contributionEnvKeys = [...lotaSdkEnvKeys, ...pluginContributions.flatMap((plugin) => plugin.envKeys)]
218
320
  const connectPluginDatabases = createPluginDatabaseConnector(pluginRuntime)
219
321
  const builtInWorkers = {
@@ -223,9 +325,91 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
223
325
  startRecentActivityTitleRefinementWorker,
224
326
  startRegularChatMemoryDigestWorker,
225
327
  startSkillExtractionWorker,
328
+ startWorkstreamTitleGenerationWorker,
226
329
  scheduleRecurringConsolidation,
227
330
  } satisfies LotaRuntimeBuiltInWorkers
228
331
 
332
+ const lota = {
333
+ organizations: {
334
+ create: organizationService.createOrganization.bind(organizationService),
335
+ upsert: organizationService.upsertOrganization.bind(organizationService),
336
+ get: organizationService.getOrganization.bind(organizationService),
337
+ list: organizationService.listOrganizations.bind(organizationService),
338
+ update: organizationService.updateOrganization.bind(organizationService),
339
+ delete: organizationService.deleteOrganization.bind(organizationService),
340
+ },
341
+ users: {
342
+ upsert: userService.upsertUser.bind(userService),
343
+ get: userService.getUser.bind(userService),
344
+ list: userService.listUsers.bind(userService),
345
+ update: userService.updateUser.bind(userService),
346
+ delete: userService.deleteUser.bind(userService),
347
+ },
348
+ memberships: {
349
+ add: organizationMemberService.addMembership.bind(organizationMemberService),
350
+ listForOrganization: organizationMemberService.listMembershipsForOrganization.bind(organizationMemberService),
351
+ listForUser: organizationMemberService.listMembershipsForUser.bind(organizationMemberService),
352
+ remove: organizationMemberService.removeMembership.bind(organizationMemberService),
353
+ isMember: organizationMemberService.isMember.bind(organizationMemberService),
354
+ },
355
+ workstreams: {
356
+ create: workstreamService.createWorkstream.bind(workstreamService),
357
+ list: workstreamService.listWorkstreams.bind(workstreamService),
358
+ get: workstreamService.getWorkstream.bind(workstreamService),
359
+ update: workstreamService.updateTitle.bind(workstreamService),
360
+ archive: async (workstreamId, status = 'archived') => await workstreamService.updateStatus(workstreamId, status),
361
+ unarchive: async (workstreamId, status = 'regular') => await workstreamService.updateStatus(workstreamId, status),
362
+ delete: workstreamService.deleteWorkstream.bind(workstreamService),
363
+ stop: workstreamService.stopActiveRun.bind(workstreamService),
364
+ listMessages: workstreamMessageService.listMessageHistoryPage.bind(workstreamMessageService),
365
+ getMessage: async ({ workstreamId, messageId }) => {
366
+ const messages = await workstreamMessageService.listMessages(ensureRecordId(workstreamId, TABLES.WORKSTREAM))
367
+ const message = messages.find((candidate) => candidate.id === messageId)
368
+ if (!message) {
369
+ throw new Error(`Workstream message not found: ${messageId}`)
370
+ }
371
+ return message
372
+ },
373
+ sendMessage: async ({ workstreamId, organizationId, userId, userName, messages }) => {
374
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
375
+ const workstream = await workstreamService.getWorkstream(workstreamRef)
376
+ const routed = routeWorkstreamChatMessages(messages)
377
+ if (routed.kind !== 'turn') {
378
+ throw new Error(routed.kind === 'invalid' ? routed.message : 'Expected a user turn payload.')
379
+ }
380
+
381
+ return await createWorkstreamTurnStream({
382
+ workstream,
383
+ workstreamRef,
384
+ orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
385
+ userRef: ensureRecordId(userId, TABLES.USER),
386
+ userName,
387
+ inputMessage: routed.inputMessage,
388
+ })
389
+ },
390
+ continueApproval: async ({ workstreamId, organizationId, userId, userName, messages }) => {
391
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
392
+ const workstream = await workstreamService.getWorkstream(workstreamRef)
393
+ const routed = routeWorkstreamChatMessages(messages)
394
+ if (routed.kind !== 'approval-continuation') {
395
+ throw new Error(
396
+ routed.kind === 'invalid' ? routed.message : 'Expected approval continuation messages payload.',
397
+ )
398
+ }
399
+
400
+ return await createWorkstreamApprovalContinuationStream({
401
+ workstream,
402
+ workstreamRef,
403
+ orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
404
+ userRef: ensureRecordId(userId, TABLES.USER),
405
+ userName,
406
+ approvalMessages: routed.approvalMessages,
407
+ })
408
+ },
409
+ uploadAttachment: attachmentService.uploadWorkstreamAttachment.bind(attachmentService),
410
+ },
411
+ } satisfies LotaRuntime['lota']
412
+
229
413
  return {
230
414
  services: {
231
415
  database: db,
@@ -233,9 +417,13 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
233
417
  redis: redisManager,
234
418
  closeRedisConnection: async () => await redisManager.closeConnection(),
235
419
  attachmentService,
420
+ documentChunkService,
236
421
  generatedDocumentStorageService,
237
422
  memoryService,
238
423
  verifyMutatingApproval,
424
+ organizationService,
425
+ organizationMemberService,
426
+ userService,
239
427
  recentActivityService,
240
428
  recentActivityTitleService,
241
429
  executionPlanService,
@@ -247,6 +435,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
247
435
  isApprovalContinuationRequest,
248
436
  runWorkstreamTurnInBackground,
249
437
  },
438
+ lota,
250
439
  redis: {
251
440
  manager: redisManager,
252
441
  getConnection: () => redisManager.getConnection(),
@@ -255,7 +444,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
255
444
  },
256
445
  workers: { ...builtInWorkers, ...config.extraWorkers },
257
446
  schemaFiles,
258
- contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles },
447
+ contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles: hostContributionSchemaFiles },
259
448
  config,
260
449
  plugins: pluginRuntime,
261
450
  async connectPluginDatabases() {
@@ -263,6 +452,12 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
263
452
  },
264
453
  async connect() {
265
454
  await db.connect()
455
+ const bunFiles = schemaFiles.map((schemaFile) =>
456
+ schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile),
457
+ )
458
+ await db.applySchemaAndMigrations(bunFiles)
459
+ const schemaFingerprint = await computeSchemaFingerprint(schemaFiles)
460
+ await publishDatabaseBootstrap({ databaseService: db, schemaFingerprint })
266
461
  },
267
462
  async disconnect() {
268
463
  await db.disconnect()
@@ -273,10 +468,12 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
273
468
 
274
469
  function getBuiltInSchemaFiles(): URL[] {
275
470
  return [
471
+ new URL('../infrastructure/schema/00_identity.surql', import.meta.url),
276
472
  new URL('../infrastructure/schema/00_workstream.surql', import.meta.url),
277
473
  new URL('../infrastructure/schema/01_memory.surql', import.meta.url),
278
474
  new URL('../infrastructure/schema/02_execution_plan.surql', import.meta.url),
279
475
  new URL('../infrastructure/schema/03_learned_skill.surql', import.meta.url),
476
+ new URL('../infrastructure/schema/05_recent_activity.surql', import.meta.url),
280
477
  new URL('../infrastructure/schema/04_runtime_bootstrap.surql', import.meta.url),
281
478
  ]
282
479
  }
@@ -32,6 +32,12 @@ function getMemoryConsolidationQueue(): Queue<MemoryConsolidationJob> {
32
32
  return _memoryConsolidationQueue
33
33
  }
34
34
 
35
+ export async function enqueueMemoryConsolidation(job: MemoryConsolidationJob = {}) {
36
+ await getMemoryConsolidationQueue().add('consolidate-turn', job, {
37
+ jobId: job.scopeId ? `consolidate-turn:${job.scopeId}` : undefined,
38
+ })
39
+ }
40
+
35
41
  export async function scheduleRecurringConsolidation() {
36
42
  await getMemoryConsolidationQueue().add(
37
43
  'consolidate',
@@ -0,0 +1,69 @@
1
+ import { Queue, Worker } from 'bullmq'
2
+ import type { Job } from 'bullmq'
3
+
4
+ import { serverLogger } from '../config/logger'
5
+ import { ensureRecordId } from '../db/record-id'
6
+ import { databaseService } from '../db/service'
7
+ import { getRedisConnectionForBullMQ } from '../redis'
8
+ import { workstreamTitleService } from '../services/workstream-title.service'
9
+ import {
10
+ attachWorkerEvents,
11
+ createTracedWorkerProcessor,
12
+ createWorkerShutdown,
13
+ registerShutdownSignals,
14
+ } from '../workers/worker-utils'
15
+ import type { WorkerHandle } from '../workers/worker-utils'
16
+
17
+ interface WorkstreamTitleGenerationJob {
18
+ workstreamId: string
19
+ sourceText: string
20
+ }
21
+
22
+ const WORKSTREAM_TITLE_GENERATION_QUEUE = 'workstream-title-generation'
23
+
24
+ let _workstreamTitleGenerationQueue: Queue<WorkstreamTitleGenerationJob> | null = null
25
+ function getWorkstreamTitleGenerationQueue(): Queue<WorkstreamTitleGenerationJob> {
26
+ if (!_workstreamTitleGenerationQueue) {
27
+ _workstreamTitleGenerationQueue = new Queue<WorkstreamTitleGenerationJob>(WORKSTREAM_TITLE_GENERATION_QUEUE, {
28
+ connection: getRedisConnectionForBullMQ(),
29
+ defaultJobOptions: {
30
+ removeOnComplete: 200,
31
+ removeOnFail: 200,
32
+ attempts: 2,
33
+ backoff: { type: 'exponential', delay: 2_000 },
34
+ },
35
+ })
36
+ }
37
+ return _workstreamTitleGenerationQueue
38
+ }
39
+
40
+ export async function enqueueWorkstreamTitleGeneration(job: WorkstreamTitleGenerationJob) {
41
+ return await getWorkstreamTitleGenerationQueue().add('generate-workstream-title', job, {
42
+ jobId: `workstream-title:${job.workstreamId}`,
43
+ })
44
+ }
45
+
46
+ async function processWorkstreamTitleGenerationJob(job: Job<WorkstreamTitleGenerationJob>): Promise<void> {
47
+ await databaseService.connect()
48
+ const workstreamRef = ensureRecordId(job.data.workstreamId)
49
+ await workstreamTitleService.generateAndPersistTitle(workstreamRef, job.data.sourceText)
50
+ }
51
+
52
+ export function startWorkstreamTitleGenerationWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
53
+ const { registerSignals = import.meta.main } = options
54
+ const worker = new Worker(
55
+ WORKSTREAM_TITLE_GENERATION_QUEUE,
56
+ createTracedWorkerProcessor(WORKSTREAM_TITLE_GENERATION_QUEUE, processWorkstreamTitleGenerationJob),
57
+ { connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 60_000 },
58
+ )
59
+
60
+ attachWorkerEvents(worker, 'Workstream title generation', serverLogger)
61
+
62
+ const shutdown = createWorkerShutdown(worker, 'Workstream title generation', serverLogger)
63
+
64
+ if (registerSignals) {
65
+ registerShutdownSignals({ name: 'Workstream title generation', shutdown, logger: serverLogger })
66
+ }
67
+
68
+ return { worker, shutdown }
69
+ }
@@ -1,22 +1,5 @@
1
- import type { Output, PrepareStepFunction, StopCondition, ToolLoopAgentOnFinishCallback, ToolSet } from 'ai'
2
-
3
- export type ChatMode = 'direct' | 'workstreamMode' | 'fixedWorkstreamMode'
4
-
5
- export interface CreateRoutedAgentOptions<TTools extends ToolSet = ToolSet> {
6
- mode: ChatMode
7
- tools: TTools
8
- extraInstructions?: string
9
- stopWhen?: StopCondition<TTools> | Array<StopCondition<TTools>>
10
- prepareStep?: PrepareStepFunction<TTools>
11
- maxRetries?: number
12
- modelOverride?: { model: unknown; providerOptions?: Record<string, unknown> }
13
- onFinish?: ToolLoopAgentOnFinishCallback<TTools>
14
- }
15
-
16
- export interface CreateHelperToolLoopAgentOptions {
17
- instructions?: string
18
- maxOutputTokens?: number
19
- temperature?: number
20
- output?: Output.Output
21
- maxRetries?: number
22
- }
1
+ export type {
2
+ ChatMode,
3
+ CreateHelperToolLoopAgentOptions,
4
+ CreateRoutedAgentOptions,
5
+ } from '@lota-sdk/shared/runtime/agent-types'
@@ -48,6 +48,7 @@ export interface GenerateHelperStructuredParams<T> extends Omit<GenerateHelperTe
48
48
  tag: string
49
49
  schema: ZodSchema<T>
50
50
  textFallbackParser?: (text: string) => T | null
51
+ normalizeCandidate?: (candidate: unknown) => unknown
51
52
  }
52
53
 
53
54
  function isObject(value: unknown): value is Record<string, unknown> {
@@ -358,6 +359,14 @@ export function createHelperModelRuntime() {
358
359
  return parsed.data
359
360
  }
360
361
 
362
+ if (params.normalizeCandidate && isObject(result.output)) {
363
+ const normalized = params.normalizeCandidate(result.output)
364
+ const normalizedParsed = parseStructuredCandidate({ schema: params.schema, candidate: normalized })
365
+ if (normalizedParsed) {
366
+ return normalizedParsed.data
367
+ }
368
+ }
369
+
361
370
  if (typeof result.text === 'string' && result.text.trim().length > 0) {
362
371
  return parseStructuredTextWithFallbacks({
363
372
  schema: params.schema,
@@ -401,5 +410,3 @@ export function createHelperModelRuntime() {
401
410
 
402
411
  return { generateHelperText, generateHelperStructured }
403
412
  }
404
-
405
- export const llmHelperService = createHelperModelRuntime()
@@ -1,3 +1,5 @@
1
+ import { getBackgroundProcessingConfig, shouldRunAtFrequency } from '../config/background-processing'
2
+
1
3
  export function shouldEnqueueOnboardingPostChatMemory(params: {
2
4
  onboardingActive: boolean
3
5
  userMessageText: string
@@ -9,6 +11,32 @@ export function shouldEnqueueOnboardingPostChatMemory(params: {
9
11
  return params.userMessageText.trim().length > 0 || params.hasAttachmentContext
10
12
  }
11
13
 
12
- export function shouldEnqueueRegularDigestForWorkstream(params: { onboardingActive: boolean }): boolean {
13
- return !params.onboardingActive
14
+ export function shouldEnqueueRegularDigestForWorkstream(params: {
15
+ onboardingActive: boolean
16
+ turnCount?: number
17
+ }): boolean {
18
+ if (params.onboardingActive) return false
19
+ const { memoryDigestFrequency } = getBackgroundProcessingConfig()
20
+ if (typeof params.turnCount === 'number') {
21
+ return shouldRunAtFrequency(params.turnCount, memoryDigestFrequency)
22
+ }
23
+ return true
24
+ }
25
+
26
+ export function shouldEnqueueMemoryExtraction(params: { onboardingActive: boolean; turnCount?: number }): boolean {
27
+ if (params.onboardingActive) return true
28
+ const { memoryExtractionFrequency } = getBackgroundProcessingConfig()
29
+ if (typeof params.turnCount === 'number') {
30
+ return shouldRunAtFrequency(params.turnCount, memoryExtractionFrequency)
31
+ }
32
+ return true
33
+ }
34
+
35
+ export function shouldEnqueueMemoryConsolidation(params: { onboardingActive: boolean; turnCount?: number }): boolean {
36
+ if (params.onboardingActive) return false
37
+ const { memoryConsolidationFrequency } = getBackgroundProcessingConfig()
38
+ if (typeof params.turnCount === 'number') {
39
+ return shouldRunAtFrequency(params.turnCount, memoryConsolidationFrequency)
40
+ }
41
+ return false
14
42
  }
@@ -1,3 +1,10 @@
1
- export function shouldEnqueueSkillExtraction(params: { onboardingActive: boolean }): boolean {
2
- return !params.onboardingActive
1
+ import { getBackgroundProcessingConfig, shouldRunAtFrequency } from '../config/background-processing'
2
+
3
+ export function shouldEnqueueSkillExtraction(params: { onboardingActive: boolean; turnCount?: number }): boolean {
4
+ if (params.onboardingActive) return false
5
+ const { skillExtractionFrequency } = getBackgroundProcessingConfig()
6
+ if (typeof params.turnCount === 'number') {
7
+ return shouldRunAtFrequency(params.turnCount, skillExtractionFrequency)
8
+ }
9
+ return true
3
10
  }
@@ -8,10 +8,12 @@ import { isUniqueIndexConflict } from '../db/memory-store.helpers'
8
8
  import type {
9
9
  AddOptions,
10
10
  ExtractedFact,
11
+ MemoryListScalar,
11
12
  MemoryRecord,
12
13
  MemorySearchResult,
13
14
  MemoryType,
14
15
  Message,
16
+ RelationType,
15
17
  } from '../db/memory-types'
16
18
  import { withOrgMemoryLock } from '../redis/org-memory-lock'
17
19
  import { createHelperModelRuntime } from '../runtime/helper-model'
@@ -413,6 +415,19 @@ class MemoryService {
413
415
  return `Agent memory (${agentName}):\n${agentResult}\n\nGlobal org memory:\n${orgResult}`
414
416
  }
415
417
 
418
+ async listOrganizationMemoryRecords(params: {
419
+ orgId: string
420
+ limit?: number
421
+ memoryType?: MemoryType
422
+ metadataEquals?: Record<string, MemoryListScalar>
423
+ metadataNotEquals?: Record<string, MemoryListScalar>
424
+ sort?: 'createdAtAsc' | 'createdAtDesc'
425
+ }): Promise<MemoryRecord[]> {
426
+ const { orgId, ...listOptions } = params
427
+ const orgMemory = this.getOrgMemory(orgId)
428
+ return await orgMemory.list({ scopeId: scopeId(ORG_SCOPE_PREFIX, orgId), ...listOptions })
429
+ }
430
+
416
431
  async getTopMemories(params: { orgId: string; agentName?: string; limit?: number }): Promise<string | undefined> {
417
432
  const orgMemory = this.getOrgMemory(params.orgId)
418
433
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, params.orgId)
@@ -568,12 +583,14 @@ class MemoryService {
568
583
  memoryType,
569
584
  metadata,
570
585
  importance,
586
+ durability,
571
587
  }: {
572
588
  orgId: string
573
589
  content: string
574
590
  memoryType: MemoryType
575
591
  metadata?: Record<string, unknown>
576
592
  importance?: number
593
+ durability?: MemoryRecord['durability']
577
594
  }): Promise<string> {
578
595
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
579
596
  aiLogger.info`[MEMORY_DEBUG] createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
@@ -583,6 +600,7 @@ class MemoryService {
583
600
  scopeId: orgScopeId,
584
601
  memoryType,
585
602
  importance: importance ?? 1,
603
+ durability,
586
604
  metadata: { orgId, ...metadata },
587
605
  })
588
606
  } catch (error) {
@@ -594,6 +612,23 @@ class MemoryService {
594
612
  }
595
613
  }
596
614
 
615
+ async addOrganizationMemoryRelation({
616
+ orgId,
617
+ fromMemoryId,
618
+ toMemoryId,
619
+ relationType,
620
+ confidence,
621
+ }: {
622
+ orgId: string
623
+ fromMemoryId: string
624
+ toMemoryId: string
625
+ relationType: RelationType
626
+ confidence?: number
627
+ }): Promise<void> {
628
+ const memory = this.getOrgMemory(orgId)
629
+ await memory.addRelation(fromMemoryId, toMemoryId, relationType, confidence)
630
+ }
631
+
597
632
  async createAgentMemory({
598
633
  orgId,
599
634
  agentName,