@lota-sdk/core 0.4.8 → 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 (272) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +96 -22
  3. package/src/ai-gateway/ai-gateway.ts +766 -223
  4. package/src/config/agent-defaults.ts +189 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/constants.ts +8 -2
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +299 -19
  10. package/src/config/thread-defaults.ts +40 -20
  11. package/src/create-runtime.ts +200 -449
  12. package/src/db/base.service.ts +52 -28
  13. package/src/db/cursor-pagination.ts +71 -30
  14. package/src/db/memory-query-builder.ts +2 -1
  15. package/src/db/memory-store.helpers.ts +4 -7
  16. package/src/db/memory-store.ts +868 -601
  17. package/src/db/memory.ts +396 -280
  18. package/src/db/record-id.ts +32 -10
  19. package/src/db/schema-fingerprint.ts +30 -12
  20. package/src/db/service-normalization.ts +288 -0
  21. package/src/db/service.ts +912 -779
  22. package/src/db/startup.ts +153 -68
  23. package/src/db/transaction-conflict.ts +15 -0
  24. package/src/effect/awaitable-effect.ts +96 -0
  25. package/src/effect/errors.ts +121 -0
  26. package/src/effect/helpers.ts +123 -0
  27. package/src/effect/index.ts +24 -0
  28. package/src/effect/layers.ts +238 -0
  29. package/src/effect/runtime-ref.ts +25 -0
  30. package/src/effect/runtime.ts +46 -0
  31. package/src/effect/services.ts +61 -0
  32. package/src/effect/zod.ts +43 -0
  33. package/src/embeddings/provider.ts +128 -83
  34. package/src/index.ts +48 -1
  35. package/src/openrouter/direct-provider.ts +11 -35
  36. package/src/queues/autonomous-job.queue.ts +117 -73
  37. package/src/queues/context-compaction.queue.ts +50 -17
  38. package/src/queues/delayed-node-promotion.queue.ts +46 -17
  39. package/src/queues/document-processor.queue.ts +52 -77
  40. package/src/queues/memory-consolidation.queue.ts +47 -32
  41. package/src/queues/organization-learning.queue.ts +26 -4
  42. package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
  43. package/src/queues/plan-scheduler.queue.ts +97 -33
  44. package/src/queues/post-chat-memory.queue.ts +56 -26
  45. package/src/queues/queue-factory.ts +227 -59
  46. package/src/queues/standalone-worker.ts +39 -0
  47. package/src/queues/title-generation.queue.ts +45 -11
  48. package/src/redis/connection.ts +182 -113
  49. package/src/redis/index.ts +6 -8
  50. package/src/redis/org-memory-lock.ts +60 -27
  51. package/src/redis/redis-lease-lock.ts +200 -121
  52. package/src/redis/runtime-connection.ts +20 -0
  53. package/src/redis/stream-context.ts +92 -46
  54. package/src/runtime/agent-identity-overrides.ts +2 -2
  55. package/src/runtime/agent-runtime-policy.ts +5 -2
  56. package/src/runtime/agent-stream-helpers.ts +24 -9
  57. package/src/runtime/chat-run-orchestration.ts +102 -19
  58. package/src/runtime/chat-run-registry.ts +36 -2
  59. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  60. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
  61. package/src/runtime/domain-layer.ts +192 -0
  62. package/src/runtime/execution-plan-visibility.ts +2 -2
  63. package/src/runtime/execution-plan.ts +42 -15
  64. package/src/runtime/graph-designer.ts +16 -4
  65. package/src/runtime/helper-model.ts +139 -48
  66. package/src/runtime/index.ts +7 -8
  67. package/src/runtime/indexed-repositories-policy.ts +3 -3
  68. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
  69. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  70. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
  71. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  72. package/src/runtime/memory/memory-scope.ts +53 -0
  73. package/src/runtime/plugin-resolution.ts +124 -25
  74. package/src/runtime/plugin-types.ts +9 -1
  75. package/src/runtime/post-turn-side-effects.ts +177 -130
  76. package/src/runtime/retrieval-adapters.ts +40 -6
  77. package/src/runtime/runtime-accessors.ts +92 -0
  78. package/src/runtime/runtime-config.ts +150 -61
  79. package/src/runtime/runtime-extensions.ts +23 -25
  80. package/src/runtime/runtime-lifecycle.ts +124 -0
  81. package/src/runtime/runtime-services.ts +386 -0
  82. package/src/runtime/runtime-token.ts +47 -0
  83. package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
  84. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
  85. package/src/runtime/social-chat/social-chat.ts +630 -0
  86. package/src/runtime/specialist-runner.ts +36 -10
  87. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
  88. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  89. package/src/runtime/thread-chat-helpers.ts +2 -2
  90. package/src/runtime/thread-plan-turn.ts +2 -1
  91. package/src/runtime/thread-turn-context.ts +183 -111
  92. package/src/runtime/turn-lifecycle.ts +93 -27
  93. package/src/services/agent-activity.service.ts +287 -203
  94. package/src/services/agent-executor.service.ts +253 -149
  95. package/src/services/artifact.service.ts +231 -149
  96. package/src/services/attachment.service.ts +171 -115
  97. package/src/services/autonomous-job.service.ts +890 -491
  98. package/src/services/background-work.service.ts +54 -0
  99. package/src/services/chat-run-registry.service.ts +13 -1
  100. package/src/services/context-compaction.service.ts +136 -86
  101. package/src/services/document-chunk.service.ts +151 -88
  102. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  103. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  104. package/src/services/execution-plan/execution-plan-graph.ts +278 -0
  105. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  106. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  107. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  108. package/src/services/feedback-loop.service.ts +132 -76
  109. package/src/services/global-orchestrator.service.ts +101 -168
  110. package/src/services/graph-full-routing.ts +193 -0
  111. package/src/services/index.ts +19 -21
  112. package/src/services/institutional-memory.service.ts +213 -125
  113. package/src/services/learned-skill.service.ts +368 -260
  114. package/src/services/memory/memory-conversation.ts +95 -0
  115. package/src/services/memory/memory-errors.ts +27 -0
  116. package/src/services/memory/memory-org-memory.ts +50 -0
  117. package/src/services/memory/memory-preseeded.ts +86 -0
  118. package/src/services/memory/memory-rerank.ts +297 -0
  119. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
  120. package/src/services/memory/memory.service.ts +674 -0
  121. package/src/services/memory/rerank.service.ts +201 -0
  122. package/src/services/monitoring-window.service.ts +92 -70
  123. package/src/services/mutating-approval.service.ts +62 -53
  124. package/src/services/node-workspace.service.ts +141 -98
  125. package/src/services/notification.service.ts +29 -16
  126. package/src/services/organization-member.service.ts +120 -66
  127. package/src/services/organization.service.ts +153 -77
  128. package/src/services/ownership-dispatcher.service.ts +456 -263
  129. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  130. package/src/services/plan/plan-agent-query.service.ts +322 -0
  131. package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
  132. package/src/services/plan/plan-artifact.service.ts +60 -0
  133. package/src/services/plan/plan-builder.service.ts +76 -0
  134. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  135. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  136. package/src/services/plan/plan-completion-side-effects.ts +169 -0
  137. package/src/services/plan/plan-coordination.service.ts +181 -0
  138. package/src/services/plan/plan-cycle.service.ts +405 -0
  139. package/src/services/plan/plan-deadline.service.ts +533 -0
  140. package/src/services/plan/plan-event-delivery.service.ts +266 -0
  141. package/src/services/plan/plan-executor-context.ts +35 -0
  142. package/src/services/plan/plan-executor-graph.ts +522 -0
  143. package/src/services/plan/plan-executor-helpers.ts +307 -0
  144. package/src/services/plan/plan-executor-persistence.ts +209 -0
  145. package/src/services/plan/plan-executor.service.ts +1737 -0
  146. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  147. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  148. package/src/services/plan/plan-run-serialization.ts +15 -0
  149. package/src/services/plan/plan-run.service.ts +637 -0
  150. package/src/services/plan/plan-scheduler.service.ts +379 -0
  151. package/src/services/plan/plan-template.service.ts +224 -0
  152. package/src/services/plan/plan-transaction-events.ts +36 -0
  153. package/src/services/plan/plan-validator.service.ts +907 -0
  154. package/src/services/plan/plan-workspace.service.ts +131 -0
  155. package/src/services/plugin-executor.service.ts +102 -68
  156. package/src/services/quality-metrics.service.ts +112 -94
  157. package/src/services/queue-job.service.ts +288 -231
  158. package/src/services/recent-activity-title.service.ts +73 -36
  159. package/src/services/recent-activity.service.ts +274 -259
  160. package/src/services/skill-resolver.service.ts +38 -12
  161. package/src/services/social-chat-history.service.ts +190 -122
  162. package/src/services/system-executor.service.ts +96 -61
  163. package/src/services/thread/thread-active-run.ts +203 -0
  164. package/src/services/thread/thread-bootstrap.ts +385 -0
  165. package/src/services/thread/thread-listing.ts +199 -0
  166. package/src/services/thread/thread-memory-block.ts +130 -0
  167. package/src/services/thread/thread-message.service.ts +379 -0
  168. package/src/services/thread/thread-record-store.ts +155 -0
  169. package/src/services/thread/thread-title.service.ts +74 -0
  170. package/src/services/thread/thread-turn-execution.ts +280 -0
  171. package/src/services/thread/thread-turn-message-context.ts +73 -0
  172. package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
  173. package/src/services/thread/thread-turn-streaming.ts +403 -0
  174. package/src/services/thread/thread-turn-tracing.ts +35 -0
  175. package/src/services/thread/thread-turn.ts +376 -0
  176. package/src/services/thread/thread.service.ts +344 -0
  177. package/src/services/user.service.ts +82 -32
  178. package/src/services/write-intent-validator.service.ts +63 -51
  179. package/src/storage/attachment-parser.ts +69 -27
  180. package/src/storage/attachment-storage.service.ts +334 -275
  181. package/src/storage/generated-document-storage.service.ts +66 -34
  182. package/src/system-agents/agent-result.ts +3 -1
  183. package/src/system-agents/context-compaction.agent.ts +3 -3
  184. package/src/system-agents/delegated-agent-factory.ts +159 -90
  185. package/src/system-agents/helper-agent-options.ts +1 -1
  186. package/src/system-agents/memory-reranker.agent.ts +3 -3
  187. package/src/system-agents/memory.agent.ts +3 -3
  188. package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
  189. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
  190. package/src/system-agents/skill-extractor.agent.ts +3 -3
  191. package/src/system-agents/skill-manager.agent.ts +3 -3
  192. package/src/system-agents/thread-router.agent.ts +157 -113
  193. package/src/system-agents/title-generator.agent.ts +3 -3
  194. package/src/tools/execution-plan.tool.ts +241 -171
  195. package/src/tools/fetch-webpage.tool.ts +29 -18
  196. package/src/tools/firecrawl-client.ts +26 -6
  197. package/src/tools/index.ts +1 -0
  198. package/src/tools/memory-block.tool.ts +14 -6
  199. package/src/tools/plan-approval.tool.ts +57 -47
  200. package/src/tools/read-file-parts.tool.ts +44 -33
  201. package/src/tools/remember-memory.tool.ts +65 -45
  202. package/src/tools/search-web.tool.ts +33 -22
  203. package/src/tools/search.tool.ts +41 -29
  204. package/src/tools/team-think.tool.ts +125 -84
  205. package/src/tools/user-questions.tool.ts +4 -3
  206. package/src/tools/web-tool-shared.ts +6 -0
  207. package/src/utils/async.ts +25 -22
  208. package/src/utils/crypto.ts +21 -0
  209. package/src/utils/date-time.ts +40 -1
  210. package/src/utils/errors.ts +111 -20
  211. package/src/utils/hono-error-handler.ts +24 -39
  212. package/src/utils/index.ts +2 -1
  213. package/src/utils/null-proto-record.ts +41 -0
  214. package/src/utils/sse-keepalive.ts +124 -21
  215. package/src/workers/bootstrap.ts +164 -52
  216. package/src/workers/memory-consolidation.worker.ts +325 -237
  217. package/src/workers/organization-learning.worker.ts +50 -16
  218. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  219. package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
  220. package/src/workers/skill-extraction.runner.ts +176 -93
  221. package/src/workers/utils/file-section-chunker.ts +8 -10
  222. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  223. package/src/workers/utils/repomix-file-sections.ts +2 -2
  224. package/src/workers/utils/thread-message-query.ts +97 -38
  225. package/src/workers/worker-utils.ts +74 -31
  226. package/src/config/debug-logger.ts +0 -47
  227. package/src/config/search.ts +0 -3
  228. package/src/redis/connection-accessor.ts +0 -26
  229. package/src/runtime/agent-types.ts +0 -1
  230. package/src/runtime/context-compaction-runtime.ts +0 -87
  231. package/src/runtime/memory-scope.ts +0 -43
  232. package/src/runtime/social-chat-agent-runner.ts +0 -118
  233. package/src/runtime/social-chat.ts +0 -516
  234. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  235. package/src/services/adaptive-playbook.service.ts +0 -152
  236. package/src/services/artifact-provenance.service.ts +0 -172
  237. package/src/services/chat-attachments.service.ts +0 -17
  238. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  239. package/src/services/execution-plan.service.ts +0 -1118
  240. package/src/services/memory.service.ts +0 -914
  241. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  242. package/src/services/plan-agent-query.service.ts +0 -267
  243. package/src/services/plan-artifact.service.ts +0 -50
  244. package/src/services/plan-builder.service.ts +0 -67
  245. package/src/services/plan-checkpoint.service.ts +0 -81
  246. package/src/services/plan-completion-side-effects.ts +0 -80
  247. package/src/services/plan-coordination.service.ts +0 -157
  248. package/src/services/plan-cycle.service.ts +0 -284
  249. package/src/services/plan-deadline.service.ts +0 -430
  250. package/src/services/plan-event-delivery.service.ts +0 -166
  251. package/src/services/plan-executor.service.ts +0 -1950
  252. package/src/services/plan-run.service.ts +0 -515
  253. package/src/services/plan-scheduler.service.ts +0 -240
  254. package/src/services/plan-template.service.ts +0 -177
  255. package/src/services/plan-validator.service.ts +0 -818
  256. package/src/services/plan-workspace.service.ts +0 -83
  257. package/src/services/rerank.service.ts +0 -156
  258. package/src/services/thread-message.service.ts +0 -275
  259. package/src/services/thread-plan-registry.service.ts +0 -22
  260. package/src/services/thread-title.service.ts +0 -39
  261. package/src/services/thread-turn-preparation.service.ts +0 -1147
  262. package/src/services/thread-turn.ts +0 -172
  263. package/src/services/thread.service.ts +0 -869
  264. package/src/utils/env.ts +0 -8
  265. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  266. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  267. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  268. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  269. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  270. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  271. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  272. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -1,28 +1,25 @@
1
1
  import { recordIdStringSchema } from '@lota-sdk/shared'
2
+ import { Cache, Context, Schema, Duration, Effect, Layer } from 'effect'
2
3
  import { BoundQuery } from 'surrealdb'
3
4
  import { z } from 'zod'
4
5
 
5
6
  import { renderLearnedSkillInstructions } from '../ai/definitions'
6
- import { lotaDebugLogger } from '../config/debug-logger'
7
7
  import { serverLogger } from '../config/logger'
8
8
  import { ensureRecordId, recordIdToString } from '../db/record-id'
9
- import { databaseService } from '../db/service'
9
+ import type { SurrealDBService } from '../db/service'
10
10
  import { TABLES } from '../db/tables'
11
- import { getDefaultEmbeddings } from '../embeddings/provider'
12
- import { getRedisConnection } from '../redis'
13
-
14
- const embeddings = getDefaultEmbeddings()
11
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
12
+ import { DatabaseServiceTag, RuntimeConfigServiceTag } from '../effect/services'
13
+ import { ProviderEmbeddings } from '../embeddings/provider'
14
+ import { sha256HexFromParts } from '../utils/crypto'
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
18
20
 
19
21
  const ACTIVE_SKILL_FILTER = "AND status IN ['learned', 'verified', 'promoted'] AND archivedAt IS NONE"
20
22
  const SKILL_EXISTS_TTL_SECONDS = 120
21
- const SKILL_EXISTS_KEY_PREFIX = 'skill-exists'
22
-
23
- function skillExistsKey(orgId: string, agentId: string): string {
24
- return `${SKILL_EXISTS_KEY_PREFIX}:${orgId}:${agentId}`
25
- }
26
23
 
27
24
  const LearnedSkillRowSchema = z.object({
28
25
  id: recordIdStringSchema,
@@ -59,7 +56,26 @@ const SearchResultRowSchema = z.object({
59
56
  similarity: z.number(),
60
57
  })
61
58
 
62
- type SearchResultRow = z.infer<typeof SearchResultRowSchema>
59
+ class LearnedSkillServiceError extends Schema.TaggedErrorClass<LearnedSkillServiceError>()('LearnedSkillServiceError', {
60
+ message: Schema.String,
61
+ cause: Schema.Defect,
62
+ }) {}
63
+
64
+ class LearnedSkillNotFoundError extends Schema.TaggedErrorClass<LearnedSkillNotFoundError>()(
65
+ 'LearnedSkillNotFoundError',
66
+ { message: Schema.String },
67
+ ) {}
68
+
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)
78
+ }
63
79
 
64
80
  interface CreateLearnedSkillInput {
65
81
  name: string
@@ -98,124 +114,102 @@ interface RetrieveForTurnParams {
98
114
  minConfidence: number
99
115
  }
100
116
 
101
- class LearnedSkillService {
102
- async create(input: CreateLearnedSkillInput): Promise<LearnedSkillRow> {
103
- const orgRef = ensureRecordId(input.organizationId, TABLES.ORGANIZATION)
104
-
105
- const data: Record<string, unknown> = {
106
- name: input.name,
107
- description: input.description,
108
- instructions: input.instructions,
109
- triggers: input.triggers,
110
- tags: input.tags,
111
- examples: input.examples,
112
- sourceType: input.sourceType,
113
- organizationId: orgRef,
114
- agentId: input.agentId,
115
- confidence: input.confidence,
116
- embedding: input.embedding,
117
- hash: input.hash,
118
- }
119
-
120
- const result = await databaseService.create(TABLES.LEARNED_SKILL, data, LearnedSkillRowSchema)
121
- await this.invalidateSkillExistsCache(input.organizationId, input.agentId)
122
- return result
123
- }
124
-
125
- async update(skillId: string, input: UpdateLearnedSkillInput): Promise<LearnedSkillRow> {
126
- const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
127
- const data: Record<string, unknown> = {}
128
-
129
- if (input.name !== undefined) data.name = input.name
130
- if (input.description !== undefined) data.description = input.description
131
- if (input.instructions !== undefined) data.instructions = input.instructions
132
- if (input.triggers !== undefined) data.triggers = input.triggers
133
- if (input.tags !== undefined) data.tags = input.tags
134
- if (input.examples !== undefined) data.examples = input.examples
135
- if (input.confidence !== undefined) data.confidence = input.confidence
136
- if (input.version !== undefined) data.version = input.version
137
- if (input.embedding !== undefined) data.embedding = input.embedding
138
- if (input.hash !== undefined) data.hash = input.hash
139
- if (input.supersedes !== undefined) data.supersedes = ensureRecordId(input.supersedes, TABLES.LEARNED_SKILL)
140
-
141
- const updated = await databaseService.update(TABLES.LEARNED_SKILL, ref, data, LearnedSkillRowSchema)
142
- if (!updated) throw new Error(`Learned skill ${skillId} not found`)
143
- return updated
144
- }
145
-
146
- async archive(skillId: string): Promise<void> {
147
- const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
148
- const skill = await this.getById(skillId)
149
- await databaseService.update(
150
- TABLES.LEARNED_SKILL,
151
- ref,
152
- { status: 'archived', archivedAt: new Date() },
153
- LearnedSkillRowSchema,
154
- )
155
- if (skill) {
156
- await this.invalidateSkillExistsCache(recordIdToString(skill.organizationId), skill.agentId ?? null)
157
- }
158
- }
159
-
160
- async getById(skillId: string): Promise<LearnedSkillRow | null> {
161
- return databaseService.findOne(
162
- TABLES.LEARNED_SKILL,
163
- { id: ensureRecordId(skillId, TABLES.LEARNED_SKILL) },
164
- LearnedSkillRowSchema,
165
- )
166
- }
167
-
168
- private async hasSkillsForAgent(orgId: string, agentId: string): Promise<boolean> {
169
- const redis = getRedisConnection()
170
- const key = skillExistsKey(orgId, agentId)
171
-
172
- const cached = await redis.get(key)
173
- if (cached !== null) return cached === '1'
174
-
175
- const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
176
- const rows = await databaseService.query<{ id: unknown }>(
177
- new BoundQuery(
178
- `SELECT id FROM ${TABLES.LEARNED_SKILL}
179
- WHERE organizationId = $orgRef
180
- ${ACTIVE_SKILL_FILTER}
181
- AND (agentId IS NONE OR agentId = $agentId)
182
- LIMIT 1`,
183
- { orgRef, agentId },
184
- ),
117
+ export function makeLearnedSkillService(
118
+ db: SurrealDBService,
119
+ options: { embeddingModel: string; openRouterApiKey?: string },
120
+ skillExistsCache: Cache.Cache<string, boolean, LearnedSkillServiceError>,
121
+ background: Context.Service.Shape<typeof BackgroundWorkService>,
122
+ ) {
123
+ const embeddings = new ProviderEmbeddings({
124
+ modelId: options.embeddingModel,
125
+ openRouterApiKey: options.openRouterApiKey,
126
+ })
127
+
128
+ const hasSkillsForAgent = (orgId: string, agentId: string) => Cache.get(skillExistsCache, `${orgId}:${agentId}`)
129
+
130
+ const invalidateSkillExistsCache = (orgId: string, agentId: string | null) =>
131
+ Effect.gen(function* () {
132
+ const keys = yield* Cache.keys(skillExistsCache)
133
+ const orgPrefix = `${orgId}:`
134
+ const matchingKeys = [...keys].filter((key) => key.startsWith(orgPrefix))
135
+ if (matchingKeys.length > 0) {
136
+ yield* Effect.forEach(matchingKeys, (key) => Cache.invalidate(skillExistsCache, key))
137
+ }
138
+ if (agentId) {
139
+ yield* Cache.set(skillExistsCache, `${orgId}:${agentId}`, true)
140
+ }
141
+ })
142
+
143
+ const create = (input: CreateLearnedSkillInput) =>
144
+ Effect.gen(function* () {
145
+ const orgRef = ensureRecordId(input.organizationId, TABLES.ORGANIZATION)
146
+
147
+ const data: Record<string, unknown> = {
148
+ name: input.name,
149
+ description: input.description,
150
+ instructions: input.instructions,
151
+ triggers: input.triggers,
152
+ tags: input.tags,
153
+ examples: input.examples,
154
+ sourceType: input.sourceType,
155
+ organizationId: orgRef,
156
+ agentId: input.agentId,
157
+ confidence: input.confidence,
158
+ embedding: input.embedding,
159
+ hash: input.hash,
160
+ }
161
+
162
+ const result = yield* tryLearnedSkillPromise('Failed to create learned skill.', () =>
163
+ db.create(TABLES.LEARNED_SKILL, data, LearnedSkillRowSchema),
164
+ )
165
+ yield* invalidateSkillExistsCache(input.organizationId, input.agentId)
166
+ return result
167
+ })
168
+
169
+ const update = (skillId: string, input: UpdateLearnedSkillInput) =>
170
+ Effect.gen(function* () {
171
+ const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
172
+ const data: Record<string, unknown> = {}
173
+
174
+ if (input.name !== undefined) data.name = input.name
175
+ if (input.description !== undefined) data.description = input.description
176
+ if (input.instructions !== undefined) data.instructions = input.instructions
177
+ if (input.triggers !== undefined) data.triggers = input.triggers
178
+ if (input.tags !== undefined) data.tags = input.tags
179
+ if (input.examples !== undefined) data.examples = input.examples
180
+ if (input.confidence !== undefined) data.confidence = input.confidence
181
+ if (input.version !== undefined) data.version = input.version
182
+ if (input.embedding !== undefined) data.embedding = input.embedding
183
+ if (input.hash !== undefined) data.hash = input.hash
184
+ if (input.supersedes !== undefined) data.supersedes = ensureRecordId(input.supersedes, TABLES.LEARNED_SKILL)
185
+
186
+ const updated = yield* tryLearnedSkillPromise(`Failed to update learned skill ${skillId}.`, () =>
187
+ db.update(TABLES.LEARNED_SKILL, ref, data, LearnedSkillRowSchema),
188
+ )
189
+ if (!updated) {
190
+ return yield* new LearnedSkillNotFoundError({ message: `Learned skill ${skillId} not found` })
191
+ }
192
+ return updated
193
+ })
194
+
195
+ const archive = (skillId: string) =>
196
+ Effect.gen(function* () {
197
+ const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
198
+ 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
+ )
202
+ if (skill) {
203
+ yield* invalidateSkillExistsCache(recordIdToString(skill.organizationId), skill.agentId ?? null)
204
+ }
205
+ })
206
+
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),
185
210
  )
186
211
 
187
- const exists = rows.length > 0
188
- await redis.set(key, exists ? '1' : '0', 'EX', SKILL_EXISTS_TTL_SECONDS)
189
- return exists
190
- }
191
-
192
- private async invalidateSkillExistsCache(orgId: string, agentId: string | null): Promise<void> {
193
- const redis = getRedisConnection()
194
- const pattern = `${SKILL_EXISTS_KEY_PREFIX}:${orgId}:*`
195
- const keys = await redis.keys(pattern)
196
- if (keys.length > 0) {
197
- await redis.del(...keys)
198
- }
199
- // Also set the specific key if we know a skill was just created
200
- if (agentId) {
201
- await redis.set(skillExistsKey(orgId, agentId), '1', 'EX', SKILL_EXISTS_TTL_SECONDS)
202
- }
203
- }
204
-
205
- async searchForTurn(params: RetrieveForTurnParams): Promise<SearchResultRow[]> {
206
- const timer = lotaDebugLogger.timer('learned-skills')
207
-
208
- const hasSkills = await this.hasSkillsForAgent(params.orgId, params.agentId)
209
- if (!hasSkills) {
210
- lotaDebugLogger.step('learned-skills: skipped — no skills for org+agent')
211
- return []
212
- }
213
- timer.step('has-skills-check')
214
-
215
- const queryEmbedding = await embeddings.embedQuery(params.query)
216
- timer.step('embed-query')
217
- if (queryEmbedding.length === 0) return []
218
-
212
+ const searchForTurn = Effect.fn('LearnedSkillService.searchForTurn')(function* (params: RetrieveForTurnParams) {
219
213
  const orgRef = ensureRecordId(params.orgId, TABLES.ORGANIZATION)
220
214
  const sql = `
221
215
  SELECT
@@ -234,153 +228,267 @@ class LearnedSkillService {
234
228
  ORDER BY similarity DESC
235
229
  `
236
230
 
237
- const rows = await databaseService.query<unknown>(
238
- new BoundQuery(sql, {
239
- organizationId: orgRef,
240
- embedding: queryEmbedding,
241
- agentId: params.agentId,
242
- minConfidence: params.minConfidence,
243
- }),
231
+ const hasSkills = yield* hasSkillsForAgent(params.orgId, params.agentId).pipe(
232
+ Effect.withSpan('LearnedSkillService.checkSkillsAvailability'),
244
233
  )
245
- timer.step('knn-query')
234
+ yield* Effect.annotateCurrentSpan({
235
+ orgId: params.orgId,
236
+ agentId: params.agentId,
237
+ limit: params.limit,
238
+ minConfidence: params.minConfidence,
239
+ skillsAvailable: hasSkills,
240
+ })
241
+ if (!hasSkills) {
242
+ return []
243
+ }
246
244
 
247
- return rows.map((row) => SearchResultRowSchema.parse(row)).filter((row) => row.similarity >= 0.3)
248
- }
245
+ const queryEmbedding = yield* tryLearnedSkillPromise('Failed to embed learned skill query.', () =>
246
+ embeddings.embedQuery(params.query),
247
+ ).pipe(Effect.withSpan('LearnedSkillService.embedQuery'))
248
+ yield* Effect.annotateCurrentSpan('embeddingLength', queryEmbedding.length)
249
+ if (queryEmbedding.length === 0) return []
249
250
 
250
- async retrieveForTurn(params: RetrieveForTurnParams): Promise<string | undefined> {
251
- const results = await this.searchForTurn(params)
251
+ const rows = yield* tryLearnedSkillPromise('Failed to query learned skills.', () =>
252
+ db.query<unknown>(
253
+ new BoundQuery(sql, {
254
+ organizationId: orgRef,
255
+ embedding: queryEmbedding,
256
+ agentId: params.agentId,
257
+ minConfidence: params.minConfidence,
258
+ }),
259
+ ),
260
+ ).pipe(Effect.withSpan('LearnedSkillService.queryNearestSkills'))
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)
267
+ })
268
+
269
+ const retrieveForTurn = Effect.fn('LearnedSkillService.retrieveForTurn')(function* (params: RetrieveForTurnParams) {
270
+ const results = yield* searchForTurn(params)
252
271
  if (results.length === 0) return undefined
253
272
 
254
- for (const result of results) {
255
- void this.recordUsage(result.id).catch((error) => {
256
- serverLogger.warn`Failed to record learned skill usage for ${result.id}: ${error}`
257
- })
258
- }
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
+ }),
282
+ ),
283
+ ),
284
+ { discard: true },
285
+ ),
286
+ 'learned-skill.recordUsage',
287
+ )
259
288
 
260
289
  const section = renderLearnedSkillInstructions(
261
290
  results.map((row) => ({ name: row.name, instructions: row.instructions })),
262
291
  )
263
292
  return section || undefined
264
- }
265
-
266
- async recordUsage(skillId: string): Promise<void> {
267
- const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
268
- await databaseService.query<unknown>(
269
- new BoundQuery(`UPDATE ${TABLES.LEARNED_SKILL} SET usageCount += 1, lastUsedAt = time::now() WHERE id = $id`, {
270
- id: ref,
271
- }),
272
- )
273
- }
274
-
275
- async recordSuccess(skillId: string): Promise<void> {
276
- const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
277
- await databaseService.query<unknown>(
278
- new BoundQuery(`UPDATE ${TABLES.LEARNED_SKILL} SET successCount += 1 WHERE id = $id`, { id: ref }),
279
- )
280
- }
281
-
282
- async promoteIfEligible(skillId: string): Promise<boolean> {
283
- const skill = await this.getById(skillId)
284
- if (!skill) return false
285
- if (skill.status !== 'learned') return false
286
- if (skill.usageCount < PROMOTION_MIN_USES) return false
287
-
288
- const successRate = skill.successCount / skill.usageCount
289
- if (successRate < PROMOTION_MIN_SUCCESS_RATE) return false
290
-
291
- const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
292
- await databaseService.update(
293
- TABLES.LEARNED_SKILL,
294
- ref,
295
- { status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0) },
296
- LearnedSkillRowSchema,
297
- )
298
- return true
299
- }
300
-
301
- async findMostSimilar(orgId: string, description: string): Promise<LearnedSkillRow | null> {
302
- const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
303
- const descEmbedding = await embeddings.embedQuery(description)
304
- if (descEmbedding.length === 0) return null
305
-
306
- const sql = `
307
- SELECT *,
308
- type::string(id) AS id,
309
- type::string(organizationId) AS organizationId,
310
- vector::similarity::cosine(embedding, $embedding) AS similarity
311
- FROM ${TABLES.LEARNED_SKILL}
312
- WHERE organizationId = $organizationId
313
- ${ACTIVE_SKILL_FILTER}
314
- AND embedding <|3|> $embedding
315
- ORDER BY similarity DESC
316
- LIMIT 1
317
- `
318
-
319
- const rows = await databaseService.query<unknown>(
320
- new BoundQuery(sql, { organizationId: orgRef, embedding: descEmbedding }),
321
- )
322
-
323
- if (rows.length === 0) return null
324
- return LearnedSkillRowSchema.parse(rows[0])
325
- }
326
-
327
- async listForOrg(orgId: string): Promise<LearnedSkillRow[]> {
328
- const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
329
- return databaseService.queryMany(
330
- new BoundQuery(
331
- `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
332
- FROM ${TABLES.LEARNED_SKILL}
333
- WHERE organizationId = $organizationId
334
- ${ACTIVE_SKILL_FILTER}
335
- ORDER BY createdAt DESC`,
336
- { organizationId: orgRef },
293
+ })
294
+
295
+ const recordUsage = (skillId: string) =>
296
+ Effect.gen(function* () {
297
+ const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
298
+ yield* tryLearnedSkillPromise(`Failed to record usage for learned skill ${skillId}.`, () =>
299
+ db.query<unknown>(
300
+ new BoundQuery(
301
+ `UPDATE ${TABLES.LEARNED_SKILL} SET usageCount += 1, lastUsedAt = time::now() WHERE id = $id`,
302
+ { id: ref },
303
+ ),
304
+ ),
305
+ )
306
+ })
307
+
308
+ const recordSuccess = (skillId: string) =>
309
+ Effect.gen(function* () {
310
+ const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
311
+ yield* tryLearnedSkillPromise(`Failed to record success for learned skill ${skillId}.`, () =>
312
+ db.query<unknown>(
313
+ new BoundQuery(`UPDATE ${TABLES.LEARNED_SKILL} SET successCount += 1 WHERE id = $id`, { id: ref }),
314
+ ),
315
+ )
316
+ })
317
+
318
+ const promoteIfEligible = (skillId: string) =>
319
+ Effect.gen(function* () {
320
+ const skill = yield* getById(skillId)
321
+ if (!skill) return false
322
+ if (skill.status !== 'learned') return false
323
+ if (skill.usageCount < PROMOTION_MIN_USES) return false
324
+
325
+ const successRate = skill.successCount / skill.usageCount
326
+ if (successRate < PROMOTION_MIN_SUCCESS_RATE) return false
327
+
328
+ const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
329
+ yield* tryLearnedSkillPromise(`Failed to promote learned skill ${skillId}.`, () =>
330
+ db.update(
331
+ TABLES.LEARNED_SKILL,
332
+ ref,
333
+ { status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0) },
334
+ LearnedSkillRowSchema,
335
+ ),
336
+ )
337
+ return true
338
+ })
339
+
340
+ const findMostSimilar = (orgId: string, description: string) =>
341
+ Effect.gen(function* () {
342
+ const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
343
+ const sql = `
344
+ SELECT *,
345
+ type::string(id) AS id,
346
+ type::string(organizationId) AS organizationId,
347
+ vector::similarity::cosine(embedding, $embedding) AS similarity
348
+ FROM ${TABLES.LEARNED_SKILL}
349
+ WHERE organizationId = $organizationId
350
+ ${ACTIVE_SKILL_FILTER}
351
+ AND embedding <|3|> $embedding
352
+ ORDER BY similarity DESC
353
+ LIMIT 1
354
+ `
355
+
356
+ const descEmbedding = yield* tryLearnedSkillPromise('Failed to embed learned skill description.', () =>
357
+ embeddings.embedQuery(description),
358
+ )
359
+ if (descEmbedding.length === 0) return null
360
+
361
+ const rows = yield* tryLearnedSkillPromise('Failed to query most similar learned skill.', () =>
362
+ db.query<unknown>(new BoundQuery(sql, { organizationId: orgRef, embedding: descEmbedding })),
363
+ )
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
+ })
370
+ })
371
+
372
+ const listForOrg = (orgId: string) =>
373
+ tryLearnedSkillPromise(`Failed to list learned skills for organization ${orgId}.`, () =>
374
+ db.queryMany(
375
+ new BoundQuery(
376
+ `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
377
+ FROM ${TABLES.LEARNED_SKILL}
378
+ WHERE organizationId = $organizationId
379
+ ${ACTIVE_SKILL_FILTER}
380
+ ORDER BY createdAt DESC`,
381
+ { organizationId: ensureRecordId(orgId, TABLES.ORGANIZATION) },
382
+ ),
383
+ LearnedSkillRowSchema,
337
384
  ),
338
- LearnedSkillRowSchema,
339
385
  )
340
- }
341
386
 
342
- generateHash(description: string, instructions: string): string {
343
- const hasher = new Bun.CryptoHasher('sha256')
344
- hasher.update(description.trim().toLowerCase())
345
- hasher.update(instructions.trim().toLowerCase())
346
- return hasher.digest('hex')
387
+ const findByNameOrTag = (orgId: string, nameOrTag: string) =>
388
+ Effect.gen(function* () {
389
+ const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
390
+ 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(
393
+ new BoundQuery(
394
+ `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
395
+ FROM ${TABLES.LEARNED_SKILL}
396
+ WHERE organizationId = $organizationId
397
+ ${ACTIVE_SKILL_FILTER}
398
+ AND (string::lowercase(name) = $nameRef OR $nameRef IN tags)
399
+ ORDER BY confidence DESC
400
+ LIMIT 1`,
401
+ { organizationId: orgRef, nameRef: normalizedRef },
402
+ ),
403
+ LearnedSkillRowSchema,
404
+ ),
405
+ )
406
+ return rows[0] ?? null
407
+ })
408
+
409
+ const findByHash = (orgId: string, hash: string): Effect.Effect<LearnedSkillRow | null, LearnedSkillServiceError> =>
410
+ Effect.gen(function* () {
411
+ const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
412
+ const rows = yield* tryLearnedSkillPromise(`Failed to find learned skill by hash for org ${orgId}.`, () =>
413
+ db.queryMany(
414
+ new BoundQuery(
415
+ `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
416
+ FROM ${TABLES.LEARNED_SKILL}
417
+ WHERE organizationId = $organizationId
418
+ AND hash = $hash
419
+ ${ACTIVE_SKILL_FILTER}
420
+ LIMIT 1`,
421
+ { organizationId: orgRef, hash },
422
+ ),
423
+ LearnedSkillRowSchema,
424
+ ),
425
+ )
426
+ return rows[0] ?? null
427
+ })
428
+
429
+ function generateHash(description: string, instructions: string): string {
430
+ return sha256HexFromParts([description.trim().toLowerCase(), instructions.trim().toLowerCase()])
347
431
  }
348
432
 
349
- async findByNameOrTag(orgId: string, nameOrTag: string): Promise<LearnedSkillRow | null> {
350
- const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
351
- const normalizedRef = nameOrTag.trim().toLowerCase()
352
- const rows = await databaseService.queryMany(
353
- new BoundQuery(
354
- `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
355
- FROM ${TABLES.LEARNED_SKILL}
356
- WHERE organizationId = $organizationId
357
- ${ACTIVE_SKILL_FILTER}
358
- AND (string::lowercase(name) = $nameRef OR $nameRef IN tags)
359
- ORDER BY confidence DESC
360
- LIMIT 1`,
361
- { organizationId: orgRef, nameRef: normalizedRef },
362
- ),
363
- LearnedSkillRowSchema,
364
- )
365
- return rows[0] ?? null
366
- }
367
-
368
- async findByHash(orgId: string, hash: string): Promise<LearnedSkillRow | null> {
369
- const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
370
- const rows = await databaseService.queryMany(
371
- new BoundQuery(
372
- `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
373
- FROM ${TABLES.LEARNED_SKILL}
374
- WHERE organizationId = $organizationId
375
- AND hash = $hash
376
- ${ACTIVE_SKILL_FILTER}
377
- LIMIT 1`,
378
- { organizationId: orgRef, hash },
379
- ),
380
- LearnedSkillRowSchema,
381
- )
382
- return rows[0] ?? null
433
+ return {
434
+ create,
435
+ update,
436
+ archive,
437
+ getById,
438
+ searchForTurn,
439
+ retrieveForTurn,
440
+ recordUsage,
441
+ recordSuccess,
442
+ promoteIfEligible,
443
+ findMostSimilar,
444
+ listForOrg,
445
+ generateHash,
446
+ findByNameOrTag,
447
+ findByHash,
383
448
  }
384
449
  }
385
450
 
386
- export const learnedSkillService = new LearnedSkillService()
451
+ export class LearnedSkillServiceTag extends Context.Service<
452
+ LearnedSkillServiceTag,
453
+ ReturnType<typeof makeLearnedSkillService>
454
+ >()('@lota-sdk/core/LearnedSkillService') {}
455
+
456
+ export const LearnedSkillServiceLive = Layer.effect(
457
+ LearnedSkillServiceTag,
458
+ Effect.gen(function* () {
459
+ const db = yield* DatabaseServiceTag
460
+ const runtimeConfig = yield* RuntimeConfigServiceTag
461
+ const background = yield* BackgroundWorkService
462
+ const skillExistsCache = yield* Cache.make({
463
+ lookup: (key: string) =>
464
+ Effect.gen(function* () {
465
+ const [orgId, agentId] = key.split(':', 2) as [string, string]
466
+ const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
467
+ const rows = yield* tryLearnedSkillPromise('Failed to check learned skill existence cache.', () =>
468
+ db.query<{ id: unknown }>(
469
+ new BoundQuery(
470
+ `SELECT id FROM ${TABLES.LEARNED_SKILL}
471
+ WHERE organizationId = $orgRef
472
+ ${ACTIVE_SKILL_FILTER}
473
+ AND (agentId IS NONE OR agentId = $agentId)
474
+ LIMIT 1`,
475
+ { orgRef, agentId },
476
+ ),
477
+ ),
478
+ )
479
+ return rows.length > 0
480
+ }),
481
+ capacity: 256,
482
+ timeToLive: Duration.seconds(SKILL_EXISTS_TTL_SECONDS),
483
+ })
484
+ return makeLearnedSkillService(
485
+ db,
486
+ {
487
+ embeddingModel: runtimeConfig.aiGateway.embeddingModel,
488
+ openRouterApiKey: runtimeConfig.aiGateway.openRouterApiKey,
489
+ },
490
+ skillExistsCache,
491
+ background,
492
+ )
493
+ }),
494
+ )