@lota-sdk/core 0.4.9 → 0.4.11

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 (182) 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 +164 -82
  4. package/src/ai-gateway/index.ts +16 -1
  5. package/src/config/agent-defaults.ts +4 -107
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/config/background-processing.ts +1 -1
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +22 -25
  10. package/src/config/thread-defaults.ts +1 -10
  11. package/src/create-runtime.ts +145 -670
  12. package/src/db/base.service.ts +30 -38
  13. package/src/db/memory-query-builder.ts +2 -1
  14. package/src/db/memory-store.ts +29 -20
  15. package/src/db/memory.ts +188 -195
  16. package/src/db/service-normalization.ts +97 -64
  17. package/src/db/service.ts +496 -384
  18. package/src/db/startup.ts +30 -19
  19. package/src/effect/helpers.ts +30 -5
  20. package/src/effect/index.ts +7 -7
  21. package/src/effect/layers.ts +75 -72
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -71
  24. package/src/index.ts +13 -12
  25. package/src/queues/autonomous-job.queue.ts +177 -143
  26. package/src/queues/context-compaction.queue.ts +41 -39
  27. package/src/queues/delayed-node-promotion.queue.ts +61 -42
  28. package/src/queues/document-processor.queue.ts +5 -3
  29. package/src/queues/index.ts +1 -0
  30. package/src/queues/memory-consolidation.queue.ts +79 -53
  31. package/src/queues/organization-learning.queue.ts +70 -33
  32. package/src/queues/plan-agent-heartbeat.queue.ts +111 -83
  33. package/src/queues/plan-scheduler.queue.ts +101 -97
  34. package/src/queues/post-chat-memory.queue.ts +56 -46
  35. package/src/queues/queue-factory.ts +146 -69
  36. package/src/queues/queues.service.ts +61 -0
  37. package/src/queues/title-generation.queue.ts +44 -44
  38. package/src/redis/connection.ts +181 -164
  39. package/src/redis/org-memory-lock.ts +24 -9
  40. package/src/redis/redis-lease-lock.ts +8 -1
  41. package/src/redis/stream-context.ts +17 -9
  42. package/src/runtime/agent-identity-overrides.ts +7 -3
  43. package/src/runtime/agent-runtime-policy.ts +10 -5
  44. package/src/runtime/agent-stream-helpers.ts +24 -15
  45. package/src/runtime/chat-run-orchestration.ts +1 -1
  46. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  47. package/src/runtime/context-compaction/context-compaction.ts +131 -85
  48. package/src/runtime/domain-layer.ts +203 -0
  49. package/src/runtime/execution-plan-visibility.ts +5 -2
  50. package/src/runtime/graph-designer.ts +0 -14
  51. package/src/runtime/helper-model.ts +8 -4
  52. package/src/runtime/index.ts +1 -1
  53. package/src/runtime/indexed-repositories-policy.ts +2 -6
  54. package/src/runtime/memory/memory-block.ts +19 -9
  55. package/src/runtime/memory/memory-pipeline.ts +53 -66
  56. package/src/runtime/memory/memory-scope.ts +33 -29
  57. package/src/runtime/plugin-resolution.ts +58 -62
  58. package/src/runtime/post-turn-side-effects.ts +139 -161
  59. package/src/runtime/retrieval-adapters.ts +4 -4
  60. package/src/runtime/runtime-config.ts +3 -9
  61. package/src/runtime/runtime-extensions.ts +0 -43
  62. package/src/runtime/runtime-lifecycle.ts +124 -0
  63. package/src/runtime/runtime-services.ts +455 -0
  64. package/src/runtime/runtime-worker-registry.ts +113 -30
  65. package/src/runtime/social-chat/social-chat-agent-runner.ts +13 -8
  66. package/src/runtime/social-chat/social-chat-history.ts +24 -13
  67. package/src/runtime/social-chat/social-chat.ts +420 -369
  68. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +64 -57
  69. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  70. package/src/runtime/thread-chat-helpers.ts +18 -9
  71. package/src/runtime/thread-turn-context.ts +28 -74
  72. package/src/runtime/turn-lifecycle.ts +6 -14
  73. package/src/services/agent-activity.service.ts +169 -176
  74. package/src/services/agent-executor.service.ts +207 -196
  75. package/src/services/artifact.service.ts +10 -5
  76. package/src/services/attachment.service.ts +16 -48
  77. package/src/services/autonomous-job.service.ts +81 -87
  78. package/src/services/background-work.service.ts +54 -0
  79. package/src/services/chat-run-registry.service.ts +3 -1
  80. package/src/services/context-compaction.service.ts +8 -10
  81. package/src/services/document-chunk.service.ts +8 -17
  82. package/src/services/execution-plan/execution-plan-graph.ts +122 -109
  83. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  84. package/src/services/execution-plan/execution-plan.service.ts +68 -51
  85. package/src/services/feedback-loop.service.ts +1 -1
  86. package/src/services/global-orchestrator.service.ts +49 -15
  87. package/src/services/graph-full-routing.ts +49 -37
  88. package/src/services/index.ts +1 -0
  89. package/src/services/institutional-memory.service.ts +8 -17
  90. package/src/services/learned-skill.service.ts +38 -35
  91. package/src/services/memory/memory-conversation.ts +10 -5
  92. package/src/services/memory/memory-errors.ts +27 -0
  93. package/src/services/memory/memory-org-memory.ts +14 -3
  94. package/src/services/memory/memory-preseeded.ts +10 -4
  95. package/src/services/memory/memory-utils.ts +2 -1
  96. package/src/services/memory/memory.service.ts +37 -52
  97. package/src/services/memory/rerank.service.ts +3 -11
  98. package/src/services/monitoring-window.service.ts +1 -1
  99. package/src/services/mutating-approval.service.ts +1 -1
  100. package/src/services/node-workspace.service.ts +2 -2
  101. package/src/services/notification.service.ts +16 -4
  102. package/src/services/organization-member.service.ts +1 -1
  103. package/src/services/organization.service.ts +34 -51
  104. package/src/services/ownership-dispatcher.service.ts +148 -95
  105. package/src/services/plan/plan-agent-heartbeat.service.ts +30 -16
  106. package/src/services/plan/plan-agent-query.service.ts +13 -9
  107. package/src/services/plan/plan-approval.service.ts +52 -48
  108. package/src/services/plan/plan-artifact.service.ts +2 -2
  109. package/src/services/plan/plan-builder.service.ts +2 -2
  110. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  111. package/src/services/plan/plan-compiler.service.ts +1 -1
  112. package/src/services/plan/plan-completion-side-effects.ts +99 -113
  113. package/src/services/plan/plan-coordination.service.ts +1 -1
  114. package/src/services/plan/plan-cycle.service.ts +171 -202
  115. package/src/services/plan/plan-deadline.service.ts +304 -307
  116. package/src/services/plan/plan-event-delivery.service.ts +84 -72
  117. package/src/services/plan/plan-executor-context.ts +2 -0
  118. package/src/services/plan/plan-executor-graph.ts +375 -353
  119. package/src/services/plan/plan-executor-helpers.ts +60 -75
  120. package/src/services/plan/plan-executor.service.ts +494 -489
  121. package/src/services/plan/plan-run.service.ts +12 -19
  122. package/src/services/plan/plan-scheduler.service.ts +89 -82
  123. package/src/services/plan/plan-template.service.ts +1 -1
  124. package/src/services/plan/plan-transaction-events.ts +8 -5
  125. package/src/services/plan/plan-validator.service.ts +1 -1
  126. package/src/services/plan/plan-workspace.service.ts +17 -11
  127. package/src/services/plugin-executor.service.ts +26 -21
  128. package/src/services/quality-metrics.service.ts +1 -1
  129. package/src/services/queue-job.service.ts +8 -17
  130. package/src/services/recent-activity-title.service.ts +22 -10
  131. package/src/services/recent-activity.service.ts +1 -1
  132. package/src/services/skill-resolver.service.ts +1 -1
  133. package/src/services/social-chat-history.service.ts +37 -20
  134. package/src/services/system-executor.service.ts +25 -20
  135. package/src/services/thread/thread-bootstrap.ts +37 -19
  136. package/src/services/thread/thread-listing.ts +2 -1
  137. package/src/services/thread/thread-memory-block.ts +18 -5
  138. package/src/services/thread/thread-message.service.ts +30 -13
  139. package/src/services/thread/thread-title.service.ts +1 -1
  140. package/src/services/thread/thread-turn-execution.ts +87 -83
  141. package/src/services/thread/thread-turn-preparation.service.ts +65 -40
  142. package/src/services/thread/thread-turn-streaming.ts +32 -36
  143. package/src/services/thread/thread-turn.ts +43 -29
  144. package/src/services/thread/thread.service.ts +32 -8
  145. package/src/services/user.service.ts +1 -1
  146. package/src/services/write-intent-validator.service.ts +1 -1
  147. package/src/storage/attachment-storage.service.ts +7 -4
  148. package/src/storage/generated-document-storage.service.ts +1 -1
  149. package/src/system-agents/context-compaction.agent.ts +1 -1
  150. package/src/system-agents/helper-agent-options.ts +1 -1
  151. package/src/system-agents/memory-reranker.agent.ts +1 -1
  152. package/src/system-agents/memory.agent.ts +1 -1
  153. package/src/system-agents/recent-activity-title-refiner.agent.ts +9 -6
  154. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  155. package/src/system-agents/skill-extractor.agent.ts +1 -1
  156. package/src/system-agents/skill-manager.agent.ts +1 -1
  157. package/src/system-agents/thread-router.agent.ts +23 -20
  158. package/src/system-agents/title-generator.agent.ts +1 -1
  159. package/src/tools/execution-plan.tool.ts +36 -20
  160. package/src/tools/fetch-webpage.tool.ts +30 -22
  161. package/src/tools/firecrawl-client.ts +1 -6
  162. package/src/tools/plan-approval.tool.ts +9 -1
  163. package/src/tools/remember-memory.tool.ts +3 -6
  164. package/src/tools/research-topic.tool.ts +12 -3
  165. package/src/tools/search-web.tool.ts +26 -18
  166. package/src/tools/search.tool.ts +4 -5
  167. package/src/tools/team-think.tool.ts +139 -121
  168. package/src/utils/async.ts +15 -6
  169. package/src/utils/errors.ts +27 -15
  170. package/src/workers/bootstrap.ts +34 -58
  171. package/src/workers/memory-consolidation.worker.ts +4 -1
  172. package/src/workers/organization-learning.worker.ts +16 -3
  173. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  174. package/src/workers/regular-chat-memory-digest.runner.ts +46 -29
  175. package/src/workers/skill-extraction.runner.ts +13 -15
  176. package/src/workers/worker-utils.ts +14 -8
  177. package/src/config/search.ts +0 -3
  178. package/src/effect/awaitable-effect.ts +0 -87
  179. package/src/effect/runtime-ref.ts +0 -25
  180. package/src/effect/runtime.ts +0 -31
  181. package/src/redis/runtime-connection.ts +0 -10
  182. package/src/runtime/agent-types.ts +0 -1
@@ -9,14 +9,19 @@ import type {
9
9
  } from '@lota-sdk/shared'
10
10
  import { Effect } from 'effect'
11
11
 
12
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
12
13
  import { serverLogger } from '../config/logger'
13
14
  import { recordIdToString } from '../db/record-id'
14
15
  import { TABLES } from '../db/tables'
15
- import { BadRequestError } from '../effect/errors'
16
+ import { BadRequestError, ServiceError } from '../effect/errors'
17
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
18
+ import type { PlanAgentHeartbeatQueueRuntime } from '../queues/plan-agent-heartbeat.queue'
16
19
  import { shouldPlanNodeUseVisibleTurn } from '../runtime/execution-plan-visibility'
17
20
  import type { makePlanExecutorService } from './plan/plan-executor.service'
18
21
  import type { makePlanRunService } from './plan/plan-run.service'
19
22
 
23
+ const tryGraphFullPromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
24
+
20
25
  function classifyDispatchFailure(ownerType: string, error: unknown): PlanFailureClass {
21
26
  const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()
22
27
  if (errorMessage.includes('timeout')) return 'timeout_exceeded'
@@ -30,19 +35,21 @@ function formatDispatchError(error: unknown): string {
30
35
 
31
36
  const STABLE_RUN_STATUSES = new Set(['pending-approval', 'awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
32
37
 
33
- interface GraphFullRoutingDeps {
38
+ interface GraphFullRoutingDeps<E = never> {
39
+ agentConfig: ResolvedAgentConfig
34
40
  dispatchReadyNode(params: {
35
41
  run: PlanRunRecord
36
42
  nodeSpecRecord: PlanNodeSpecRecord
37
43
  nodeRun: PlanNodeRunRecord
38
44
  spec: PlanSpecRecord
39
45
  executionModeOverride?: ExecutionMode
40
- }): Effect.Effect<PlanNodeResultSubmission, unknown>
46
+ }): Effect.Effect<PlanNodeResultSubmission, E, never>
41
47
  planExecutorService: ReturnType<typeof makePlanExecutorService>
42
48
  planRunService: ReturnType<typeof makePlanRunService>
49
+ planAgentHeartbeatQueue: PlanAgentHeartbeatQueueRuntime
43
50
  }
44
51
 
45
- export function routeGraphFullEffect(params: { threadId: string; runId: string }, deps: GraphFullRoutingDeps) {
52
+ export function routeGraphFullEffect<E>(params: { threadId: string; runId: string }, deps: GraphFullRoutingDeps<E>) {
46
53
  return Effect.gen(function* () {
47
54
  const MAX_ROUNDS = 32
48
55
  const STRUCTURAL_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
@@ -65,26 +72,25 @@ export function routeGraphFullEffect(params: { threadId: string; runId: string }
65
72
 
66
73
  const silentNodes = readyNodes.filter((nr: PlanNodeRunRecord) => {
67
74
  const ns = nodeSpecs.find((s: PlanNodeSpecRecord) => s.nodeId === nr.nodeId)
68
- return ns && !shouldPlanNodeUseVisibleTurn(spec, ns)
75
+ return ns && !shouldPlanNodeUseVisibleTurn(deps.agentConfig, spec, ns)
69
76
  })
70
77
  const visibleNodes = readyNodes.filter((nr: PlanNodeRunRecord) => {
71
78
  const ns = nodeSpecs.find((s: PlanNodeSpecRecord) => s.nodeId === nr.nodeId)
72
- return ns && shouldPlanNodeUseVisibleTurn(spec, ns)
79
+ return ns && shouldPlanNodeUseVisibleTurn(deps.agentConfig, spec, ns)
73
80
  })
74
81
 
75
82
  yield* Effect.forEach(
76
83
  readyNodes,
77
84
  (nodeRun) =>
78
- Effect.tryPromise(() =>
79
- deps.planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId }),
85
+ tryGraphFullPromise(
86
+ () => deps.planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId }),
87
+ 'Failed to transition plan node to running.',
80
88
  ),
81
89
  { concurrency: 'unbounded' },
82
90
  )
83
91
 
84
92
  if (visibleNodes.length > 0) {
85
- const { enqueuePlanAgentHeartbeatWake } = yield* Effect.tryPromise(
86
- () => import('../queues/plan-agent-heartbeat.queue'),
87
- )
93
+ const enqueuePlanAgentHeartbeatWake = deps.planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake
88
94
  const updatedRunForWake = yield* deps.planRunService.getRunById(params.runId)
89
95
  yield* Effect.forEach(
90
96
  visibleNodes,
@@ -95,15 +101,17 @@ export function routeGraphFullEffect(params: { threadId: string; runId: string }
95
101
  return
96
102
  }
97
103
 
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
- }),
104
+ yield* tryGraphFullPromise(
105
+ () =>
106
+ enqueuePlanAgentHeartbeatWake({
107
+ organizationId: recordIdToString(updatedRunForWake.organizationId, TABLES.ORGANIZATION),
108
+ threadId: recordIdToString(updatedRunForWake.threadId, TABLES.THREAD),
109
+ runId: recordIdToString(updatedRunForWake.id, TABLES.PLAN_RUN),
110
+ nodeId: nodeRun.nodeId,
111
+ agentId: ns.owner.ref,
112
+ reason: 'graph-full-visible',
113
+ }),
114
+ 'Failed to enqueue plan agent heartbeat wake.',
107
115
  )
108
116
  }),
109
117
  { concurrency: 'unbounded' },
@@ -150,26 +158,30 @@ export function routeGraphFullEffect(params: { threadId: string; runId: string }
150
158
  const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
151
159
 
152
160
  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
- }),
161
+ yield* tryGraphFullPromise(
162
+ () =>
163
+ deps.planExecutorService.submitNodeResult({
164
+ threadId,
165
+ runId,
166
+ nodeId: settled.value.nodeId,
167
+ emittedBy: settled.value.ownerRef,
168
+ result: settled.value.result,
169
+ }),
170
+ 'Failed to submit plan node result.',
161
171
  )
162
172
  } else {
163
173
  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
- }),
174
+ yield* tryGraphFullPromise(
175
+ () =>
176
+ deps.planExecutorService.blockNodeOnDispatchFailure({
177
+ threadId,
178
+ runId,
179
+ nodeId: nodeRun.nodeId,
180
+ emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
181
+ message: formatDispatchError(settled.reason),
182
+ failureClass: classifyDispatchFailure(nodeSpecRecord?.owner.executorType ?? 'agent', settled.reason),
183
+ }),
184
+ 'Failed to block plan node on dispatch failure.',
173
185
  )
174
186
  }
175
187
  }
@@ -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
  )
@@ -1,4 +1,5 @@
1
- import { getAgentRoster } from '../../config/agent-defaults'
1
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
2
+ import { isAgentName } from '../../config/agent-defaults'
2
3
  import type { ExtractedFact, Message } from '../../db/memory-types'
3
4
  import { sanitizeAgentOutputForMemory } from '../../runtime/llm-content'
4
5
  import { compactWhitespace, truncateText } from '../../utils/string'
@@ -9,8 +10,8 @@ const MAX_CONVERSATION_MEMORY_BLOCK_CHARS = 2_000
9
10
  const MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS = 6_000
10
11
  const LOW_VALUE_MEMORY_IMPORTANCE_THRESHOLD = 0.45
11
12
 
12
- export const isRoutableAgentName = (value?: string): value is string =>
13
- Boolean(value && getAgentRoster().includes(value))
13
+ export const isRoutableAgentName = (agentConfig: ResolvedAgentConfig, value?: string): value is string =>
14
+ isAgentName(agentConfig, value)
14
15
 
15
16
  function normalizeConversationText(value: string, maxChars: number): string {
16
17
  const normalized = compactWhitespace(value)
@@ -18,7 +19,11 @@ function normalizeConversationText(value: string, maxChars: number): string {
18
19
  return truncateText(normalized, maxChars)
19
20
  }
20
21
 
21
- export function resolveAgentScopeNames(agentName?: string, agentNames: string[] = []): string[] {
22
+ export function resolveAgentScopeNames(
23
+ agentConfig: ResolvedAgentConfig,
24
+ agentName?: string,
25
+ agentNames: string[] = [],
26
+ ): string[] {
22
27
  const unique = new Set<string>()
23
28
  if (typeof agentName === 'string' && agentName.trim()) {
24
29
  unique.add(agentName.trim())
@@ -29,7 +34,7 @@ export function resolveAgentScopeNames(agentName?: string, agentNames: string[]
29
34
  if (!normalized) continue
30
35
  unique.add(normalized)
31
36
  }
32
- return [...unique].filter((name) => isRoutableAgentName(name))
37
+ return [...unique].filter((name) => isRoutableAgentName(agentConfig, name))
33
38
  }
34
39
 
35
40
  export function buildConversationMessages(params: {
@@ -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'