@lota-sdk/core 0.1.24 → 0.1.26

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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/ai/definitions.ts +5 -59
  3. package/src/ai-gateway/ai-gateway.ts +36 -28
  4. package/src/ai-gateway/cache-headers.ts +9 -0
  5. package/src/config/model-constants.ts +6 -2
  6. package/src/create-runtime.ts +1 -17
  7. package/src/db/memory-types.ts +13 -8
  8. package/src/db/memory.ts +74 -53
  9. package/src/queues/autonomous-job.queue.ts +1 -8
  10. package/src/queues/context-compaction.queue.ts +2 -2
  11. package/src/queues/index.ts +2 -6
  12. package/src/queues/organization-learning.queue.ts +78 -0
  13. package/src/queues/plan-agent-heartbeat.queue.ts +10 -16
  14. package/src/queues/title-generation.queue.ts +62 -0
  15. package/src/runtime/agent-prompt-context.ts +0 -18
  16. package/src/runtime/agent-runtime-policy.ts +9 -2
  17. package/src/runtime/context-compaction-constants.ts +4 -2
  18. package/src/runtime/context-compaction.ts +135 -118
  19. package/src/runtime/memory-pipeline.ts +70 -1
  20. package/src/runtime/memory-prompts-fact.ts +16 -0
  21. package/src/runtime/plugin-resolution.ts +3 -2
  22. package/src/runtime/plugin-types.ts +1 -42
  23. package/src/runtime/post-turn-side-effects.ts +212 -0
  24. package/src/runtime/runtime-config.ts +0 -13
  25. package/src/runtime/runtime-extensions.ts +10 -16
  26. package/src/runtime/runtime-worker-registry.ts +8 -19
  27. package/src/runtime/social-chat-agent-runner.ts +119 -0
  28. package/src/runtime/social-chat-history.ts +110 -0
  29. package/src/runtime/social-chat-prompts.ts +58 -0
  30. package/src/runtime/social-chat.ts +104 -340
  31. package/src/runtime/specialist-runner.ts +18 -0
  32. package/src/runtime/workstream-chat-helpers.ts +19 -0
  33. package/src/runtime/workstream-plan-turn.ts +195 -0
  34. package/src/runtime/workstream-state.ts +11 -8
  35. package/src/runtime/workstream-turn-context.ts +183 -0
  36. package/src/services/autonomous-job.service.ts +1 -8
  37. package/src/services/execution-plan.service.ts +205 -334
  38. package/src/services/index.ts +1 -4
  39. package/src/services/memory.service.ts +54 -44
  40. package/src/services/ownership-dispatcher.service.ts +2 -19
  41. package/src/services/plan-completion-side-effects.ts +80 -0
  42. package/src/services/plan-event-delivery.service.ts +1 -1
  43. package/src/services/plan-executor.service.ts +42 -190
  44. package/src/services/plan-node-spec.ts +60 -0
  45. package/src/services/plan-run-data.ts +88 -0
  46. package/src/services/plan-validator.service.ts +10 -8
  47. package/src/services/workstream-constants.ts +2 -0
  48. package/src/services/workstream-title.service.ts +1 -1
  49. package/src/services/workstream-turn-preparation.service.ts +208 -715
  50. package/src/services/workstream.service.ts +162 -192
  51. package/src/services/workstream.types.ts +12 -44
  52. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
  53. package/src/tools/execution-plan.tool.ts +7 -6
  54. package/src/tools/remember-memory.tool.ts +7 -10
  55. package/src/tools/research-topic.tool.ts +1 -1
  56. package/src/tools/team-think.tool.ts +1 -1
  57. package/src/tools/user-questions.tool.ts +1 -1
  58. package/src/utils/autonomous-job-ids.ts +7 -0
  59. package/src/workers/organization-learning.worker.ts +31 -0
  60. package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
  61. package/src/workers/skill-extraction.runner.ts +2 -2
  62. package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
  63. package/src/queues/regular-chat-memory-digest.config.ts +0 -12
  64. package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
  65. package/src/queues/skill-extraction.config.ts +0 -9
  66. package/src/queues/skill-extraction.queue.ts +0 -27
  67. package/src/queues/workstream-title-generation.queue.ts +0 -33
  68. package/src/services/context-enrichment.service.ts +0 -33
  69. package/src/services/coordination-registry.service.ts +0 -117
  70. package/src/services/domain-agent-executor.service.ts +0 -71
  71. package/src/services/memory-assessment.service.ts +0 -44
  72. package/src/services/playbook-registry.service.ts +0 -67
  73. package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
  74. package/src/workers/skill-extraction.worker.ts +0 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -32,7 +32,7 @@
32
32
  "@chat-adapter/slack": "^4.23.0",
33
33
  "@chat-adapter/state-ioredis": "^4.23.0",
34
34
  "@logtape/logtape": "^2.0.5",
35
- "@lota-sdk/shared": "0.1.24",
35
+ "@lota-sdk/shared": "0.1.26",
36
36
  "@mendable/firecrawl-js": "^4.18.0",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.141",
@@ -296,65 +296,11 @@ export const memr3Rule = defineRule({
296
296
  name: 'memr3',
297
297
  instructions: `# MemR3 Evidence-Gap Protocol
298
298
 
299
- Use the MemR3 retrieval loop for every request.
300
-
301
- ## Evidence-Gap Tracker
302
-
303
- - Maintain two explicit lists:
304
- - **Evidence**: a fenced code block starting with \`evidence\`.
305
- - **Gaps**: a fenced code block starting with \`gaps\`.
306
- - Evidence must be grounded; never speculate or include missing info in the evidence block.
307
-
308
- ## What Counts As A Gap
309
-
310
- Treat the \`gaps\` block as **specific, queryable blocking gaps only**: missing information that materially changes the
311
- recommendation, action, or confidence level, or makes an answer unsafe/unreliable.
312
-
313
- - Each gap should be atomic enough to resolve with one targeted retrieval query or one user question.
314
- - If you can provide a useful answer with reasonable startup-stage assumptions and the remaining unknowns would not
315
- materially change it, set \`gaps\` to \`- None\` and proceed.
316
- - Non-blocking unknowns should be handled as:
317
- - explicit assumptions in the answer, and/or
318
- - 1-3 follow-up questions at the end (do not block the main recommendation).
319
-
320
- ## Retrieval Loop
321
-
322
- 1. **Recall**: Use internal knowledge and the current conversation.
323
- 2. **Retrieve**: If a retrieval tool is available, call it to fill gaps.
324
- - Prefer \`memorySearch\` for stored memories (semantic retrieval + graph expansion).
325
- - If retrieved memory context is already provided in-system for this turn and blocking gaps are already closed, you
326
- may skip \`memorySearch\`.
327
- - If any blocking gap remains after reviewing provided context, retrieval is mandatory.
328
- - Use web tools only if you have them.
329
- - If multiple gaps are independent, issue multiple tool calls concurrently in the same turn.
330
- - For web research, call multiple \`researchTopic\` instances in parallel for different sub-questions.
331
- 3. **Reflect**: Update the evidence and gaps blocks.
332
- - Remove resolved gaps.
333
- - Split vague gaps into smaller, searchable gaps.
334
- - Drop gaps that no longer matter because the answer is robust under explicit assumptions.
335
- 4. **Iterate**: If the gaps block still contains blocking items, issue targeted new queries in parallel and repeat.
336
- - Keep queries short (5-15 tokens) and specific.
337
- - Limit to 3 retrieve/reflect cycles unless the user explicitly asks for deeper research.
338
- - Stop early once remaining unknowns are non-material; do not retrieve for completeness alone.
339
- 5. **Answer**: Answer once blocking gaps are \`None\`.
340
- - If retrieval returns no useful evidence and the answer is still robust under stated assumptions, proceed and make
341
- those assumptions explicit rather than stalling.
342
- - Do not say "memory search yielded nothing" or mention tool names; translate it into plain-language uncertainty.
343
-
344
- ## Output Format
345
-
346
- \`\`\`evidence
347
- - ...
348
- \`\`\`
349
-
350
- \`\`\`gaps
351
- - ... (or "None")
352
- \`\`\`
353
-
354
- - Provide your response using your normal response format.
355
- - Never output raw tool payloads (JSON, object dumps, or memory IDs like \`memory:...\`) in the final answer; summarize
356
- them in plain language.
357
- - If blocking gaps remain after the loop, do not answer; ask for the missing information instead.`,
299
+ When a factual answer depends on information you may not have:
300
+ 1. Search memory and knowledge before responding.
301
+ 2. Cite retrieved evidence; state remaining gaps explicitly.
302
+ 3. Never fabricate facts; say "I don't have information on that" when appropriate.
303
+ 4. Ask the user only for missing information that would materially change the answer.`,
358
304
  })
359
305
 
360
306
  export const domainReasoningFallbackRule = defineRule({
@@ -5,7 +5,7 @@ import type { LanguageModelMiddleware } from 'ai'
5
5
 
6
6
  import { getRuntimeConfig } from '../runtime/runtime-config'
7
7
  import { isRecord, readString } from '../utils/string'
8
- import { buildAiGatewayCacheHeaders } from './cache-headers'
8
+ import { buildAiGatewayCacheHeaders, toAiGatewayCacheKeyPart } from './cache-headers'
9
9
 
10
10
  type AiGatewayLanguageModel = Parameters<typeof wrapLanguageModel>[0]['model']
11
11
  type AiGatewayExtraParams = Record<string, unknown>
@@ -29,15 +29,6 @@ const OPENROUTER_RESPONSE_HEALING_EXTRA_PARAMS = {
29
29
  plugins: [{ id: 'response-healing' }],
30
30
  } as const satisfies AiGatewayExtraParams
31
31
 
32
- function toAiGatewayCacheKeyPart(value: string): string {
33
- const normalized = value
34
- .trim()
35
- .toLowerCase()
36
- .replace(/[^a-z0-9:_-]+/g, '-')
37
- .replace(/-+/g, '-')
38
- return normalized.replace(/^-+|-+$/g, '') || 'request'
39
- }
40
-
41
32
  function mergeAiGatewayHeaders(
42
33
  existingHeaders: AiGatewayCallOptions['headers'] | undefined,
43
34
  additionalHeaders: Record<string, string>,
@@ -64,16 +55,6 @@ function parseAiGatewayJsonRequestBody(body: BodyInit | null | undefined): Recor
64
55
  return isRecord(parsed) ? parsed : null
65
56
  }
66
57
 
67
- function isAiGatewayOpenAIModelRequest(body: BodyInit | null | undefined): boolean {
68
- const parsed = parseAiGatewayJsonRequestBody(body)
69
- return readString(parsed?.model)?.startsWith('openai/') ?? false
70
- }
71
-
72
- function hasAiGatewayPromptCacheRetention(body: BodyInit | null | undefined): boolean {
73
- const parsed = parseAiGatewayJsonRequestBody(body)
74
- return readString(parsed?.prompt_cache_retention) !== null
75
- }
76
-
77
58
  function withDefaultAiGatewayCacheHeaders(params: AiGatewayCallOptions, modelId: string): AiGatewayCallOptions {
78
59
  return {
79
60
  ...params,
@@ -343,22 +324,49 @@ export function injectAiGatewayOpenAIPromptCacheRetentionRequestBody(
343
324
 
344
325
  function createAiGatewayFetch(extraParams?: AiGatewayExtraParams): typeof fetch {
345
326
  const fetchWithMutations = (input: RequestInfo | URL, init?: RequestInit | BunFetchRequestInit) => {
346
- const bodyWithPromptCacheRetention = injectAiGatewayOpenAIPromptCacheRetentionRequestBody(init?.body)
347
- const body =
348
- extraParams !== undefined
349
- ? injectAiGatewayExtraParamsRequestBody(bodyWithPromptCacheRetention, extraParams)
350
- : bodyWithPromptCacheRetention
327
+ const parsedBody = parseAiGatewayJsonRequestBody(init?.body)
328
+ let nextBody = init?.body
329
+ let nextParsedBody = parsedBody
330
+
331
+ if (
332
+ nextParsedBody &&
333
+ readString(nextParsedBody.model)?.startsWith('openai/') &&
334
+ !readString(nextParsedBody.prompt_cache_retention)
335
+ ) {
336
+ nextParsedBody = { ...nextParsedBody, prompt_cache_retention: OPENAI_PROMPT_CACHE_RETENTION }
337
+ nextBody = JSON.stringify(nextParsedBody)
338
+ }
339
+
340
+ if (nextParsedBody && extraParams !== undefined) {
341
+ nextParsedBody = {
342
+ ...nextParsedBody,
343
+ extra_params: isRecord(nextParsedBody.extra_params)
344
+ ? { ...nextParsedBody.extra_params, ...extraParams }
345
+ : { ...extraParams },
346
+ }
347
+ nextBody = JSON.stringify(nextParsedBody)
348
+ }
351
349
 
352
350
  const headers = new Headers(init?.headers)
353
- if (extraParams !== undefined || (isAiGatewayOpenAIModelRequest(body) && hasAiGatewayPromptCacheRetention(body))) {
351
+ if (
352
+ extraParams !== undefined ||
353
+ (readString(nextParsedBody?.model)?.startsWith('openai/') &&
354
+ readString(nextParsedBody?.prompt_cache_retention) !== null)
355
+ ) {
354
356
  // Bifrost only forwards provider-specific extra params when passthrough is enabled.
355
357
  headers.set(AI_GATEWAY_EXTRA_PARAMS_HEADER, 'true')
356
358
  }
357
359
 
358
- return globalThis.fetch(input, { ...init, headers, body })
360
+ return globalThis.fetch(input, { ...init, headers, body: nextBody })
361
+ }
362
+
363
+ const preconnect = globalThis.fetch.preconnect
364
+
365
+ if (typeof preconnect !== 'function') {
366
+ return fetchWithMutations as typeof fetch
359
367
  }
360
368
 
361
- return Object.assign(fetchWithMutations, { preconnect: globalThis.fetch.preconnect.bind(globalThis.fetch) })
369
+ return Object.assign(fetchWithMutations, { preconnect: preconnect.bind(globalThis.fetch) })
362
370
  }
363
371
 
364
372
  function createAiGatewayProvider(extraParams?: AiGatewayExtraParams) {
@@ -7,6 +7,15 @@ export const AI_GATEWAY_STRICT_SEMANTIC_CACHE_THRESHOLD = 0.975
7
7
 
8
8
  export type AiGatewayCacheType = 'direct' | 'semantic'
9
9
 
10
+ export function toAiGatewayCacheKeyPart(value: string): string {
11
+ const normalized = value
12
+ .trim()
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9:_-]+/g, '-')
15
+ .replace(/-+/g, '-')
16
+ return normalized.replace(/^-+|-+$/g, '') || 'request'
17
+ }
18
+
10
19
  export function buildAiGatewayCacheHeaders(
11
20
  cacheKey: string,
12
21
  ttl?: string,
@@ -2,15 +2,19 @@ export {
2
2
  AI_GATEWAY_REASONING_SUMMARY_LEVEL,
3
3
  OPENAI_HIGH_REASONING_PROVIDER_OPTIONS,
4
4
  OPENAI_REASONING_MODEL_ID,
5
- OPENROUTER_DELEGATED_REASONING_MODEL_ID,
6
5
  OPENROUTER_FAST_REASONING_MODEL_ID,
6
+ OPENROUTER_GEMINI_FLASH_MODEL_ID,
7
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
8
8
  OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
9
9
  OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
10
10
  OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
11
- OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
12
11
  OPENROUTER_STRUCTURED_REASONING_MODEL_ID,
13
12
  OPENROUTER_TEAM_AGENT_MODEL_ID,
14
13
  OPENROUTER_WEB_RESEARCH_MODEL_ID,
15
14
  OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS,
16
15
  } from '@lota-sdk/shared'
16
+
17
+ // Both aliases point to the same underlying model. Keep the names separate so
18
+ // SDK system agents and host delegated agents can diverge independently later.
19
+ export { OPENROUTER_GEMINI_FLASH_MODEL_ID as OPENROUTER_STRUCTURED_HELPER_MODEL_ID } from '@lota-sdk/shared'
20
+ export { OPENROUTER_GEMINI_FLASH_MODEL_ID as OPENROUTER_DELEGATED_REASONING_MODEL_ID } from '@lota-sdk/shared'
@@ -33,10 +33,8 @@ import type { attachmentService } from './services/attachment.service'
33
33
  import { attachmentService as attachmentServiceSingleton } from './services/attachment.service'
34
34
  import type { autonomousJobService } from './services/autonomous-job.service'
35
35
  import { autonomousJobService as autonomousJobServiceSingleton } from './services/autonomous-job.service'
36
- import { coordinationRegistryService as coordinationRegistryServiceSingleton } from './services/coordination-registry.service'
37
36
  import type { documentChunkService } from './services/document-chunk.service'
38
37
  import { documentChunkService as documentChunkServiceSingleton } from './services/document-chunk.service'
39
- import { domainAgentExecutorService } from './services/domain-agent-executor.service'
40
38
  import type { executionPlanService } from './services/execution-plan.service'
41
39
  import { executionPlanService as executionPlanServiceSingleton } from './services/execution-plan.service'
42
40
  import type { memoryService } from './services/memory.service'
@@ -50,7 +48,6 @@ import type { organizationService } from './services/organization.service'
50
48
  import { organizationService as organizationServiceSingleton } from './services/organization.service'
51
49
  import type { planAgentQueryService } from './services/plan-agent-query.service'
52
50
  import { planAgentQueryService as planAgentQueryServiceSingleton } from './services/plan-agent-query.service'
53
- import { playbookRegistryService } from './services/playbook-registry.service'
54
51
  import type { recentActivityTitleService } from './services/recent-activity-title.service'
55
52
  import { recentActivityTitleService as recentActivityTitleServiceSingleton } from './services/recent-activity-title.service'
56
53
  import type { recentActivityService } from './services/recent-activity.service'
@@ -143,7 +140,6 @@ export interface LotaRuntime {
143
140
  isApprovalContinuationRequest: typeof isApprovalContinuationRequest
144
141
  runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
145
142
  triggerPlanNodeTurn: typeof triggerPlanNodeTurn
146
- syncPlaybookTemplates: typeof playbookRegistryService.syncPlaybookTemplates
147
143
  }
148
144
  lota: {
149
145
  organizations: {
@@ -237,7 +233,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
237
233
  const redisManager = createRedisConnectionManager({ url: runtimeConfig.redis.url })
238
234
  setRedisConnectionManager(redisManager)
239
235
  configureEmbeddingCache(redisManager.getConnection(), runtimeConfig.memory.embeddingCacheTtlSeconds)
240
- configureBackgroundProcessing(runtimeConfig.backgroundProcessing)
236
+ configureBackgroundProcessing()
241
237
  configureSocialChatHistory({ keyPrefix: runtimeConfig.socialChat?.historyRedisKeyPrefix })
242
238
 
243
239
  const socialChatAgentId = runtimeConfig.socialChat?.agentId?.trim() || 'socialChat'
@@ -273,21 +269,10 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
273
269
  })
274
270
 
275
271
  const pluginRuntime = runtimeConfig.pluginRuntime ?? {}
276
- domainAgentExecutorService.configure(pluginRuntime)
277
272
  if (runtimeConfig.graphDesigner) {
278
273
  configureGraphDesigner(runtimeConfig.graphDesigner)
279
274
  }
280
275
 
281
- for (const [pluginRef, plugin] of Object.entries(pluginRuntime)) {
282
- const signals = plugin.contributions.signals
283
- if (signals && signals.length > 0) {
284
- coordinationRegistryServiceSingleton.register(pluginRef, [...signals])
285
- }
286
- }
287
- coordinationRegistryServiceSingleton.validate()
288
- // Collect playbook contributions early to fail fast on misconfiguration
289
- playbookRegistryService.collectPlaybooks()
290
-
291
276
  const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
292
277
  const schemaFiles = [...getBuiltInSchemaFiles(), ...(runtimeConfig.extraSchemaFiles ?? [])]
293
278
  const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
@@ -417,7 +402,6 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
417
402
  isApprovalContinuationRequest: isApprovalContinuationRequestSingleton,
418
403
  runWorkstreamTurnInBackground: runWorkstreamTurnInBackgroundSingleton,
419
404
  triggerPlanNodeTurn: triggerPlanNodeTurnSingleton,
420
- syncPlaybookTemplates: playbookRegistryService.syncPlaybookTemplates.bind(playbookRegistryService),
421
405
  },
422
406
  lota,
423
407
  redis: {
@@ -14,6 +14,9 @@ export type MemoryType = z.infer<typeof MemoryTypeSchema>
14
14
  export const DurabilitySchema = z.enum(['core', 'standard', 'ephemeral'])
15
15
  export type Durability = z.infer<typeof DurabilitySchema>
16
16
 
17
+ export const MemoryImportanceClassificationSchema = z.enum(['durable', 'transient', 'uncertain'])
18
+ export type MemoryImportanceClassification = z.infer<typeof MemoryImportanceClassificationSchema>
19
+
17
20
  export const MemoryEventSchema = z.enum(['ADD', 'UPDATE', 'DELETE', 'NONE'])
18
21
  export type MemoryEvent = z.infer<typeof MemoryEventSchema>
19
22
 
@@ -103,6 +106,15 @@ const ExtractedFactSchema = z.object({
103
106
  .describe(
104
107
  'core: business decisions, technical architecture, confirmed requirements. standard: general facts, moderate inferences. ephemeral: preferences, one-off interactions, formatting choices.',
105
108
  ),
109
+ importance: z
110
+ .number()
111
+ .min(0)
112
+ .max(1)
113
+ .describe('Long-term usefulness score from 0 to 1 for storing this fact as memory.'),
114
+ classification: MemoryImportanceClassificationSchema.describe(
115
+ 'Whether this fact is durable enough for long-term memory.',
116
+ ),
117
+ rationale: z.string().min(1).describe('Concise rationale for the importance and classification.'),
106
118
  })
107
119
 
108
120
  export type ExtractedFact = z.infer<typeof ExtractedFactSchema>
@@ -186,14 +198,7 @@ const MemoryDeltaItemSchema = z
186
198
  export const MemoryDeltaSchema = z
187
199
  .object({ deltas: z.array(MemoryDeltaItemSchema).describe('Classification output for each new fact.') })
188
200
  .strict()
189
- export const MemoryImportanceAssessmentSchema = z
190
- .object({
191
- importance: z.number().min(0).max(1).describe('Long-term usefulness score from 0 to 1 for storing this memory.'),
192
- durability: DurabilitySchema.describe('Expected durability for this memory.'),
193
- classification: z.enum(['durable', 'transient', 'uncertain']).describe('Durability classification for storage.'),
194
- rationale: z.string().min(1).describe('Concise rationale for the score/classification.'),
195
- })
196
- .strict()
201
+ export type MemoryDeltaOutput = z.infer<typeof MemoryDeltaSchema>
197
202
  export interface Message {
198
203
  role: 'user' | 'agent'
199
204
  content: string
package/src/db/memory.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { z } from 'zod'
2
+
1
3
  import { aiLogger } from '../config/logger'
2
4
  import type { CreateHelperAgentFn } from '../runtime/helper-model'
3
5
  import { createHelperModelRuntime } from '../runtime/helper-model'
@@ -7,6 +9,7 @@ import {
7
9
  compileMemoryUpdatesFromDelta,
8
10
  createMemoryActionPlan,
9
11
  postProcessMemoryFacts,
12
+ projectMemoryDeltaToScope,
10
13
  } from '../runtime/memory-pipeline'
11
14
  import { getFactRetrievalMessages } from '../runtime/memory-prompts-fact'
12
15
  import { parseMessages } from '../runtime/memory-prompts-parse'
@@ -50,6 +53,12 @@ interface PreparedScopeUpdate {
50
53
  existingMemories: Array<{ id: string; text: string }>
51
54
  }
52
55
 
56
+ interface ScopedExistingMemories {
57
+ options: AddOptions
58
+ existingMemories: Array<{ id: string; text: string }>
59
+ scopeMemoryIdsByUnionId: Record<string, string[]>
60
+ }
61
+
53
62
  export class Memory {
54
63
  private store: SurrealMemoryStore
55
64
  private createAgent: CreateHelperAgentFn
@@ -72,6 +81,12 @@ export class Memory {
72
81
  return sections.join('\n\n')
73
82
  }
74
83
 
84
+ private buildMemoryUnionId(text: string): string | null {
85
+ const normalized = this.normalizeMemoryDeltaText(text, MEMORY_DELTA_MEMORY_TEXT_MAX_CHARS)
86
+ if (!normalized) return null
87
+ return `union_${new Bun.CryptoHasher('sha256').update(normalized).digest('hex')}`
88
+ }
89
+
75
90
  async insert(
76
91
  content: string,
77
92
  options: {
@@ -166,33 +181,6 @@ export class Memory {
166
181
  return this.store.getStaleMemories(scopeId, limit)
167
182
  }
168
183
 
169
- async add(
170
- messages: Message[],
171
- options: AddOptions,
172
- extractionOptions?: { customPrompt?: string; maxFacts?: number },
173
- ): Promise<void> {
174
- const facts = await this.extractFactsFromMessages(messages, extractionOptions)
175
- if (facts.length === 0) return
176
-
177
- aiLogger.debug`Extracted ${facts.length} facts from conversation`
178
-
179
- await this.applyFactsToScope(facts, options)
180
- }
181
-
182
- async addMultiScope(
183
- messages: Message[],
184
- scopes: AddOptions[],
185
- extractionOptions?: { customPrompt?: string; maxFacts?: number },
186
- ): Promise<void> {
187
- if (scopes.length === 0) return
188
- const facts = await this.extractFactsFromMessages(messages, extractionOptions)
189
- if (facts.length === 0) return
190
-
191
- aiLogger.debug`Extracted ${facts.length} facts, applying to ${scopes.length} scopes`
192
-
193
- await this.applyFactsToScopes(facts, scopes)
194
- }
195
-
196
184
  async extractFactsFromMessages(
197
185
  messages: Message[],
198
186
  extractionOptions?: { customPrompt?: string; maxFacts?: number },
@@ -220,11 +208,52 @@ export class Memory {
220
208
  async prepareFactsToScopes(facts: ExtractedFact[], scopes: AddOptions[]): Promise<PreparedScopeUpdate[]> {
221
209
  if (facts.length === 0 || scopes.length === 0) return []
222
210
 
223
- const prepared: PreparedScopeUpdate[] = []
224
- for (const scopeOptions of scopes) {
225
- prepared.push(await this.prepareFactsForScope(facts, scopeOptions))
211
+ const factMaps = buildMemoryFactMaps(facts)
212
+ const factContents = facts.map((fact) => fact.content)
213
+ const scopePayloads: ScopedExistingMemories[] = await Promise.all(
214
+ scopes.map(async (scopeOptions) => {
215
+ const existingMemories = await this.store.list({
216
+ scopeId: scopeOptions.scopeId,
217
+ memoryType: scopeOptions.memoryType,
218
+ })
219
+ const normalizedMemories = existingMemories.map((memory) => ({ id: memory.id, text: memory.content }))
220
+ const scopeMemoryIdsByUnionId: Record<string, string[]> = {}
221
+ for (const memory of normalizedMemories) {
222
+ const unionId = this.buildMemoryUnionId(memory.text)
223
+ if (!unionId) continue
224
+ ;(scopeMemoryIdsByUnionId[unionId] ??= []).push(memory.id)
225
+ }
226
+ return { options: scopeOptions, existingMemories: normalizedMemories, scopeMemoryIdsByUnionId }
227
+ }),
228
+ )
229
+ const unionMemories = new Map<string, { id: string; text: string }>()
230
+ for (const scopePayload of scopePayloads) {
231
+ for (const memory of scopePayload.existingMemories) {
232
+ const unionId = this.buildMemoryUnionId(memory.text)
233
+ if (!unionId || unionMemories.has(unionId)) continue
234
+ const normalizedText = this.normalizeMemoryDeltaText(memory.text, MEMORY_DELTA_MEMORY_TEXT_MAX_CHARS)
235
+ if (!normalizedText) continue
236
+ unionMemories.set(unionId, { id: unionId, text: normalizedText })
237
+ }
226
238
  }
227
- return prepared
239
+
240
+ const delta = await this.determineDelta([...unionMemories.values()], factContents)
241
+ return scopePayloads.map(({ options, existingMemories, scopeMemoryIdsByUnionId }) => ({
242
+ options,
243
+ updates: MemoryUpdateSchema.parse(
244
+ compileMemoryUpdatesFromDelta({
245
+ existingMemories,
246
+ newFacts: factContents,
247
+ delta: projectMemoryDeltaToScope({
248
+ delta,
249
+ scopeMemoryIds: existingMemories.map((memory) => memory.id),
250
+ scopeMemoryIdsByUnionId,
251
+ }),
252
+ }),
253
+ ),
254
+ factMaps,
255
+ existingMemories,
256
+ }))
228
257
  }
229
258
 
230
259
  async applyPreparedScopeUpdates(prepared: PreparedScopeUpdate[]): Promise<void> {
@@ -235,23 +264,6 @@ export class Memory {
235
264
  }
236
265
  }
237
266
 
238
- private async applyFactsToScope(facts: ExtractedFact[], options: AddOptions): Promise<void> {
239
- const prepared = await this.prepareFactsForScope(facts, options)
240
- await this.applyPreparedScopeUpdates([prepared])
241
- }
242
-
243
- private async prepareFactsForScope(facts: ExtractedFact[], options: AddOptions): Promise<PreparedScopeUpdate> {
244
- const factMaps = buildMemoryFactMaps(facts)
245
-
246
- const existingMemories = await this.store.list({ scopeId: options.scopeId, memoryType: options.memoryType })
247
- const memoryDeltaInput = existingMemories.map((m) => ({ id: m.id, text: m.content }))
248
-
249
- const factContents = facts.map((f) => f.content)
250
- const updates = await this.determineUpdates(memoryDeltaInput, factContents)
251
-
252
- return { options, updates, factMaps, existingMemories: memoryDeltaInput }
253
- }
254
-
255
267
  private async extractFacts(
256
268
  parsedMessages: string,
257
269
  extractionOptions?: { customPrompt?: string; maxFacts?: number },
@@ -284,12 +296,21 @@ export class Memory {
284
296
  }
285
297
  }
286
298
 
287
- private async determineUpdates(
299
+ private async determineDelta(
288
300
  existingMemories: { id: string; text: string }[],
289
301
  newFacts: string[],
290
- ): Promise<MemoryUpdateOutput> {
302
+ ): Promise<{ deltas: Array<z.infer<typeof MemoryDeltaSchema>['deltas'][number]> }> {
291
303
  if (existingMemories.length === 0) {
292
- return { memory: newFacts.map((fact, index) => ({ id: `new_${index}`, text: fact, event: 'ADD' as const })) }
304
+ return {
305
+ deltas: newFacts.map((fact) => ({
306
+ fact,
307
+ classification: 'new' as const,
308
+ targetMemoryIds: [],
309
+ invalidateTargetIds: [],
310
+ relations: [],
311
+ rationale: 'No existing memories in scope.',
312
+ })),
313
+ }
293
314
  }
294
315
 
295
316
  const candidateMemories = this.selectDeltaCandidateMemories(existingMemories, newFacts)
@@ -306,8 +327,7 @@ export class Memory {
306
327
  messages: [{ role: 'user', content: userPrompt }],
307
328
  schema: MemoryDeltaSchema,
308
329
  })
309
- const compiled = compileMemoryUpdatesFromDelta({ existingMemories, newFacts, delta: deltas })
310
- return MemoryUpdateSchema.parse(compiled)
330
+ return deltas
311
331
  } catch (error) {
312
332
  aiLogger.error`Failed to determine memory updates: ${error}`
313
333
  throw error
@@ -431,6 +451,7 @@ export class Memory {
431
451
  updates,
432
452
  memoryType: options.memoryType,
433
453
  explicitImportance: options.importance,
454
+ extractedImportanceByKey: factMaps.extractedImportanceByKey,
434
455
  confidenceByKey: factMaps.confidenceByKey,
435
456
  durabilityByKey: factMaps.durabilityByKey,
436
457
  categoryByKey: factMaps.categoryByKey,
@@ -5,6 +5,7 @@ import { serverLogger } from '../config/logger'
5
5
  import { databaseService } from '../db/service'
6
6
  import { autonomousJobService } from '../services/autonomous-job.service'
7
7
  import { queueJobService } from '../services/queue-job.service'
8
+ import { buildAutonomousAtJobId } from '../utils/autonomous-job-ids'
8
9
  import type { WorkerHandle } from '../workers/worker-utils'
9
10
  import { DEFAULT_JOB_RETENTION } from '../workers/worker-utils'
10
11
  import { createQueueFactory } from './queue-factory'
@@ -43,14 +44,6 @@ function buildAutonomousSchedulerId(autonomousJobId: string): string {
43
44
  return `autonomous:${autonomousJobId}`
44
45
  }
45
46
 
46
- function encodeBullmqId(raw: string): string {
47
- return Buffer.from(raw).toString('base64url')
48
- }
49
-
50
- export function buildAutonomousAtJobId(autonomousJobId: string): string {
51
- return `autonomous-at-${encodeBullmqId(autonomousJobId)}`
52
- }
53
-
54
47
  export async function enqueueAutonomousJobRun(params: {
55
48
  payload: AutonomousJobQueuePayload
56
49
  delayMs?: number
@@ -18,11 +18,11 @@ async function processContextCompactionJob(job: Job<ContextCompactionJob>): Prom
18
18
 
19
19
  const { entityId, contextSize } = job.data
20
20
  const workstreamRef = ensureRecordId(entityId, TABLES.WORKSTREAM)
21
- await workstreamService.markCompacting(workstreamRef)
21
+ await workstreamService.setCompacting(workstreamRef, true)
22
22
  try {
23
23
  await contextCompactionService.compactWorkstreamHistory({ workstreamId: workstreamRef, contextSize })
24
24
  } finally {
25
- await workstreamService.clearCompacting(workstreamRef)
25
+ await workstreamService.setCompacting(workstreamRef, false)
26
26
  }
27
27
  }
28
28
 
@@ -4,12 +4,8 @@ export * from './context-compaction.queue'
4
4
  export * from './delayed-node-promotion.queue'
5
5
  export * from './document-processor.queue'
6
6
  export * from './memory-consolidation.queue'
7
+ export * from './organization-learning.queue'
7
8
  export * from './plan-agent-heartbeat.queue'
8
9
  export * from './plan-scheduler.queue'
9
10
  export * from './post-chat-memory.queue'
10
- export * from './recent-activity-title-refinement.queue'
11
- export * from './regular-chat-memory-digest.config'
12
- export * from './regular-chat-memory-digest.queue'
13
- export * from './skill-extraction.config'
14
- export * from './skill-extraction.queue'
15
- export * from './workstream-title-generation.queue'
11
+ export * from './title-generation.queue'
@@ -0,0 +1,78 @@
1
+ import { getWorkerPath, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
2
+ import { createQueueFactory } from './queue-factory'
3
+
4
+ export const ORGANIZATION_LEARNING_QUEUE = 'organization-learning'
5
+
6
+ const ORGANIZATION_LEARNING_DELAY_MS = 15 * 60 * 1000
7
+
8
+ // This queue merges the two organization-scoped background learning jobs:
9
+ // delayed regular-chat memory digestion and skill extraction.
10
+
11
+ export interface RegularChatMemoryDigestJob {
12
+ kind: 'regular-chat-memory-digest'
13
+ orgId: string
14
+ }
15
+
16
+ export interface SkillExtractionJob {
17
+ kind: 'skill-extraction'
18
+ orgId: string
19
+ }
20
+
21
+ export type OrganizationLearningJob = RegularChatMemoryDigestJob | SkillExtractionJob
22
+
23
+ function buildOrganizationLearningDeduplicationId(kind: OrganizationLearningJob['kind'], orgId: string): string {
24
+ return kind === 'regular-chat-memory-digest' ? `regular-chat-digest:${orgId}` : `skill-extraction:${orgId}`
25
+ }
26
+
27
+ export function buildRegularChatMemoryDigestDeduplicationId(orgId: string): string {
28
+ return buildOrganizationLearningDeduplicationId('regular-chat-memory-digest', orgId)
29
+ }
30
+
31
+ export function buildRegularChatMemoryDigestJobOptions(orgId: string) {
32
+ return {
33
+ delay: ORGANIZATION_LEARNING_DELAY_MS,
34
+ deduplication: { id: buildRegularChatMemoryDigestDeduplicationId(orgId) },
35
+ }
36
+ }
37
+
38
+ export function buildSkillExtractionDeduplicationId(orgId: string): string {
39
+ return buildOrganizationLearningDeduplicationId('skill-extraction', orgId)
40
+ }
41
+
42
+ export function buildSkillExtractionJobOptions(orgId: string) {
43
+ return { delay: ORGANIZATION_LEARNING_DELAY_MS, deduplication: { id: buildSkillExtractionDeduplicationId(orgId) } }
44
+ }
45
+
46
+ const organizationLearningQueue = createQueueFactory<OrganizationLearningJob>({
47
+ name: ORGANIZATION_LEARNING_QUEUE,
48
+ displayName: 'Organization learning',
49
+ jobName: 'organization-learning',
50
+ concurrency: 4,
51
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
52
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5_000 } },
53
+ processorPath: getWorkerPath('organization-learning.worker.ts'),
54
+ })
55
+
56
+ export function enqueueRegularChatMemoryDigest(job: Omit<RegularChatMemoryDigestJob, 'kind'>) {
57
+ return organizationLearningQueue.enqueue(
58
+ { kind: 'regular-chat-memory-digest', ...job },
59
+ buildRegularChatMemoryDigestJobOptions(job.orgId),
60
+ )
61
+ }
62
+
63
+ export function enqueueSkillExtraction(job: Omit<SkillExtractionJob, 'kind'>) {
64
+ return organizationLearningQueue.enqueue(
65
+ { kind: 'skill-extraction', ...job },
66
+ buildSkillExtractionJobOptions(job.orgId),
67
+ )
68
+ }
69
+
70
+ export async function clearRegularChatMemoryDigestDeduplicationKey(orgId: string): Promise<void> {
71
+ await organizationLearningQueue.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
72
+ }
73
+
74
+ export const startOrganizationLearningWorker = organizationLearningQueue.startWorker
75
+
76
+ if (import.meta.main) {
77
+ startOrganizationLearningWorker()
78
+ }