@lota-sdk/core 0.4.9 → 0.4.10

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 (158) hide show
  1. package/package.json +2 -2
  2. package/src/ai/embedding-cache.ts +3 -1
  3. package/src/ai-gateway/ai-gateway.ts +38 -10
  4. package/src/config/agent-defaults.ts +22 -9
  5. package/src/config/agent-types.ts +1 -1
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/index.ts +0 -1
  8. package/src/config/logger.ts +20 -7
  9. package/src/config/thread-defaults.ts +12 -4
  10. package/src/create-runtime.ts +69 -656
  11. package/src/db/memory-query-builder.ts +2 -1
  12. package/src/db/memory-store.ts +29 -20
  13. package/src/db/memory.ts +188 -195
  14. package/src/db/service-normalization.ts +97 -64
  15. package/src/db/service.ts +706 -538
  16. package/src/db/startup.ts +30 -19
  17. package/src/effect/awaitable-effect.ts +46 -37
  18. package/src/effect/helpers.ts +30 -5
  19. package/src/effect/index.ts +7 -5
  20. package/src/effect/layers.ts +82 -72
  21. package/src/effect/runtime.ts +18 -3
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -66
  24. package/src/index.ts +13 -11
  25. package/src/queues/autonomous-job.queue.ts +59 -71
  26. package/src/queues/context-compaction.queue.ts +6 -18
  27. package/src/queues/delayed-node-promotion.queue.ts +9 -17
  28. package/src/queues/organization-learning.queue.ts +17 -4
  29. package/src/queues/plan-agent-heartbeat.queue.ts +23 -20
  30. package/src/queues/plan-scheduler.queue.ts +6 -18
  31. package/src/queues/post-chat-memory.queue.ts +6 -18
  32. package/src/queues/queue-factory.ts +128 -50
  33. package/src/queues/title-generation.queue.ts +6 -17
  34. package/src/redis/connection.ts +181 -164
  35. package/src/redis/runtime-connection.ts +13 -3
  36. package/src/redis/stream-context.ts +17 -9
  37. package/src/runtime/agent-runtime-policy.ts +1 -1
  38. package/src/runtime/agent-stream-helpers.ts +15 -11
  39. package/src/runtime/chat-run-orchestration.ts +1 -1
  40. package/src/runtime/context-compaction/context-compaction-runtime.ts +1 -1
  41. package/src/runtime/context-compaction/context-compaction.ts +126 -82
  42. package/src/runtime/domain-layer.ts +192 -0
  43. package/src/runtime/graph-designer.ts +15 -7
  44. package/src/runtime/helper-model.ts +8 -4
  45. package/src/runtime/index.ts +0 -1
  46. package/src/runtime/memory/memory-block.ts +19 -9
  47. package/src/runtime/memory/memory-pipeline.ts +53 -66
  48. package/src/runtime/memory/memory-scope.ts +33 -29
  49. package/src/runtime/plugin-resolution.ts +33 -54
  50. package/src/runtime/post-turn-side-effects.ts +6 -26
  51. package/src/runtime/retrieval-adapters.ts +4 -4
  52. package/src/runtime/runtime-accessors.ts +92 -0
  53. package/src/runtime/runtime-config.ts +3 -3
  54. package/src/runtime/runtime-extensions.ts +20 -9
  55. package/src/runtime/runtime-lifecycle.ts +124 -0
  56. package/src/runtime/runtime-services.ts +386 -0
  57. package/src/runtime/runtime-token.ts +47 -0
  58. package/src/runtime/social-chat/social-chat-agent-runner.ts +7 -5
  59. package/src/runtime/social-chat/social-chat-history.ts +21 -12
  60. package/src/runtime/social-chat/social-chat.ts +401 -365
  61. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +58 -52
  62. package/src/runtime/thread-turn-context.ts +21 -27
  63. package/src/services/agent-activity.service.ts +1 -1
  64. package/src/services/agent-executor.service.ts +179 -187
  65. package/src/services/artifact.service.ts +10 -5
  66. package/src/services/attachment.service.ts +35 -1
  67. package/src/services/autonomous-job.service.ts +58 -56
  68. package/src/services/background-work.service.ts +54 -0
  69. package/src/services/chat-run-registry.service.ts +3 -1
  70. package/src/services/context-compaction.service.ts +1 -1
  71. package/src/services/document-chunk.service.ts +8 -17
  72. package/src/services/execution-plan/execution-plan-graph.ts +74 -52
  73. package/src/services/execution-plan/execution-plan.service.ts +1 -1
  74. package/src/services/feedback-loop.service.ts +1 -1
  75. package/src/services/global-orchestrator.service.ts +33 -10
  76. package/src/services/graph-full-routing.ts +44 -33
  77. package/src/services/index.ts +1 -0
  78. package/src/services/institutional-memory.service.ts +8 -17
  79. package/src/services/learned-skill.service.ts +38 -35
  80. package/src/services/memory/memory-errors.ts +27 -0
  81. package/src/services/memory/memory-org-memory.ts +14 -3
  82. package/src/services/memory/memory-preseeded.ts +10 -4
  83. package/src/services/memory/memory-utils.ts +2 -1
  84. package/src/services/memory/memory.service.ts +26 -44
  85. package/src/services/memory/rerank.service.ts +3 -11
  86. package/src/services/monitoring-window.service.ts +1 -1
  87. package/src/services/mutating-approval.service.ts +1 -1
  88. package/src/services/node-workspace.service.ts +2 -2
  89. package/src/services/notification.service.ts +16 -4
  90. package/src/services/organization-member.service.ts +1 -1
  91. package/src/services/organization.service.ts +34 -51
  92. package/src/services/ownership-dispatcher.service.ts +132 -90
  93. package/src/services/plan/plan-agent-heartbeat.service.ts +1 -1
  94. package/src/services/plan/plan-agent-query.service.ts +1 -1
  95. package/src/services/plan/plan-approval.service.ts +52 -48
  96. package/src/services/plan/plan-artifact.service.ts +2 -2
  97. package/src/services/plan/plan-builder.service.ts +2 -2
  98. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  99. package/src/services/plan/plan-compiler.service.ts +1 -1
  100. package/src/services/plan/plan-completion-side-effects.ts +18 -24
  101. package/src/services/plan/plan-coordination.service.ts +1 -1
  102. package/src/services/plan/plan-cycle.service.ts +171 -164
  103. package/src/services/plan/plan-deadline.service.ts +290 -304
  104. package/src/services/plan/plan-event-delivery.service.ts +44 -39
  105. package/src/services/plan/plan-executor-graph.ts +114 -67
  106. package/src/services/plan/plan-executor-helpers.ts +60 -75
  107. package/src/services/plan/plan-executor.service.ts +550 -467
  108. package/src/services/plan/plan-run.service.ts +12 -19
  109. package/src/services/plan/plan-scheduler.service.ts +27 -33
  110. package/src/services/plan/plan-template.service.ts +1 -1
  111. package/src/services/plan/plan-transaction-events.ts +8 -5
  112. package/src/services/plan/plan-validator.service.ts +1 -1
  113. package/src/services/plan/plan-workspace.service.ts +17 -11
  114. package/src/services/plugin-executor.service.ts +26 -21
  115. package/src/services/quality-metrics.service.ts +1 -1
  116. package/src/services/queue-job.service.ts +8 -17
  117. package/src/services/recent-activity-title.service.ts +17 -9
  118. package/src/services/recent-activity.service.ts +1 -1
  119. package/src/services/skill-resolver.service.ts +1 -1
  120. package/src/services/social-chat-history.service.ts +37 -20
  121. package/src/services/system-executor.service.ts +25 -20
  122. package/src/services/thread/thread-bootstrap.ts +26 -10
  123. package/src/services/thread/thread-listing.ts +2 -1
  124. package/src/services/thread/thread-memory-block.ts +18 -5
  125. package/src/services/thread/thread-message.service.ts +24 -8
  126. package/src/services/thread/thread-title.service.ts +1 -1
  127. package/src/services/thread/thread-turn-execution.ts +1 -1
  128. package/src/services/thread/thread-turn-preparation.service.ts +18 -16
  129. package/src/services/thread/thread-turn-streaming.ts +12 -11
  130. package/src/services/thread/thread-turn.ts +43 -10
  131. package/src/services/thread/thread.service.ts +11 -2
  132. package/src/services/user.service.ts +1 -1
  133. package/src/services/write-intent-validator.service.ts +1 -1
  134. package/src/storage/attachment-storage.service.ts +7 -4
  135. package/src/storage/generated-document-storage.service.ts +1 -1
  136. package/src/system-agents/context-compaction.agent.ts +1 -1
  137. package/src/system-agents/helper-agent-options.ts +1 -1
  138. package/src/system-agents/memory-reranker.agent.ts +1 -1
  139. package/src/system-agents/memory.agent.ts +1 -1
  140. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  141. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  142. package/src/system-agents/skill-extractor.agent.ts +1 -1
  143. package/src/system-agents/skill-manager.agent.ts +1 -1
  144. package/src/system-agents/title-generator.agent.ts +1 -1
  145. package/src/tools/execution-plan.tool.ts +28 -17
  146. package/src/tools/fetch-webpage.tool.ts +20 -13
  147. package/src/tools/firecrawl-client.ts +13 -3
  148. package/src/tools/plan-approval.tool.ts +9 -1
  149. package/src/tools/search-web.tool.ts +16 -9
  150. package/src/tools/team-think.tool.ts +2 -2
  151. package/src/utils/async.ts +15 -6
  152. package/src/utils/errors.ts +27 -15
  153. package/src/workers/bootstrap.ts +25 -48
  154. package/src/workers/organization-learning.worker.ts +1 -1
  155. package/src/workers/regular-chat-memory-digest.runner.ts +25 -15
  156. package/src/workers/worker-utils.ts +20 -2
  157. package/src/config/search.ts +0 -3
  158. package/src/runtime/agent-types.ts +0 -1
@@ -12,11 +12,14 @@ import { Effect } from 'effect'
12
12
  import { serverLogger } from '../config/logger'
13
13
  import { recordIdToString } from '../db/record-id'
14
14
  import { TABLES } from '../db/tables'
15
- import { BadRequestError } from '../effect/errors'
15
+ import { BadRequestError, ServiceError } from '../effect/errors'
16
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
16
17
  import { shouldPlanNodeUseVisibleTurn } from '../runtime/execution-plan-visibility'
17
18
  import type { makePlanExecutorService } from './plan/plan-executor.service'
18
19
  import type { makePlanRunService } from './plan/plan-run.service'
19
20
 
21
+ const tryGraphFullPromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
22
+
20
23
  function classifyDispatchFailure(ownerType: string, error: unknown): PlanFailureClass {
21
24
  const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()
22
25
  if (errorMessage.includes('timeout')) return 'timeout_exceeded'
@@ -30,19 +33,19 @@ function formatDispatchError(error: unknown): string {
30
33
 
31
34
  const STABLE_RUN_STATUSES = new Set(['pending-approval', 'awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
32
35
 
33
- interface GraphFullRoutingDeps {
36
+ interface GraphFullRoutingDeps<E = never> {
34
37
  dispatchReadyNode(params: {
35
38
  run: PlanRunRecord
36
39
  nodeSpecRecord: PlanNodeSpecRecord
37
40
  nodeRun: PlanNodeRunRecord
38
41
  spec: PlanSpecRecord
39
42
  executionModeOverride?: ExecutionMode
40
- }): Effect.Effect<PlanNodeResultSubmission, unknown>
43
+ }): Effect.Effect<PlanNodeResultSubmission, E, never>
41
44
  planExecutorService: ReturnType<typeof makePlanExecutorService>
42
45
  planRunService: ReturnType<typeof makePlanRunService>
43
46
  }
44
47
 
45
- export function routeGraphFullEffect(params: { threadId: string; runId: string }, deps: GraphFullRoutingDeps) {
48
+ export function routeGraphFullEffect<E>(params: { threadId: string; runId: string }, deps: GraphFullRoutingDeps<E>) {
46
49
  return Effect.gen(function* () {
47
50
  const MAX_ROUNDS = 32
48
51
  const STRUCTURAL_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
@@ -75,15 +78,17 @@ export function routeGraphFullEffect(params: { threadId: string; runId: string }
75
78
  yield* Effect.forEach(
76
79
  readyNodes,
77
80
  (nodeRun) =>
78
- Effect.tryPromise(() =>
79
- deps.planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId }),
81
+ tryGraphFullPromise(
82
+ () => deps.planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId }),
83
+ 'Failed to transition plan node to running.',
80
84
  ),
81
85
  { concurrency: 'unbounded' },
82
86
  )
83
87
 
84
88
  if (visibleNodes.length > 0) {
85
- const { enqueuePlanAgentHeartbeatWake } = yield* Effect.tryPromise(
89
+ const { enqueuePlanAgentHeartbeatWake } = yield* tryGraphFullPromise(
86
90
  () => import('../queues/plan-agent-heartbeat.queue'),
91
+ 'Failed to load plan agent heartbeat queue module.',
87
92
  )
88
93
  const updatedRunForWake = yield* deps.planRunService.getRunById(params.runId)
89
94
  yield* Effect.forEach(
@@ -95,15 +100,17 @@ export function routeGraphFullEffect(params: { threadId: string; runId: string }
95
100
  return
96
101
  }
97
102
 
98
- yield* Effect.tryPromise(() =>
99
- enqueuePlanAgentHeartbeatWake({
100
- organizationId: recordIdToString(updatedRunForWake.organizationId, TABLES.ORGANIZATION),
101
- threadId: recordIdToString(updatedRunForWake.threadId, TABLES.THREAD),
102
- runId: recordIdToString(updatedRunForWake.id, TABLES.PLAN_RUN),
103
- nodeId: nodeRun.nodeId,
104
- agentId: ns.owner.ref,
105
- reason: 'graph-full-visible',
106
- }),
103
+ yield* tryGraphFullPromise(
104
+ () =>
105
+ enqueuePlanAgentHeartbeatWake({
106
+ organizationId: recordIdToString(updatedRunForWake.organizationId, TABLES.ORGANIZATION),
107
+ threadId: recordIdToString(updatedRunForWake.threadId, TABLES.THREAD),
108
+ runId: recordIdToString(updatedRunForWake.id, TABLES.PLAN_RUN),
109
+ nodeId: nodeRun.nodeId,
110
+ agentId: ns.owner.ref,
111
+ reason: 'graph-full-visible',
112
+ }),
113
+ 'Failed to enqueue plan agent heartbeat wake.',
107
114
  )
108
115
  }),
109
116
  { concurrency: 'unbounded' },
@@ -150,26 +157,30 @@ export function routeGraphFullEffect(params: { threadId: string; runId: string }
150
157
  const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
151
158
 
152
159
  if (settled.status === 'fulfilled') {
153
- yield* Effect.tryPromise(() =>
154
- deps.planExecutorService.submitNodeResult({
155
- threadId,
156
- runId,
157
- nodeId: settled.value.nodeId,
158
- emittedBy: settled.value.ownerRef,
159
- result: settled.value.result,
160
- }),
160
+ yield* tryGraphFullPromise(
161
+ () =>
162
+ deps.planExecutorService.submitNodeResult({
163
+ threadId,
164
+ runId,
165
+ nodeId: settled.value.nodeId,
166
+ emittedBy: settled.value.ownerRef,
167
+ result: settled.value.result,
168
+ }),
169
+ 'Failed to submit plan node result.',
161
170
  )
162
171
  } else {
163
172
  serverLogger.warn`routeGraphFull: dispatch failed for node "${nodeRun.nodeId}": ${settled.reason}`
164
- yield* Effect.tryPromise(() =>
165
- deps.planExecutorService.blockNodeOnDispatchFailure({
166
- threadId,
167
- runId,
168
- nodeId: nodeRun.nodeId,
169
- emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
170
- message: formatDispatchError(settled.reason),
171
- failureClass: classifyDispatchFailure(nodeSpecRecord?.owner.executorType ?? 'agent', settled.reason),
172
- }),
173
+ yield* tryGraphFullPromise(
174
+ () =>
175
+ deps.planExecutorService.blockNodeOnDispatchFailure({
176
+ threadId,
177
+ runId,
178
+ nodeId: nodeRun.nodeId,
179
+ emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
180
+ message: formatDispatchError(settled.reason),
181
+ failureClass: classifyDispatchFailure(nodeSpecRecord?.owner.executorType ?? 'agent', settled.reason),
182
+ }),
183
+ 'Failed to block plan node on dispatch failure.',
173
184
  )
174
185
  }
175
186
  }
@@ -3,6 +3,7 @@ export * from './agent-activity.service'
3
3
  export * from './artifact.service'
4
4
  export * from './attachment.service'
5
5
  export * from './autonomous-job.service'
6
+ export * from './background-work.service'
6
7
  export * from './document-chunk.service'
7
8
  export * from './execution-plan/execution-plan.service'
8
9
  export * from './institutional-memory.service'
@@ -11,6 +11,7 @@ import { BoundQuery } from 'surrealdb'
11
11
  import { ensureRecordId } from '../db/record-id'
12
12
  import type { SurrealDBService } from '../db/service'
13
13
  import { TABLES } from '../db/tables'
14
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
14
15
  import { DatabaseServiceTag } from '../effect/services'
15
16
  import { unsafeDateFrom } from '../utils/date-time'
16
17
  import type { makePlanRunService } from './plan/plan-run.service'
@@ -26,25 +27,15 @@ class InstitutionalMemoryServiceError extends Schema.TaggedErrorClass<Institutio
26
27
  { message: Schema.String, cause: Schema.Defect },
27
28
  ) {}
28
29
 
30
+ const effectTryInstitutionalMemoryPromise = makeEffectTryPromiseWithMessage(
31
+ (message, cause) => new InstitutionalMemoryServiceError({ message, cause }),
32
+ )
33
+
29
34
  function tryInstitutionalMemoryPromise<A>(
30
35
  message: string,
31
- thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
36
+ evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
32
37
  ): Effect.Effect<A, InstitutionalMemoryServiceError> {
33
- return Effect.suspend(() => {
34
- try {
35
- const value = thunk()
36
- if (Effect.isEffect(value)) {
37
- return value.pipe(Effect.mapError((cause) => new InstitutionalMemoryServiceError({ message, cause })))
38
- }
39
-
40
- return Effect.tryPromise({
41
- try: () => Promise.resolve(value),
42
- catch: (cause) => new InstitutionalMemoryServiceError({ message, cause }),
43
- })
44
- } catch (cause) {
45
- return Effect.fail(new InstitutionalMemoryServiceError({ message, cause }))
46
- }
47
- })
38
+ return effectTryInstitutionalMemoryPromise(evaluate, message)
48
39
  }
49
40
 
50
41
  export function makeInstitutionalMemoryService(deps: InstitutionalMemoryDeps) {
@@ -230,7 +221,7 @@ export function makeInstitutionalMemoryService(deps: InstitutionalMemoryDeps) {
230
221
  export class InstitutionalMemoryServiceTag extends Context.Service<
231
222
  InstitutionalMemoryServiceTag,
232
223
  ReturnType<typeof makeInstitutionalMemoryService>
233
- >()('InstitutionalMemoryService') {}
224
+ >()('@lota-sdk/core/InstitutionalMemoryService') {}
234
225
 
235
226
  export const InstitutionalMemoryServiceLive = Layer.effect(
236
227
  InstitutionalMemoryServiceTag,
@@ -8,10 +8,12 @@ import { serverLogger } from '../config/logger'
8
8
  import { ensureRecordId, recordIdToString } from '../db/record-id'
9
9
  import type { SurrealDBService } from '../db/service'
10
10
  import { TABLES } from '../db/tables'
11
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
11
12
  import { DatabaseServiceTag, RuntimeConfigServiceTag } from '../effect/services'
12
13
  import { ProviderEmbeddings } from '../embeddings/provider'
13
14
  import { sha256HexFromParts } from '../utils/crypto'
14
15
  import { nowDate } from '../utils/date-time'
16
+ import { BackgroundWorkService } from './background-work.service'
15
17
 
16
18
  const PROMOTION_MIN_USES = 5
17
19
  const PROMOTION_MIN_SUCCESS_RATE = 0.6
@@ -64,25 +66,15 @@ class LearnedSkillNotFoundError extends Schema.TaggedErrorClass<LearnedSkillNotF
64
66
  { message: Schema.String },
65
67
  ) {}
66
68
 
69
+ const effectTryLearnedSkillPromise = makeEffectTryPromiseWithMessage(
70
+ (message, cause) => new LearnedSkillServiceError({ message, cause }),
71
+ )
72
+
67
73
  function tryLearnedSkillPromise<A>(
68
74
  message: string,
69
- thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
75
+ evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
70
76
  ): Effect.Effect<A, LearnedSkillServiceError> {
71
- return Effect.suspend(() => {
72
- try {
73
- const value = thunk()
74
- if (Effect.isEffect(value)) {
75
- return value.pipe(Effect.mapError((cause) => new LearnedSkillServiceError({ message, cause })))
76
- }
77
-
78
- return Effect.tryPromise({
79
- try: () => Promise.resolve(value),
80
- catch: (cause) => new LearnedSkillServiceError({ message, cause }),
81
- })
82
- } catch (cause) {
83
- return Effect.fail(new LearnedSkillServiceError({ message, cause }))
84
- }
85
- })
77
+ return effectTryLearnedSkillPromise(evaluate, message)
86
78
  }
87
79
 
88
80
  interface CreateLearnedSkillInput {
@@ -126,6 +118,7 @@ export function makeLearnedSkillService(
126
118
  db: SurrealDBService,
127
119
  options: { embeddingModel: string; openRouterApiKey?: string },
128
120
  skillExistsCache: Cache.Cache<string, boolean, LearnedSkillServiceError>,
121
+ background: Context.Service.Shape<typeof BackgroundWorkService>,
129
122
  ) {
130
123
  const embeddings = new ProviderEmbeddings({
131
124
  modelId: options.embeddingModel,
@@ -265,30 +258,33 @@ export function makeLearnedSkillService(
265
258
  }),
266
259
  ),
267
260
  ).pipe(Effect.withSpan('LearnedSkillService.queryNearestSkills'))
268
- return rows.map((row) => SearchResultRowSchema.parse(row)).filter((row) => row.similarity >= 0.3)
261
+ const parsedRows = yield* Effect.try({
262
+ try: () => rows.map((row) => SearchResultRowSchema.parse(row)),
263
+ catch: (cause) =>
264
+ new LearnedSkillServiceError({ message: 'Failed to parse learned skill search results.', cause }),
265
+ })
266
+ return parsedRows.filter((row) => row.similarity >= 0.3)
269
267
  })
270
268
 
271
269
  const retrieveForTurn = Effect.fn('LearnedSkillService.retrieveForTurn')(function* (params: RetrieveForTurnParams) {
272
270
  const results = yield* searchForTurn(params)
273
271
  if (results.length === 0) return undefined
274
272
 
275
- const currentContext = yield* Effect.context<never>()
276
- yield* Effect.sync(() => {
277
- void Effect.runForkWith(currentContext)(
278
- Effect.forEach(
279
- results,
280
- (result) =>
281
- recordUsage(result.id).pipe(
282
- Effect.tapError((error) =>
283
- Effect.sync(() => {
284
- serverLogger.warn`Failed to record learned skill usage for ${result.id}: ${error}`
285
- }),
286
- ),
273
+ yield* background.run(
274
+ Effect.forEach(
275
+ results,
276
+ (result) =>
277
+ recordUsage(result.id).pipe(
278
+ Effect.tapError((error) =>
279
+ Effect.sync(() => {
280
+ serverLogger.warn`Failed to record learned skill usage for ${result.id}: ${error}`
281
+ }),
287
282
  ),
288
- { discard: true },
289
- ),
290
- )
291
- })
283
+ ),
284
+ { discard: true },
285
+ ),
286
+ 'learned-skill.recordUsage',
287
+ )
292
288
 
293
289
  const section = renderLearnedSkillInstructions(
294
290
  results.map((row) => ({ name: row.name, instructions: row.instructions })),
@@ -365,7 +361,12 @@ export function makeLearnedSkillService(
365
361
  const rows = yield* tryLearnedSkillPromise('Failed to query most similar learned skill.', () =>
366
362
  db.query<unknown>(new BoundQuery(sql, { organizationId: orgRef, embedding: descEmbedding })),
367
363
  )
368
- return rows.length === 0 ? null : LearnedSkillRowSchema.parse(rows[0])
364
+ if (rows.length === 0) return null
365
+ return yield* Effect.try({
366
+ try: () => LearnedSkillRowSchema.parse(rows[0]),
367
+ catch: (cause) =>
368
+ new LearnedSkillServiceError({ message: 'Failed to parse most similar learned skill row.', cause }),
369
+ })
369
370
  })
370
371
 
371
372
  const listForOrg = (orgId: string) =>
@@ -450,13 +451,14 @@ export function makeLearnedSkillService(
450
451
  export class LearnedSkillServiceTag extends Context.Service<
451
452
  LearnedSkillServiceTag,
452
453
  ReturnType<typeof makeLearnedSkillService>
453
- >()('LearnedSkillService') {}
454
+ >()('@lota-sdk/core/LearnedSkillService') {}
454
455
 
455
456
  export const LearnedSkillServiceLive = Layer.effect(
456
457
  LearnedSkillServiceTag,
457
458
  Effect.gen(function* () {
458
459
  const db = yield* DatabaseServiceTag
459
460
  const runtimeConfig = yield* RuntimeConfigServiceTag
461
+ const background = yield* BackgroundWorkService
460
462
  const skillExistsCache = yield* Cache.make({
461
463
  lookup: (key: string) =>
462
464
  Effect.gen(function* () {
@@ -486,6 +488,7 @@ export const LearnedSkillServiceLive = Layer.effect(
486
488
  openRouterApiKey: runtimeConfig.aiGateway.openRouterApiKey,
487
489
  },
488
490
  skillExistsCache,
491
+ background,
489
492
  )
490
493
  }),
491
494
  )
@@ -0,0 +1,27 @@
1
+ import { Effect, Schema } from 'effect'
2
+
3
+ export class MemoryServiceError extends Schema.TaggedErrorClass<MemoryServiceError>()('MemoryServiceError', {
4
+ message: Schema.String,
5
+ cause: Schema.Defect,
6
+ }) {}
7
+
8
+ export function tryMemoryPromise<A, E, R = never>(
9
+ message: string,
10
+ thunk: () => PromiseLike<A> | Effect.Effect<A, E, R>,
11
+ ): Effect.Effect<A, MemoryServiceError, R> {
12
+ return Effect.suspend(() => {
13
+ try {
14
+ const value = thunk()
15
+ if (Effect.isEffect(value)) {
16
+ return value.pipe(Effect.mapError((cause) => new MemoryServiceError({ message, cause })))
17
+ }
18
+
19
+ return Effect.tryPromise({
20
+ try: () => Promise.resolve(value),
21
+ catch: (cause) => new MemoryServiceError({ message, cause }),
22
+ })
23
+ } catch (cause) {
24
+ return Effect.fail(new MemoryServiceError({ message, cause }))
25
+ }
26
+ })
27
+ }
@@ -1,3 +1,4 @@
1
+ import type { Context } from 'effect'
1
2
  import { Cache, Duration, Effect } from 'effect'
2
3
 
3
4
  import { aiLogger } from '../../config/logger'
@@ -7,6 +8,7 @@ import type { HelperModelRuntime } from '../../runtime/helper-model'
7
8
  import { ORG_SCOPE_PREFIX, scopeId } from '../../runtime/memory/memory-scope'
8
9
  import type { ResolvedLotaRuntimeConfig } from '../../runtime/runtime-config'
9
10
  import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../../system-agents/memory.agent'
11
+ import type { BackgroundWorkService } from '../background-work.service'
10
12
 
11
13
  const MAX_ORG_MEMORY_CLIENTS = 128
12
14
 
@@ -14,6 +16,7 @@ interface OrgMemoryDeps {
14
16
  db: SurrealDBService
15
17
  runtimeConfig: ResolvedLotaRuntimeConfig
16
18
  helperModelRuntime: HelperModelRuntime
19
+ background: Context.Service.Shape<typeof BackgroundWorkService>
17
20
  }
18
21
 
19
22
  export type OrgMemoryCache = Cache.Cache<string, Memory>
@@ -24,7 +27,12 @@ export function makeOrgMemoryCache(deps: OrgMemoryDeps) {
24
27
  Effect.sync(() => {
25
28
  aiLogger.debug`Memory client created and cached for ${cacheKey}`
26
29
  return new Memory(
27
- { db: deps.db, runtimeConfig: deps.runtimeConfig, helperModelRuntime: deps.helperModelRuntime },
30
+ {
31
+ db: deps.db,
32
+ runtimeConfig: deps.runtimeConfig,
33
+ helperModelRuntime: deps.helperModelRuntime,
34
+ background: deps.background,
35
+ },
28
36
  { createAgent: createOrgMemoryAgent },
29
37
  { customPrompt: ORG_MEMORY_PROMPT },
30
38
  )
@@ -34,6 +42,9 @@ export function makeOrgMemoryCache(deps: OrgMemoryDeps) {
34
42
  })
35
43
  }
36
44
 
37
- export function getOrgMemory(cache: OrgMemoryCache, orgId: string): Effect.Effect<Memory> {
38
- return Cache.get(cache, scopeId(ORG_SCOPE_PREFIX, orgId))
45
+ export function getOrgMemory(cache: OrgMemoryCache, orgId: string) {
46
+ return Effect.gen(function* () {
47
+ const key = yield* scopeId(ORG_SCOPE_PREFIX, orgId)
48
+ return yield* Cache.get(cache, key)
49
+ })
39
50
  }
@@ -1,6 +1,8 @@
1
1
  import { Effect } from 'effect'
2
2
 
3
+ import type { Memory } from '../../db/memory'
3
4
  import type { MemoryRecord } from '../../db/memory-types'
5
+ import type { MemoryScopeError } from '../../runtime/memory/memory-scope'
4
6
  import { ORG_SCOPE_PREFIX, agentScopeId, scopeId } from '../../runtime/memory/memory-scope'
5
7
  import { compactWhitespace, truncateText } from '../../utils/string'
6
8
  import type { OrgMemoryCache } from './memory-org-memory'
@@ -10,6 +12,7 @@ const PRESEEDED_MEMORY_LIMIT = 5
10
12
  const PRESEEDED_MEMORY_MAX_CHARS = 300
11
13
  const PRESEEDED_MIN_IMPORTANCE = 0.7
12
14
  const PRESEEDED_MEMORY_DURABILITY: MemoryRecord['durability'] = 'core'
15
+ type PreSeededMemoryError = Effect.Error<ReturnType<Memory['listTopMemories']>> | MemoryScopeError
13
16
 
14
17
  function normalizePreSeededMemoryText(value: string): string {
15
18
  return truncateText(compactWhitespace(value), PRESEEDED_MEMORY_MAX_CHARS)
@@ -30,13 +33,16 @@ export function getTopMemoriesSection(params: {
30
33
  orgId: string
31
34
  agentName?: string
32
35
  limit?: number
33
- }): Effect.Effect<string | undefined, unknown> {
36
+ }): Effect.Effect<string | undefined, PreSeededMemoryError, never> {
34
37
  return Effect.gen(function* () {
35
38
  const orgMemory = yield* getOrgMemory(params.orgMemoryCache, params.orgId)
36
- const orgScopeId = scopeId(ORG_SCOPE_PREFIX, params.orgId)
39
+ const orgScopeId = yield* scopeId(ORG_SCOPE_PREFIX, params.orgId)
37
40
  const requestedLimit = params.limit ?? PRESEEDED_MEMORY_LIMIT
38
41
  const limit = Math.max(1, Math.min(requestedLimit, PRESEEDED_MEMORY_LIMIT))
39
42
 
43
+ const agentScopedId =
44
+ params.agentName && params.agentName.trim() ? yield* agentScopeId(params.orgId, params.agentName) : undefined
45
+
40
46
  const [orgTopMemories, agentTopMemories] = yield* Effect.all([
41
47
  orgMemory.listTopMemories({
42
48
  scopeId: orgScopeId,
@@ -45,9 +51,9 @@ export function getTopMemoriesSection(params: {
45
51
  durability: PRESEEDED_MEMORY_DURABILITY,
46
52
  minImportance: PRESEEDED_MIN_IMPORTANCE,
47
53
  }),
48
- params.agentName && params.agentName.trim()
54
+ agentScopedId
49
55
  ? orgMemory.listTopMemories({
50
- scopeId: agentScopeId(params.orgId, params.agentName),
56
+ scopeId: agentScopedId,
51
57
  limit,
52
58
  memoryType: 'fact',
53
59
  durability: PRESEEDED_MEMORY_DURABILITY,
@@ -1,5 +1,6 @@
1
+ import { VECTOR_SEARCH_OVERFETCH_MULTIPLIER } from '@lota-sdk/shared'
2
+
1
3
  import { MEMORY } from '../../config/constants'
2
- import { VECTOR_SEARCH_OVERFETCH_MULTIPLIER } from '../../config/search'
3
4
  import type { MemorySearchResult } from '../../db/memory-types'
4
5
  import { compactWhitespace, truncateText } from '../../utils/string'
5
6
  import type { MemoryRerankOutput } from './memory-rerank'