@lota-sdk/core 0.1.23 → 0.1.25

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 (78) 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 +5 -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/execution-plan.ts +2 -1
  20. package/src/runtime/memory-pipeline.ts +70 -1
  21. package/src/runtime/memory-prompts-fact.ts +16 -0
  22. package/src/runtime/plugin-resolution.ts +3 -2
  23. package/src/runtime/plugin-types.ts +1 -42
  24. package/src/runtime/post-turn-side-effects.ts +212 -0
  25. package/src/runtime/runtime-config.ts +0 -13
  26. package/src/runtime/runtime-extensions.ts +10 -16
  27. package/src/runtime/runtime-worker-registry.ts +8 -19
  28. package/src/runtime/social-chat-agent-runner.ts +119 -0
  29. package/src/runtime/social-chat-history.ts +110 -0
  30. package/src/runtime/social-chat-prompts.ts +58 -0
  31. package/src/runtime/social-chat.ts +104 -340
  32. package/src/runtime/specialist-runner.ts +18 -0
  33. package/src/runtime/workstream-chat-helpers.ts +19 -0
  34. package/src/runtime/workstream-plan-turn.ts +195 -0
  35. package/src/runtime/workstream-state.ts +11 -8
  36. package/src/runtime/workstream-turn-context.ts +183 -0
  37. package/src/services/agent-activity.service.ts +350 -0
  38. package/src/services/autonomous-job.service.ts +1 -8
  39. package/src/services/execution-plan.service.ts +205 -334
  40. package/src/services/index.ts +2 -4
  41. package/src/services/memory.service.ts +54 -44
  42. package/src/services/ownership-dispatcher.service.ts +2 -19
  43. package/src/services/plan-completion-side-effects.ts +80 -0
  44. package/src/services/plan-event-delivery.service.ts +1 -1
  45. package/src/services/plan-executor.service.ts +42 -190
  46. package/src/services/plan-node-spec.ts +60 -0
  47. package/src/services/plan-run-data.ts +88 -0
  48. package/src/services/plan-validator.service.ts +10 -8
  49. package/src/services/workstream-constants.ts +2 -0
  50. package/src/services/workstream-title.service.ts +1 -1
  51. package/src/services/workstream-turn-preparation.service.ts +208 -715
  52. package/src/services/workstream.service.ts +162 -192
  53. package/src/services/workstream.types.ts +12 -44
  54. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
  55. package/src/tools/execution-plan.tool.ts +11 -6
  56. package/src/tools/index.ts +1 -0
  57. package/src/tools/project-with-plan.tool.ts +87 -0
  58. package/src/tools/remember-memory.tool.ts +7 -10
  59. package/src/tools/research-topic.tool.ts +1 -1
  60. package/src/tools/team-think.tool.ts +1 -1
  61. package/src/tools/user-questions.tool.ts +1 -1
  62. package/src/utils/autonomous-job-ids.ts +7 -0
  63. package/src/workers/organization-learning.worker.ts +31 -0
  64. package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
  65. package/src/workers/skill-extraction.runner.ts +2 -2
  66. package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
  67. package/src/queues/regular-chat-memory-digest.config.ts +0 -12
  68. package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
  69. package/src/queues/skill-extraction.config.ts +0 -9
  70. package/src/queues/skill-extraction.queue.ts +0 -27
  71. package/src/queues/workstream-title-generation.queue.ts +0 -33
  72. package/src/services/context-enrichment.service.ts +0 -33
  73. package/src/services/coordination-registry.service.ts +0 -117
  74. package/src/services/domain-agent-executor.service.ts +0 -71
  75. package/src/services/memory-assessment.service.ts +0 -44
  76. package/src/services/playbook-registry.service.ts +0 -67
  77. package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
  78. 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.23",
3
+ "version": "0.1.25",
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.23",
35
+ "@lota-sdk/shared": "0.1.25",
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'
@@ -27,14 +27,14 @@ import type { LotaRuntimeWorkers } from './runtime/runtime-worker-registry'
27
27
  import { buildRuntimeWorkerRegistry } from './runtime/runtime-worker-registry'
28
28
  import type { LotaRuntimeSocialChat } from './runtime/social-chat'
29
29
  import { createSocialChatRuntime } from './runtime/social-chat'
30
+ import type { agentActivityService } from './services/agent-activity.service'
31
+ import { agentActivityService as agentActivityServiceSingleton } from './services/agent-activity.service'
30
32
  import type { attachmentService } from './services/attachment.service'
31
33
  import { attachmentService as attachmentServiceSingleton } from './services/attachment.service'
32
34
  import type { autonomousJobService } from './services/autonomous-job.service'
33
35
  import { autonomousJobService as autonomousJobServiceSingleton } from './services/autonomous-job.service'
34
- import { coordinationRegistryService as coordinationRegistryServiceSingleton } from './services/coordination-registry.service'
35
36
  import type { documentChunkService } from './services/document-chunk.service'
36
37
  import { documentChunkService as documentChunkServiceSingleton } from './services/document-chunk.service'
37
- import { domainAgentExecutorService } from './services/domain-agent-executor.service'
38
38
  import type { executionPlanService } from './services/execution-plan.service'
39
39
  import { executionPlanService as executionPlanServiceSingleton } from './services/execution-plan.service'
40
40
  import type { memoryService } from './services/memory.service'
@@ -48,7 +48,6 @@ import type { organizationService } from './services/organization.service'
48
48
  import { organizationService as organizationServiceSingleton } from './services/organization.service'
49
49
  import type { planAgentQueryService } from './services/plan-agent-query.service'
50
50
  import { planAgentQueryService as planAgentQueryServiceSingleton } from './services/plan-agent-query.service'
51
- import { playbookRegistryService } from './services/playbook-registry.service'
52
51
  import type { recentActivityTitleService } from './services/recent-activity-title.service'
53
52
  import { recentActivityTitleService as recentActivityTitleServiceSingleton } from './services/recent-activity-title.service'
54
53
  import type { recentActivityService } from './services/recent-activity.service'
@@ -117,6 +116,7 @@ export interface LotaRuntime {
117
116
  database: SurrealDBService
118
117
  redis: RedisConnectionManager
119
118
  closeRedisConnection: () => Promise<void>
119
+ agentActivityService: typeof agentActivityService
120
120
  attachmentService: typeof attachmentService
121
121
  autonomousJobService: typeof autonomousJobService
122
122
  documentChunkService: typeof documentChunkService
@@ -140,7 +140,6 @@ export interface LotaRuntime {
140
140
  isApprovalContinuationRequest: typeof isApprovalContinuationRequest
141
141
  runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
142
142
  triggerPlanNodeTurn: typeof triggerPlanNodeTurn
143
- syncPlaybookTemplates: typeof playbookRegistryService.syncPlaybookTemplates
144
143
  }
145
144
  lota: {
146
145
  organizations: {
@@ -234,7 +233,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
234
233
  const redisManager = createRedisConnectionManager({ url: runtimeConfig.redis.url })
235
234
  setRedisConnectionManager(redisManager)
236
235
  configureEmbeddingCache(redisManager.getConnection(), runtimeConfig.memory.embeddingCacheTtlSeconds)
237
- configureBackgroundProcessing(runtimeConfig.backgroundProcessing)
236
+ configureBackgroundProcessing()
238
237
  configureSocialChatHistory({ keyPrefix: runtimeConfig.socialChat?.historyRedisKeyPrefix })
239
238
 
240
239
  const socialChatAgentId = runtimeConfig.socialChat?.agentId?.trim() || 'socialChat'
@@ -270,21 +269,10 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
270
269
  })
271
270
 
272
271
  const pluginRuntime = runtimeConfig.pluginRuntime ?? {}
273
- domainAgentExecutorService.configure(pluginRuntime)
274
272
  if (runtimeConfig.graphDesigner) {
275
273
  configureGraphDesigner(runtimeConfig.graphDesigner)
276
274
  }
277
275
 
278
- for (const [pluginRef, plugin] of Object.entries(pluginRuntime)) {
279
- const signals = plugin.contributions.signals
280
- if (signals && signals.length > 0) {
281
- coordinationRegistryServiceSingleton.register(pluginRef, [...signals])
282
- }
283
- }
284
- coordinationRegistryServiceSingleton.validate()
285
- // Collect playbook contributions early to fail fast on misconfiguration
286
- playbookRegistryService.collectPlaybooks()
287
-
288
276
  const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
289
277
  const schemaFiles = [...getBuiltInSchemaFiles(), ...(runtimeConfig.extraSchemaFiles ?? [])]
290
278
  const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
@@ -390,6 +378,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
390
378
  database: db,
391
379
  redis: redisManager,
392
380
  closeRedisConnection: async () => await redisManager.closeConnection(),
381
+ agentActivityService: agentActivityServiceSingleton,
393
382
  attachmentService: attachmentServiceSingleton,
394
383
  autonomousJobService: autonomousJobServiceSingleton,
395
384
  documentChunkService: documentChunkServiceSingleton,
@@ -413,7 +402,6 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
413
402
  isApprovalContinuationRequest: isApprovalContinuationRequestSingleton,
414
403
  runWorkstreamTurnInBackground: runWorkstreamTurnInBackgroundSingleton,
415
404
  triggerPlanNodeTurn: triggerPlanNodeTurnSingleton,
416
- syncPlaybookTemplates: playbookRegistryService.syncPlaybookTemplates.bind(playbookRegistryService),
417
405
  },
418
406
  lota,
419
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'