@lota-sdk/core 0.4.12 → 0.4.14

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 (139) hide show
  1. package/package.json +4 -4
  2. package/src/ai/embedding-cache.ts +17 -11
  3. package/src/ai-gateway/ai-gateway.ts +164 -94
  4. package/src/ai-gateway/index.ts +4 -1
  5. package/src/config/agent-defaults.ts +2 -2
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/create-runtime.ts +259 -200
  8. package/src/db/cursor-pagination.ts +2 -9
  9. package/src/db/memory-store.ts +194 -175
  10. package/src/db/memory.ts +125 -71
  11. package/src/db/schema-fingerprint.ts +5 -4
  12. package/src/db/service-normalization.ts +4 -3
  13. package/src/db/service.ts +3 -2
  14. package/src/db/startup.ts +15 -16
  15. package/src/effect/errors.ts +161 -21
  16. package/src/effect/index.ts +0 -1
  17. package/src/embeddings/provider.ts +15 -7
  18. package/src/queues/autonomous-job.queue.ts +10 -22
  19. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  20. package/src/queues/document-processor.queue.ts +13 -4
  21. package/src/queues/memory-consolidation.queue.ts +26 -14
  22. package/src/queues/plan-agent-heartbeat.queue.ts +10 -9
  23. package/src/queues/plan-scheduler.queue.ts +37 -15
  24. package/src/queues/queue-factory.ts +59 -35
  25. package/src/queues/standalone-worker.ts +3 -2
  26. package/src/redis/connection.ts +10 -3
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +5 -5
  29. package/src/redis/stream-context.ts +1 -1
  30. package/src/runtime/chat-message.ts +64 -1
  31. package/src/runtime/chat-run-orchestration.ts +33 -20
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  33. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  34. package/src/runtime/domain-layer.ts +13 -7
  35. package/src/runtime/execution-plan.ts +7 -3
  36. package/src/runtime/live-turn-trace.ts +6 -49
  37. package/src/runtime/memory/memory-block.ts +3 -9
  38. package/src/runtime/memory/memory-scope.ts +3 -1
  39. package/src/runtime/plugin-resolution.ts +2 -1
  40. package/src/runtime/post-turn-side-effects.ts +6 -5
  41. package/src/runtime/retrieval-adapters.ts +8 -20
  42. package/src/runtime/runtime-config.ts +3 -9
  43. package/src/runtime/runtime-extensions.ts +2 -4
  44. package/src/runtime/runtime-lifecycle.ts +56 -16
  45. package/src/runtime/runtime-services.ts +180 -102
  46. package/src/runtime/runtime-worker-registry.ts +3 -1
  47. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  48. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  49. package/src/runtime/social-chat/social-chat.ts +356 -223
  50. package/src/runtime/specialist-runner.ts +3 -1
  51. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  52. package/src/runtime/thread-turn-context.ts +142 -102
  53. package/src/runtime/turn-lifecycle.ts +15 -46
  54. package/src/services/agent-activity.service.ts +1 -1
  55. package/src/services/agent-executor.service.ts +107 -77
  56. package/src/services/autonomous-job.service.ts +354 -293
  57. package/src/services/background-work.service.ts +3 -3
  58. package/src/services/context-compaction.service.ts +7 -2
  59. package/src/services/document-chunk.service.ts +50 -32
  60. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  61. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  62. package/src/services/feedback-loop.service.ts +5 -4
  63. package/src/services/graph-full-routing.ts +37 -36
  64. package/src/services/institutional-memory.service.ts +28 -30
  65. package/src/services/learned-skill.service.ts +107 -72
  66. package/src/services/memory/memory-errors.ts +4 -23
  67. package/src/services/memory/memory-org-memory.ts +10 -5
  68. package/src/services/memory/memory-rerank.ts +18 -6
  69. package/src/services/memory/memory.service.ts +170 -111
  70. package/src/services/memory/rerank.service.ts +29 -20
  71. package/src/services/organization-member.service.ts +1 -1
  72. package/src/services/organization.service.ts +69 -75
  73. package/src/services/ownership-dispatcher.service.ts +40 -39
  74. package/src/services/plan/plan-agent-heartbeat.service.ts +26 -23
  75. package/src/services/plan/plan-agent-query.service.ts +39 -31
  76. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  77. package/src/services/plan/plan-coordination.service.ts +2 -1
  78. package/src/services/plan/plan-cycle.service.ts +6 -5
  79. package/src/services/plan/plan-deadline.service.ts +57 -54
  80. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  81. package/src/services/plan/plan-executor-graph.ts +18 -15
  82. package/src/services/plan/plan-executor.service.ts +235 -262
  83. package/src/services/plan/plan-run.service.ts +169 -93
  84. package/src/services/plan/plan-scheduler.service.ts +192 -202
  85. package/src/services/plan/plan-template.service.ts +1 -1
  86. package/src/services/plan/plan-transaction-events.ts +1 -1
  87. package/src/services/plan/plan-workspace.service.ts +23 -14
  88. package/src/services/plugin-executor.service.ts +5 -9
  89. package/src/services/queue-job.service.ts +117 -59
  90. package/src/services/recent-activity-title.service.ts +13 -12
  91. package/src/services/recent-activity.service.ts +6 -1
  92. package/src/services/social-chat-history.service.ts +29 -25
  93. package/src/services/system-executor.service.ts +5 -9
  94. package/src/services/thread/thread-active-run.ts +2 -2
  95. package/src/services/thread/thread-listing.ts +61 -57
  96. package/src/services/thread/thread-memory-block.ts +73 -48
  97. package/src/services/thread/thread-message.service.ts +76 -65
  98. package/src/services/thread/thread-record-store.ts +8 -8
  99. package/src/services/thread/thread-title.service.ts +10 -4
  100. package/src/services/thread/thread-turn-execution.ts +43 -45
  101. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  102. package/src/services/thread/thread-turn-streaming.ts +82 -85
  103. package/src/services/thread/thread-turn.ts +8 -8
  104. package/src/services/thread/thread.service.ts +135 -100
  105. package/src/services/user.service.ts +45 -48
  106. package/src/storage/attachment-parser.ts +6 -2
  107. package/src/storage/attachment-storage.service.ts +5 -6
  108. package/src/storage/generated-document-storage.service.ts +1 -1
  109. package/src/system-agents/context-compaction.agent.ts +10 -9
  110. package/src/system-agents/delegated-agent-factory.ts +30 -6
  111. package/src/system-agents/memory-reranker.agent.ts +10 -9
  112. package/src/system-agents/memory.agent.ts +10 -9
  113. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  114. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  115. package/src/system-agents/skill-extractor.agent.ts +13 -12
  116. package/src/system-agents/skill-manager.agent.ts +13 -12
  117. package/src/system-agents/thread-router.agent.ts +10 -5
  118. package/src/system-agents/title-generator.agent.ts +13 -12
  119. package/src/tools/fetch-webpage.tool.ts +13 -13
  120. package/src/tools/memory-block.tool.ts +3 -1
  121. package/src/tools/plan-approval.tool.ts +4 -2
  122. package/src/tools/read-file-parts.tool.ts +10 -4
  123. package/src/tools/remember-memory.tool.ts +3 -1
  124. package/src/tools/research-topic.tool.ts +9 -5
  125. package/src/tools/search-web.tool.ts +16 -16
  126. package/src/tools/search.tool.ts +20 -5
  127. package/src/tools/team-think.tool.ts +61 -38
  128. package/src/utils/async.ts +5 -5
  129. package/src/utils/errors.ts +19 -18
  130. package/src/utils/sse-keepalive.ts +28 -25
  131. package/src/workers/bootstrap.ts +75 -11
  132. package/src/workers/memory-consolidation.worker.ts +82 -91
  133. package/src/workers/organization-learning.worker.ts +14 -4
  134. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  135. package/src/workers/skill-extraction.runner.ts +97 -61
  136. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  137. package/src/workers/utils/thread-message-query.ts +24 -24
  138. package/src/workers/worker-utils.ts +23 -4
  139. package/src/effect/helpers.ts +0 -123
@@ -14,14 +14,11 @@ import { serverLogger } from '../config/logger'
14
14
  import { recordIdToString } from '../db/record-id'
15
15
  import { TABLES } from '../db/tables'
16
16
  import { BadRequestError, ServiceError } from '../effect/errors'
17
- import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
18
17
  import type { PlanAgentHeartbeatQueueRuntime } from '../queues/plan-agent-heartbeat.queue'
19
18
  import { shouldPlanNodeUseVisibleTurn } from '../runtime/execution-plan-visibility'
20
19
  import type { makePlanExecutorService } from './plan/plan-executor.service'
21
20
  import type { makePlanRunService } from './plan/plan-run.service'
22
21
 
23
- const tryGraphFullPromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
24
-
25
22
  function classifyDispatchFailure(ownerType: string, error: unknown): PlanFailureClass {
26
23
  const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()
27
24
  if (errorMessage.includes('timeout')) return 'timeout_exceeded'
@@ -82,15 +79,17 @@ export function routeGraphFullEffect<E>(params: { threadId: string; runId: strin
82
79
  yield* Effect.forEach(
83
80
  readyNodes,
84
81
  (nodeRun) =>
85
- tryGraphFullPromise(
86
- () => deps.planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId }),
87
- 'Failed to transition plan node to running.',
88
- ),
82
+ deps.planExecutorService
83
+ .transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId })
84
+ .pipe(
85
+ Effect.mapError(
86
+ (cause) => new ServiceError({ message: 'Failed to transition plan node to running.', cause }),
87
+ ),
88
+ ),
89
89
  { concurrency: 'unbounded' },
90
90
  )
91
91
 
92
92
  if (visibleNodes.length > 0) {
93
- const enqueuePlanAgentHeartbeatWake = deps.planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake
94
93
  const updatedRunForWake = yield* deps.planRunService.getRunById(params.runId)
95
94
  yield* Effect.forEach(
96
95
  visibleNodes,
@@ -101,9 +100,9 @@ export function routeGraphFullEffect<E>(params: { threadId: string; runId: strin
101
100
  return
102
101
  }
103
102
 
104
- yield* tryGraphFullPromise(
105
- () =>
106
- enqueuePlanAgentHeartbeatWake({
103
+ yield* Effect.tryPromise({
104
+ try: () =>
105
+ deps.planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake({
107
106
  organizationId: recordIdToString(updatedRunForWake.organizationId, TABLES.ORGANIZATION),
108
107
  threadId: recordIdToString(updatedRunForWake.threadId, TABLES.THREAD),
109
108
  runId: recordIdToString(updatedRunForWake.id, TABLES.PLAN_RUN),
@@ -111,8 +110,8 @@ export function routeGraphFullEffect<E>(params: { threadId: string; runId: strin
111
110
  agentId: ns.owner.ref,
112
111
  reason: 'graph-full-visible',
113
112
  }),
114
- 'Failed to enqueue plan agent heartbeat wake.',
115
- )
113
+ catch: (cause) => new ServiceError({ message: 'Failed to enqueue plan agent heartbeat wake.', cause }),
114
+ })
116
115
  }),
117
116
  { concurrency: 'unbounded' },
118
117
  )
@@ -158,31 +157,33 @@ export function routeGraphFullEffect<E>(params: { threadId: string; runId: strin
158
157
  const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
159
158
 
160
159
  if (settled.status === 'fulfilled') {
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.',
171
- )
160
+ yield* deps.planExecutorService
161
+ .submitNodeResult({
162
+ threadId,
163
+ runId,
164
+ nodeId: settled.value.nodeId,
165
+ emittedBy: settled.value.ownerRef,
166
+ result: settled.value.result,
167
+ })
168
+ .pipe(
169
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to submit plan node result.', cause })),
170
+ )
172
171
  } else {
173
172
  serverLogger.warn`routeGraphFull: dispatch failed for node "${nodeRun.nodeId}": ${settled.reason}`
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.',
185
- )
173
+ yield* deps.planExecutorService
174
+ .blockNodeOnDispatchFailure({
175
+ threadId,
176
+ runId,
177
+ nodeId: nodeRun.nodeId,
178
+ emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
179
+ message: formatDispatchError(settled.reason),
180
+ failureClass: classifyDispatchFailure(nodeSpecRecord?.owner.executorType ?? 'agent', settled.reason),
181
+ })
182
+ .pipe(
183
+ Effect.mapError(
184
+ (cause) => new ServiceError({ message: 'Failed to block plan node on dispatch failure.', cause }),
185
+ ),
186
+ )
186
187
  }
187
188
  }
188
189
  }
@@ -11,7 +11,6 @@ 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'
15
14
  import { DatabaseServiceTag } from '../effect/services'
16
15
  import { unsafeDateFrom } from '../utils/date-time'
17
16
  import type { makePlanRunService } from './plan/plan-run.service'
@@ -23,21 +22,10 @@ interface InstitutionalMemoryDeps {
23
22
  }
24
23
 
25
24
  class InstitutionalMemoryServiceError extends Schema.TaggedErrorClass<InstitutionalMemoryServiceError>()(
26
- 'InstitutionalMemoryServiceError',
25
+ '@lota-sdk/core/InstitutionalMemoryServiceError',
27
26
  { message: Schema.String, cause: Schema.Defect },
28
27
  ) {}
29
28
 
30
- const effectTryInstitutionalMemoryPromise = makeEffectTryPromiseWithMessage(
31
- (message, cause) => new InstitutionalMemoryServiceError({ message, cause }),
32
- )
33
-
34
- function tryInstitutionalMemoryPromise<A>(
35
- message: string,
36
- evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
37
- ): Effect.Effect<A, InstitutionalMemoryServiceError> {
38
- return effectTryInstitutionalMemoryPromise(evaluate, message)
39
- }
40
-
41
29
  export function makeInstitutionalMemoryService(deps: InstitutionalMemoryDeps) {
42
30
  const { db, planRunService } = deps
43
31
 
@@ -48,8 +36,8 @@ export function makeInstitutionalMemoryService(deps: InstitutionalMemoryDeps) {
48
36
  confidence: number
49
37
  sampleCount: number
50
38
  }) =>
51
- tryInstitutionalMemoryPromise('Failed to persist institutional memory.', () =>
52
- db.create(
39
+ db
40
+ .create(
53
41
  TABLES.INSTITUTIONAL_MEMORY,
54
42
  {
55
43
  organizationId: params.organizationId,
@@ -59,8 +47,12 @@ export function makeInstitutionalMemoryService(deps: InstitutionalMemoryDeps) {
59
47
  sampleCount: params.sampleCount,
60
48
  },
61
49
  InstitutionalMemorySchema,
62
- ),
63
- )
50
+ )
51
+ .pipe(
52
+ Effect.mapError(
53
+ (cause) => new InstitutionalMemoryServiceError({ message: 'Failed to persist institutional memory.', cause }),
54
+ ),
55
+ )
64
56
 
65
57
  const extractPatternsEffect = (params: { organizationId: string; runId: string }) =>
66
58
  Effect.gen(function* () {
@@ -172,20 +164,26 @@ export function makeInstitutionalMemoryService(deps: InstitutionalMemoryDeps) {
172
164
  const orgRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
173
165
  const limit = params.limit ?? 10
174
166
  const fetchLimit = Math.max(limit * 3, 30)
175
- const records = yield* tryInstitutionalMemoryPromise(
176
- `Failed to query institutional memory for organization ${params.organizationId}.`,
177
- () =>
178
- db.queryMany(
179
- new BoundQuery(
180
- `SELECT * FROM ${TABLES.INSTITUTIONAL_MEMORY}
181
- WHERE organizationId = $orgId
182
- ORDER BY confidence DESC, createdAt DESC
183
- LIMIT $fetchLimit`,
184
- { orgId: orgRef, fetchLimit },
185
- ),
186
- InstitutionalMemorySchema,
167
+ const records = yield* db
168
+ .queryMany(
169
+ new BoundQuery(
170
+ `SELECT * FROM ${TABLES.INSTITUTIONAL_MEMORY}
171
+ WHERE organizationId = $orgId
172
+ ORDER BY confidence DESC, createdAt DESC
173
+ LIMIT $fetchLimit`,
174
+ { orgId: orgRef, fetchLimit },
187
175
  ),
188
- )
176
+ InstitutionalMemorySchema,
177
+ )
178
+ .pipe(
179
+ Effect.mapError(
180
+ (cause) =>
181
+ new InstitutionalMemoryServiceError({
182
+ message: `Failed to query institutional memory for organization ${params.organizationId}.`,
183
+ cause,
184
+ }),
185
+ ),
186
+ )
189
187
 
190
188
  const objectiveTerms = params.objective
191
189
  .toLowerCase()
@@ -3,17 +3,19 @@ import { Cache, Context, Schema, Duration, Effect, Layer } from 'effect'
3
3
  import { BoundQuery } from 'surrealdb'
4
4
  import { z } from 'zod'
5
5
 
6
+ import { RuntimeBridgeTag } from '../ai-gateway/ai-gateway'
7
+ import type { RuntimeBridge } from '../ai-gateway/ai-gateway'
6
8
  import { renderLearnedSkillInstructions } from '../ai/definitions'
7
9
  import { serverLogger } from '../config/logger'
8
10
  import { ensureRecordId, recordIdToString } from '../db/record-id'
9
11
  import type { SurrealDBService } from '../db/service'
10
12
  import { TABLES } from '../db/tables'
11
- import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
13
+ import { ERROR_TAGS } from '../effect/errors'
12
14
  import { DatabaseServiceTag, RuntimeConfigServiceTag } from '../effect/services'
13
15
  import { ProviderEmbeddings } from '../embeddings/provider'
14
16
  import { sha256HexFromParts } from '../utils/crypto'
15
17
  import { nowDate } from '../utils/date-time'
16
- import { BackgroundWorkService } from './background-work.service'
18
+ import { BackgroundWorkServiceTag } from './background-work.service'
17
19
 
18
20
  const PROMOTION_MIN_USES = 5
19
21
  const PROMOTION_MIN_SUCCESS_RATE = 0.6
@@ -56,25 +58,18 @@ const SearchResultRowSchema = z.object({
56
58
  similarity: z.number(),
57
59
  })
58
60
 
59
- class LearnedSkillServiceError extends Schema.TaggedErrorClass<LearnedSkillServiceError>()('LearnedSkillServiceError', {
60
- message: Schema.String,
61
- cause: Schema.Defect,
62
- }) {}
61
+ class LearnedSkillServiceError extends Schema.TaggedErrorClass<LearnedSkillServiceError>()(
62
+ ERROR_TAGS.LearnedSkillServiceError,
63
+ { message: Schema.String, cause: Schema.Defect },
64
+ ) {}
63
65
 
64
66
  class LearnedSkillNotFoundError extends Schema.TaggedErrorClass<LearnedSkillNotFoundError>()(
65
- 'LearnedSkillNotFoundError',
67
+ '@lota-sdk/core/LearnedSkillNotFoundError',
66
68
  { message: Schema.String },
67
69
  ) {}
68
70
 
69
- const effectTryLearnedSkillPromise = makeEffectTryPromiseWithMessage(
70
- (message, cause) => new LearnedSkillServiceError({ message, cause }),
71
- )
72
-
73
- function tryLearnedSkillPromise<A>(
74
- message: string,
75
- evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
76
- ): Effect.Effect<A, LearnedSkillServiceError> {
77
- return effectTryLearnedSkillPromise(evaluate, message)
71
+ function toLearnedSkillServiceError(message: string, cause: unknown): LearnedSkillServiceError {
72
+ return new LearnedSkillServiceError({ message, cause })
78
73
  }
79
74
 
80
75
  interface CreateLearnedSkillInput {
@@ -116,13 +111,14 @@ interface RetrieveForTurnParams {
116
111
 
117
112
  export function makeLearnedSkillService(
118
113
  db: SurrealDBService,
119
- options: { embeddingModel: string; openRouterApiKey?: string },
114
+ options: { embeddingModel: string; openRouterApiKey?: string; runPromise: RuntimeBridge['runPromise'] },
120
115
  skillExistsCache: Cache.Cache<string, boolean, LearnedSkillServiceError>,
121
- background: Context.Service.Shape<typeof BackgroundWorkService>,
116
+ background: Context.Service.Shape<typeof BackgroundWorkServiceTag>,
122
117
  ) {
123
118
  const embeddings = new ProviderEmbeddings({
124
119
  modelId: options.embeddingModel,
125
120
  openRouterApiKey: options.openRouterApiKey,
121
+ runPromise: options.runPromise,
126
122
  })
127
123
 
128
124
  const hasSkillsForAgent = (orgId: string, agentId: string) => Cache.get(skillExistsCache, `${orgId}:${agentId}`)
@@ -159,9 +155,9 @@ export function makeLearnedSkillService(
159
155
  hash: input.hash,
160
156
  }
161
157
 
162
- const result = yield* tryLearnedSkillPromise('Failed to create learned skill.', () =>
163
- db.create(TABLES.LEARNED_SKILL, data, LearnedSkillRowSchema),
164
- )
158
+ const result = yield* db
159
+ .create(TABLES.LEARNED_SKILL, data, LearnedSkillRowSchema)
160
+ .pipe(Effect.mapError((cause) => toLearnedSkillServiceError('Failed to create learned skill.', cause)))
165
161
  yield* invalidateSkillExistsCache(input.organizationId, input.agentId)
166
162
  return result
167
163
  })
@@ -183,9 +179,11 @@ export function makeLearnedSkillService(
183
179
  if (input.hash !== undefined) data.hash = input.hash
184
180
  if (input.supersedes !== undefined) data.supersedes = ensureRecordId(input.supersedes, TABLES.LEARNED_SKILL)
185
181
 
186
- const updated = yield* tryLearnedSkillPromise(`Failed to update learned skill ${skillId}.`, () =>
187
- db.update(TABLES.LEARNED_SKILL, ref, data, LearnedSkillRowSchema),
188
- )
182
+ const updated = yield* db
183
+ .update(TABLES.LEARNED_SKILL, ref, data, LearnedSkillRowSchema)
184
+ .pipe(
185
+ Effect.mapError((cause) => toLearnedSkillServiceError(`Failed to update learned skill ${skillId}.`, cause)),
186
+ )
189
187
  if (!updated) {
190
188
  return yield* new LearnedSkillNotFoundError({ message: `Learned skill ${skillId} not found` })
191
189
  }
@@ -196,18 +194,20 @@ export function makeLearnedSkillService(
196
194
  Effect.gen(function* () {
197
195
  const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
198
196
  const skill = yield* getById(skillId)
199
- yield* tryLearnedSkillPromise(`Failed to archive learned skill ${skillId}.`, () =>
200
- db.update(TABLES.LEARNED_SKILL, ref, { status: 'archived', archivedAt: nowDate() }, LearnedSkillRowSchema),
201
- )
197
+ yield* db
198
+ .update(TABLES.LEARNED_SKILL, ref, { status: 'archived', archivedAt: nowDate() }, LearnedSkillRowSchema)
199
+ .pipe(
200
+ Effect.mapError((cause) => toLearnedSkillServiceError(`Failed to archive learned skill ${skillId}.`, cause)),
201
+ )
202
202
  if (skill) {
203
203
  yield* invalidateSkillExistsCache(recordIdToString(skill.organizationId), skill.agentId ?? null)
204
204
  }
205
205
  })
206
206
 
207
207
  const getById = (skillId: string): Effect.Effect<LearnedSkillRow | null, LearnedSkillServiceError> =>
208
- tryLearnedSkillPromise(`Failed to load learned skill ${skillId}.`, () =>
209
- db.findOne(TABLES.LEARNED_SKILL, { id: ensureRecordId(skillId, TABLES.LEARNED_SKILL) }, LearnedSkillRowSchema),
210
- )
208
+ db
209
+ .findOne(TABLES.LEARNED_SKILL, { id: ensureRecordId(skillId, TABLES.LEARNED_SKILL) }, LearnedSkillRowSchema)
210
+ .pipe(Effect.mapError((cause) => toLearnedSkillServiceError(`Failed to load learned skill ${skillId}.`, cause)))
211
211
 
212
212
  const searchForTurn = Effect.fn('LearnedSkillService.searchForTurn')(function* (params: RetrieveForTurnParams) {
213
213
  const orgRef = ensureRecordId(params.orgId, TABLES.ORGANIZATION)
@@ -242,22 +242,26 @@ export function makeLearnedSkillService(
242
242
  return []
243
243
  }
244
244
 
245
- const queryEmbedding = yield* tryLearnedSkillPromise('Failed to embed learned skill query.', () =>
246
- embeddings.embedQuery(params.query),
247
- ).pipe(Effect.withSpan('LearnedSkillService.embedQuery'))
245
+ const queryEmbedding = yield* Effect.tryPromise({
246
+ try: () => embeddings.embedQuery(params.query),
247
+ catch: (cause) => toLearnedSkillServiceError('Failed to embed learned skill query.', cause),
248
+ }).pipe(Effect.withSpan('LearnedSkillService.embedQuery'))
248
249
  yield* Effect.annotateCurrentSpan('embeddingLength', queryEmbedding.length)
249
250
  if (queryEmbedding.length === 0) return []
250
251
 
251
- const rows = yield* tryLearnedSkillPromise('Failed to query learned skills.', () =>
252
- db.query<unknown>(
252
+ const rows = yield* db
253
+ .query<unknown>(
253
254
  new BoundQuery(sql, {
254
255
  organizationId: orgRef,
255
256
  embedding: queryEmbedding,
256
257
  agentId: params.agentId,
257
258
  minConfidence: params.minConfidence,
258
259
  }),
259
- ),
260
- ).pipe(Effect.withSpan('LearnedSkillService.queryNearestSkills'))
260
+ )
261
+ .pipe(
262
+ Effect.mapError((cause) => toLearnedSkillServiceError('Failed to query learned skills.', cause)),
263
+ Effect.withSpan('LearnedSkillService.queryNearestSkills'),
264
+ )
261
265
  const parsedRows = yield* Effect.try({
262
266
  try: () => rows.map((row) => SearchResultRowSchema.parse(row)),
263
267
  catch: (cause) =>
@@ -295,24 +299,32 @@ export function makeLearnedSkillService(
295
299
  const recordUsage = (skillId: string) =>
296
300
  Effect.gen(function* () {
297
301
  const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
298
- yield* tryLearnedSkillPromise(`Failed to record usage for learned skill ${skillId}.`, () =>
299
- db.query<unknown>(
302
+ yield* db
303
+ .query<unknown>(
300
304
  new BoundQuery(
301
305
  `UPDATE ${TABLES.LEARNED_SKILL} SET usageCount += 1, lastUsedAt = time::now() WHERE id = $id`,
302
306
  { id: ref },
303
307
  ),
304
- ),
305
- )
308
+ )
309
+ .pipe(
310
+ Effect.mapError((cause) =>
311
+ toLearnedSkillServiceError(`Failed to record usage for learned skill ${skillId}.`, cause),
312
+ ),
313
+ )
306
314
  })
307
315
 
308
316
  const recordSuccess = (skillId: string) =>
309
317
  Effect.gen(function* () {
310
318
  const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
311
- yield* tryLearnedSkillPromise(`Failed to record success for learned skill ${skillId}.`, () =>
312
- db.query<unknown>(
319
+ yield* db
320
+ .query<unknown>(
313
321
  new BoundQuery(`UPDATE ${TABLES.LEARNED_SKILL} SET successCount += 1 WHERE id = $id`, { id: ref }),
314
- ),
315
- )
322
+ )
323
+ .pipe(
324
+ Effect.mapError((cause) =>
325
+ toLearnedSkillServiceError(`Failed to record success for learned skill ${skillId}.`, cause),
326
+ ),
327
+ )
316
328
  })
317
329
 
318
330
  const promoteIfEligible = (skillId: string) =>
@@ -326,14 +338,16 @@ export function makeLearnedSkillService(
326
338
  if (successRate < PROMOTION_MIN_SUCCESS_RATE) return false
327
339
 
328
340
  const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
329
- yield* tryLearnedSkillPromise(`Failed to promote learned skill ${skillId}.`, () =>
330
- db.update(
341
+ yield* db
342
+ .update(
331
343
  TABLES.LEARNED_SKILL,
332
344
  ref,
333
345
  { status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0) },
334
346
  LearnedSkillRowSchema,
335
- ),
336
- )
347
+ )
348
+ .pipe(
349
+ Effect.mapError((cause) => toLearnedSkillServiceError(`Failed to promote learned skill ${skillId}.`, cause)),
350
+ )
337
351
  return true
338
352
  })
339
353
 
@@ -353,14 +367,17 @@ export function makeLearnedSkillService(
353
367
  LIMIT 1
354
368
  `
355
369
 
356
- const descEmbedding = yield* tryLearnedSkillPromise('Failed to embed learned skill description.', () =>
357
- embeddings.embedQuery(description),
358
- )
370
+ const descEmbedding = yield* Effect.tryPromise({
371
+ try: () => embeddings.embedQuery(description),
372
+ catch: (cause) => toLearnedSkillServiceError('Failed to embed learned skill description.', cause),
373
+ })
359
374
  if (descEmbedding.length === 0) return null
360
375
 
361
- const rows = yield* tryLearnedSkillPromise('Failed to query most similar learned skill.', () =>
362
- db.query<unknown>(new BoundQuery(sql, { organizationId: orgRef, embedding: descEmbedding })),
363
- )
376
+ const rows = yield* db
377
+ .query<unknown>(new BoundQuery(sql, { organizationId: orgRef, embedding: descEmbedding }))
378
+ .pipe(
379
+ Effect.mapError((cause) => toLearnedSkillServiceError('Failed to query most similar learned skill.', cause)),
380
+ )
364
381
  if (rows.length === 0) return null
365
382
  return yield* Effect.try({
366
383
  try: () => LearnedSkillRowSchema.parse(rows[0]),
@@ -370,8 +387,8 @@ export function makeLearnedSkillService(
370
387
  })
371
388
 
372
389
  const listForOrg = (orgId: string) =>
373
- tryLearnedSkillPromise(`Failed to list learned skills for organization ${orgId}.`, () =>
374
- db.queryMany(
390
+ db
391
+ .queryMany(
375
392
  new BoundQuery(
376
393
  `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
377
394
  FROM ${TABLES.LEARNED_SKILL}
@@ -381,15 +398,19 @@ export function makeLearnedSkillService(
381
398
  { organizationId: ensureRecordId(orgId, TABLES.ORGANIZATION) },
382
399
  ),
383
400
  LearnedSkillRowSchema,
384
- ),
385
- )
401
+ )
402
+ .pipe(
403
+ Effect.mapError((cause) =>
404
+ toLearnedSkillServiceError(`Failed to list learned skills for organization ${orgId}.`, cause),
405
+ ),
406
+ )
386
407
 
387
408
  const findByNameOrTag = (orgId: string, nameOrTag: string) =>
388
409
  Effect.gen(function* () {
389
410
  const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
390
411
  const normalizedRef = nameOrTag.trim().toLowerCase()
391
- const rows = yield* tryLearnedSkillPromise(`Failed to find learned skill by name or tag for org ${orgId}.`, () =>
392
- db.queryMany(
412
+ const rows = yield* db
413
+ .queryMany(
393
414
  new BoundQuery(
394
415
  `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
395
416
  FROM ${TABLES.LEARNED_SKILL}
@@ -401,16 +422,20 @@ export function makeLearnedSkillService(
401
422
  { organizationId: orgRef, nameRef: normalizedRef },
402
423
  ),
403
424
  LearnedSkillRowSchema,
404
- ),
405
- )
425
+ )
426
+ .pipe(
427
+ Effect.mapError((cause) =>
428
+ toLearnedSkillServiceError(`Failed to find learned skill by name or tag for org ${orgId}.`, cause),
429
+ ),
430
+ )
406
431
  return rows[0] ?? null
407
432
  })
408
433
 
409
434
  const findByHash = (orgId: string, hash: string): Effect.Effect<LearnedSkillRow | null, LearnedSkillServiceError> =>
410
435
  Effect.gen(function* () {
411
436
  const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
412
- const rows = yield* tryLearnedSkillPromise(`Failed to find learned skill by hash for org ${orgId}.`, () =>
413
- db.queryMany(
437
+ const rows = yield* db
438
+ .queryMany(
414
439
  new BoundQuery(
415
440
  `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
416
441
  FROM ${TABLES.LEARNED_SKILL}
@@ -421,8 +446,12 @@ export function makeLearnedSkillService(
421
446
  { organizationId: orgRef, hash },
422
447
  ),
423
448
  LearnedSkillRowSchema,
424
- ),
425
- )
449
+ )
450
+ .pipe(
451
+ Effect.mapError((cause) =>
452
+ toLearnedSkillServiceError(`Failed to find learned skill by hash for org ${orgId}.`, cause),
453
+ ),
454
+ )
426
455
  return rows[0] ?? null
427
456
  })
428
457
 
@@ -458,14 +487,15 @@ export const LearnedSkillServiceLive = Layer.effect(
458
487
  Effect.gen(function* () {
459
488
  const db = yield* DatabaseServiceTag
460
489
  const runtimeConfig = yield* RuntimeConfigServiceTag
461
- const background = yield* BackgroundWorkService
490
+ const background = yield* BackgroundWorkServiceTag
491
+ const bridge = yield* RuntimeBridgeTag
462
492
  const skillExistsCache = yield* Cache.make({
463
493
  lookup: (key: string) =>
464
494
  Effect.gen(function* () {
465
495
  const [orgId, agentId] = key.split(':', 2) as [string, string]
466
496
  const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
467
- const rows = yield* tryLearnedSkillPromise('Failed to check learned skill existence cache.', () =>
468
- db.query<{ id: unknown }>(
497
+ const rows = yield* db
498
+ .query<{ id: unknown }>(
469
499
  new BoundQuery(
470
500
  `SELECT id FROM ${TABLES.LEARNED_SKILL}
471
501
  WHERE organizationId = $orgRef
@@ -474,8 +504,12 @@ export const LearnedSkillServiceLive = Layer.effect(
474
504
  LIMIT 1`,
475
505
  { orgRef, agentId },
476
506
  ),
477
- ),
478
- )
507
+ )
508
+ .pipe(
509
+ Effect.mapError((cause) =>
510
+ toLearnedSkillServiceError('Failed to check learned skill existence cache.', cause),
511
+ ),
512
+ )
479
513
  return rows.length > 0
480
514
  }),
481
515
  capacity: 256,
@@ -486,6 +520,7 @@ export const LearnedSkillServiceLive = Layer.effect(
486
520
  {
487
521
  embeddingModel: runtimeConfig.aiGateway.embeddingModel,
488
522
  openRouterApiKey: runtimeConfig.aiGateway.openRouterApiKey,
523
+ runPromise: bridge.runPromise,
489
524
  },
490
525
  skillExistsCache,
491
526
  background,
@@ -1,27 +1,8 @@
1
- import { Effect, Schema } from 'effect'
1
+ import { Schema } from 'effect'
2
2
 
3
- export class MemoryServiceError extends Schema.TaggedErrorClass<MemoryServiceError>()('MemoryServiceError', {
3
+ import { ERROR_TAGS } from '../../effect/errors'
4
+
5
+ export class MemoryServiceError extends Schema.TaggedErrorClass<MemoryServiceError>()(ERROR_TAGS.MemoryServiceError, {
4
6
  message: Schema.String,
5
7
  cause: Schema.Defect,
6
8
  }) {}
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,14 +1,15 @@
1
- import type { Context } from 'effect'
1
+ import type { Context, Effect as EffectType } from 'effect'
2
2
  import { Cache, Duration, Effect } from 'effect'
3
3
 
4
+ import type { AiGatewayModels } from '../../ai-gateway/ai-gateway'
4
5
  import { aiLogger } from '../../config/logger'
5
6
  import { Memory } from '../../db/memory'
6
7
  import type { SurrealDBService } from '../../db/service'
7
8
  import type { HelperModelRuntime } from '../../runtime/helper-model'
8
9
  import { ORG_SCOPE_PREFIX, scopeId } from '../../runtime/memory/memory-scope'
9
10
  import type { ResolvedLotaRuntimeConfig } from '../../runtime/runtime-config'
10
- import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../../system-agents/memory.agent'
11
- import type { BackgroundWorkService } from '../background-work.service'
11
+ import { makeOrgMemoryAgentFactory, ORG_MEMORY_PROMPT } from '../../system-agents/memory.agent'
12
+ import type { BackgroundWorkServiceTag } from '../background-work.service'
12
13
 
13
14
  const MAX_ORG_MEMORY_CLIENTS = 128
14
15
 
@@ -16,12 +17,15 @@ interface OrgMemoryDeps {
16
17
  db: SurrealDBService
17
18
  runtimeConfig: ResolvedLotaRuntimeConfig
18
19
  helperModelRuntime: HelperModelRuntime
19
- background: Context.Service.Shape<typeof BackgroundWorkService>
20
+ background: Context.Service.Shape<typeof BackgroundWorkServiceTag>
21
+ aiGatewayModels: AiGatewayModels
22
+ runPromise: <A, E>(effect: EffectType.Effect<A, E>) => Promise<A>
20
23
  }
21
24
 
22
25
  export type OrgMemoryCache = Cache.Cache<string, Memory>
23
26
 
24
27
  export function makeOrgMemoryCache(deps: OrgMemoryDeps) {
28
+ const createAgent = makeOrgMemoryAgentFactory(deps.aiGatewayModels)
25
29
  return Cache.make({
26
30
  lookup: (cacheKey: string) =>
27
31
  Effect.sync(() => {
@@ -32,8 +36,9 @@ export function makeOrgMemoryCache(deps: OrgMemoryDeps) {
32
36
  runtimeConfig: deps.runtimeConfig,
33
37
  helperModelRuntime: deps.helperModelRuntime,
34
38
  background: deps.background,
39
+ runPromise: deps.runPromise,
35
40
  },
36
- { createAgent: createOrgMemoryAgent },
41
+ { createAgent },
37
42
  { customPrompt: ORG_MEMORY_PROMPT },
38
43
  )
39
44
  }),