@lota-sdk/core 0.4.7 → 0.4.9

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