@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
@@ -0,0 +1,198 @@
1
+ import { THREAD } from '@lota-sdk/shared'
2
+ import { Schema, Effect } from 'effect'
3
+ import { BoundQuery } from 'surrealdb'
4
+
5
+ import type { RecordIdRef } from '../../db/record-id'
6
+ import type { SurrealDBService } from '../../db/service'
7
+ import { TABLES } from '../../db/tables'
8
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
9
+ import { ThreadSchema } from './thread.types'
10
+ import type { NormalizedThread, ThreadRecord } from './thread.types'
11
+
12
+ function buildListThreadsQuery(options: {
13
+ includeArchived: boolean
14
+ paginate: boolean
15
+ type?: string
16
+ types?: string[]
17
+ }): string {
18
+ const clauses = [`SELECT * FROM ${TABLES.THREAD}`, 'WHERE userId = $userId', ' AND organizationId = $orgId']
19
+
20
+ if (options.types) {
21
+ clauses.push(' AND type IN $types')
22
+ } else if (options.type) {
23
+ clauses.push(' AND type = $type')
24
+ }
25
+
26
+ if (!options.includeArchived) {
27
+ clauses.push(' AND status = "active"')
28
+ }
29
+ clauses.push('ORDER BY updatedAt DESC')
30
+ if (options.paginate) {
31
+ clauses.push('LIMIT $limit START $offset')
32
+ }
33
+ return clauses.join('\n')
34
+ }
35
+
36
+ export function createThreadListingHelpers(deps: {
37
+ db: SurrealDBService
38
+ normalizeThreads(
39
+ threads: ThreadRecord[],
40
+ options?: { checkLease?: boolean },
41
+ ): Effect.Effect<NormalizedThread[], unknown>
42
+ }) {
43
+ class ThreadListingError extends Schema.TaggedErrorClass<ThreadListingError>()('ThreadListingError', {
44
+ message: Schema.String,
45
+ cause: Schema.optional(Schema.Defect),
46
+ }) {}
47
+
48
+ const effectTryPromise = makeEffectTryPromiseWithMessage(
49
+ (message, cause) => new ThreadListingError({ message, cause }),
50
+ )
51
+
52
+ function listThreads(
53
+ userId: RecordIdRef,
54
+ orgId: RecordIdRef,
55
+ options: { type?: string; types?: string[]; take?: number; page?: number; includeArchived?: boolean },
56
+ ): Effect.Effect<{ threads: NormalizedThread[]; hasMore: boolean }, ThreadListingError> {
57
+ const includeArchived = options.includeArchived ?? false
58
+ const type = options.type
59
+ const types = options.types
60
+
61
+ if (type === 'default' || type === 'thread') {
62
+ const vars: Record<string, unknown> = { userId, orgId, type }
63
+ return Effect.gen(function* () {
64
+ const threads = yield* effectTryPromise(
65
+ () =>
66
+ deps.db.queryMany(
67
+ new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: false, type }), vars),
68
+ ThreadSchema,
69
+ ),
70
+ 'Failed to list threads.',
71
+ )
72
+ const normalizedThreads = yield* effectTryPromise(
73
+ () => deps.normalizeThreads(threads, { checkLease: false }),
74
+ 'Failed to normalize listed threads.',
75
+ )
76
+ return { threads: normalizedThreads, hasMore: false }
77
+ })
78
+ }
79
+
80
+ const take = options.take ?? THREAD.DEFAULT_PAGE_LIMIT
81
+ const page = options.page ?? 1
82
+ const vars: Record<string, unknown> = { userId, orgId, limit: take + 1, offset: (page - 1) * take }
83
+
84
+ if (types) {
85
+ vars.types = types
86
+ } else if (type) {
87
+ vars.type = type
88
+ }
89
+
90
+ return Effect.gen(function* () {
91
+ const threads = yield* effectTryPromise(
92
+ () =>
93
+ deps.db.queryMany(
94
+ new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: true, type, types }), vars),
95
+ ThreadSchema,
96
+ ),
97
+ 'Failed to list paginated threads.',
98
+ )
99
+ const hasMore = threads.length > take
100
+ const sliced = hasMore ? threads.slice(0, take) : threads
101
+ const normalizedThreads = yield* effectTryPromise(
102
+ () => deps.normalizeThreads(sliced, { checkLease: false }),
103
+ 'Failed to normalize paginated threads.',
104
+ )
105
+ return { threads: normalizedThreads, hasMore }
106
+ })
107
+ }
108
+
109
+ function listOrganizationThreads(params: {
110
+ orgId: RecordIdRef
111
+ type?: string
112
+ agentId?: string
113
+ includeArchived?: boolean
114
+ }): Effect.Effect<NormalizedThread[], ThreadListingError> {
115
+ const whereClauses = ['organizationId = $orgId']
116
+ const variables: Record<string, unknown> = { orgId: params.orgId }
117
+
118
+ if (params.type) {
119
+ whereClauses.push('type = $type')
120
+ variables.type = params.type
121
+ }
122
+
123
+ if (params.agentId) {
124
+ whereClauses.push('agentId = $agentId')
125
+ variables.agentId = params.agentId
126
+ }
127
+
128
+ if (params.includeArchived !== true) {
129
+ whereClauses.push('status = "active"')
130
+ }
131
+
132
+ return Effect.gen(function* () {
133
+ const threads = yield* effectTryPromise(
134
+ () =>
135
+ deps.db.queryMany(
136
+ new BoundQuery(
137
+ `SELECT * FROM ${TABLES.THREAD}
138
+ WHERE ${whereClauses.join('\n AND ')}
139
+ ORDER BY createdAt ASC, id ASC`,
140
+ variables,
141
+ ),
142
+ ThreadSchema,
143
+ ),
144
+ 'Failed to list organization threads.',
145
+ )
146
+ return yield* effectTryPromise(
147
+ () => deps.normalizeThreads(threads, { checkLease: false }),
148
+ 'Failed to normalize organization threads.',
149
+ )
150
+ })
151
+ }
152
+
153
+ function listRecentThreads({
154
+ userId,
155
+ orgId,
156
+ excludeThreadId,
157
+ limit,
158
+ }: {
159
+ userId: RecordIdRef
160
+ orgId: RecordIdRef
161
+ excludeThreadId?: RecordIdRef
162
+ limit: number
163
+ }): Effect.Effect<NormalizedThread[], ThreadListingError> {
164
+ let excludeCondition = ''
165
+ const vars: Record<string, unknown> = { userId, orgId, limit }
166
+
167
+ if (excludeThreadId) {
168
+ excludeCondition = 'AND id != $excludeThreadId'
169
+ vars.excludeThreadId = excludeThreadId
170
+ }
171
+
172
+ return Effect.gen(function* () {
173
+ const threads = yield* effectTryPromise(
174
+ () =>
175
+ deps.db.queryMany(
176
+ new BoundQuery(
177
+ `SELECT * FROM ${TABLES.THREAD}
178
+ WHERE userId = $userId
179
+ AND organizationId = $orgId
180
+ ${excludeCondition}
181
+ AND status != "archived"
182
+ ORDER BY updatedAt DESC
183
+ LIMIT $limit`,
184
+ vars,
185
+ ),
186
+ ThreadSchema,
187
+ ),
188
+ 'Failed to list recent threads.',
189
+ )
190
+ return yield* effectTryPromise(
191
+ () => deps.normalizeThreads(threads, { checkLease: false }),
192
+ 'Failed to normalize recent threads.',
193
+ )
194
+ })
195
+ }
196
+
197
+ return { listThreads, listOrganizationThreads, listRecentThreads }
198
+ }
@@ -0,0 +1,117 @@
1
+ import { Schema, Effect } from 'effect'
2
+
3
+ import { serverLogger } from '../../config/logger'
4
+ import type { RecordIdRef } from '../../db/record-id'
5
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
6
+ import { TABLES } from '../../db/tables'
7
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
8
+ import {
9
+ appendToMemoryBlock,
10
+ compactMemoryBlockEntries,
11
+ formatPersistedMemoryBlockForPrompt,
12
+ parseMemoryBlock,
13
+ serializeMemoryBlock,
14
+ } from '../../runtime/memory/memory-block'
15
+ import { MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES, MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES } from './thread-constants'
16
+ import type { ThreadRecordStore } from './thread-record-store'
17
+ import type { ThreadRecord } from './thread.types'
18
+
19
+ type ContextCompactionServiceLike = {
20
+ compactMemoryBlock(params: { previousSummary: string; newEntriesText: string }): Effect.Effect<string, unknown>
21
+ }
22
+
23
+ class ThreadMemoryBlockError extends Schema.TaggedErrorClass<ThreadMemoryBlockError>()('ThreadMemoryBlockError', {
24
+ message: Schema.String,
25
+ cause: Schema.optional(Schema.Defect),
26
+ }) {}
27
+
28
+ const tryThreadMemoryBlockEffect = makeEffectTryPromiseWithMessage(
29
+ (message, cause) => new ThreadMemoryBlockError({ message, cause }),
30
+ )
31
+
32
+ export function formatMemoryBlockForPrompt(thread: Pick<ThreadRecord, 'memoryBlock' | 'memoryBlockSummary'>): string {
33
+ return formatPersistedMemoryBlockForPrompt({
34
+ summary: thread.memoryBlockSummary,
35
+ entries: parseMemoryBlock(thread.memoryBlock),
36
+ })
37
+ }
38
+
39
+ export function createThreadMemoryBlockHelpers(deps: {
40
+ threadStore: ThreadRecordStore
41
+ contextCompactionService: ContextCompactionServiceLike
42
+ }) {
43
+ function appendMemoryBlock(threadId: RecordIdRef, entry: string): Effect.Effect<string, ThreadMemoryBlockError> {
44
+ return Effect.gen(function* () {
45
+ const threadRef = ensureRecordId(threadId, TABLES.THREAD)
46
+ const threadIdString = recordIdToString(threadRef, TABLES.THREAD)
47
+ const thread = yield* tryThreadMemoryBlockEffect(
48
+ () => deps.threadStore.getById(threadRef),
49
+ `Failed to load thread ${threadIdString} for memory block append`,
50
+ )
51
+ const entries = parseMemoryBlock(thread.memoryBlock)
52
+
53
+ const labelMatch = entry.match(/^(\w+):\s*/i)
54
+ const role = labelMatch ? labelMatch[1].toLowerCase() : 'system'
55
+ const content = labelMatch ? entry.slice(labelMatch[0].length).trim() : entry.trim()
56
+
57
+ const updatedEntries = appendToMemoryBlock(entries, role, content)
58
+ const serialized = serializeMemoryBlock(updatedEntries)
59
+
60
+ yield* tryThreadMemoryBlockEffect(
61
+ () => deps.threadStore.update(threadRef, { memoryBlock: serialized }),
62
+ `Failed to persist memory block for thread ${threadIdString}`,
63
+ )
64
+
65
+ if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
66
+ yield* Effect.forkDetach(
67
+ compactMemoryBlock(threadRef).pipe(
68
+ Effect.catch((error: unknown) =>
69
+ Effect.sync(() => {
70
+ serverLogger.warn`Memory block compaction failed for ${threadIdString}: ${error}`
71
+ }),
72
+ ),
73
+ ),
74
+ )
75
+ }
76
+
77
+ return formatMemoryBlockForPrompt({ memoryBlock: serialized, memoryBlockSummary: thread.memoryBlockSummary })
78
+ })
79
+ }
80
+
81
+ function compactMemoryBlock(threadId: RecordIdRef): Effect.Effect<boolean, ThreadMemoryBlockError> {
82
+ return Effect.gen(function* () {
83
+ const threadRef = ensureRecordId(threadId, TABLES.THREAD)
84
+ const threadIdString = recordIdToString(threadRef, TABLES.THREAD)
85
+ const thread = yield* tryThreadMemoryBlockEffect(
86
+ () => deps.threadStore.getById(threadRef),
87
+ `Failed to load thread ${threadIdString} for memory block compaction`,
88
+ )
89
+ const result = yield* tryThreadMemoryBlockEffect(
90
+ () =>
91
+ compactMemoryBlockEntries({
92
+ previousSummary: thread.memoryBlockSummary,
93
+ entries: parseMemoryBlock(thread.memoryBlock),
94
+ triggerEntries: MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
95
+ chunkEntries: MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
96
+ compact: (params) => deps.contextCompactionService.compactMemoryBlock(params),
97
+ }),
98
+ `Failed to compact memory block for thread ${threadIdString}`,
99
+ )
100
+
101
+ if (!result.compacted) return false
102
+
103
+ yield* tryThreadMemoryBlockEffect(
104
+ () =>
105
+ deps.threadStore.update(threadRef, {
106
+ memoryBlockSummary: result.summary || '',
107
+ memoryBlock: serializeMemoryBlock(result.entries),
108
+ }),
109
+ `Failed to persist compacted memory block for thread ${threadIdString}`,
110
+ )
111
+
112
+ return true
113
+ })
114
+ }
115
+
116
+ return { appendMemoryBlock, compactMemoryBlock }
117
+ }
@@ -0,0 +1,363 @@
1
+ import { parseRowMetadata, recordIdSchema, requireTimestamp, withCreatedAtMetadata } from '@lota-sdk/shared'
2
+ import type { ChatMessage } from '@lota-sdk/shared'
3
+ import { Context, Effect, Layer } from 'effect'
4
+ import { RecordId, surql } from 'surrealdb'
5
+ import { z } from 'zod'
6
+
7
+ import { getAgentDisplayNames } from '../../config/agent-defaults'
8
+ import { CursorRowSchema, listMessageHistoryPageEffect } from '../../db/cursor-pagination'
9
+ import type { CursorPaginationConfig } from '../../db/cursor-pagination'
10
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
11
+ import type { RecordIdRef } from '../../db/record-id'
12
+ import type { SurrealDBService } from '../../db/service'
13
+ import { TABLES } from '../../db/tables'
14
+ import { ThreadMessageRowSchema } from '../../db/thread-message-row'
15
+ import type { ThreadMessageRow } from '../../db/thread-message-row'
16
+ import { ServiceError } from '../../effect/errors'
17
+ import { effectTryServicePromise } from '../../effect/helpers'
18
+ import { DatabaseServiceTag } from '../../effect/services'
19
+ import { sha256Hex } from '../../utils/crypto'
20
+ import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
21
+
22
+ const ThreadMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
23
+
24
+ function toMessageId(value: string | RecordIdRef): string {
25
+ return recordIdToString(value, TABLES.THREAD_MESSAGE)
26
+ }
27
+
28
+ function toThreadMessageRowId(threadId: RecordIdRef, messageId: string): RecordId {
29
+ const threadStr = recordIdToString(threadId, TABLES.THREAD)
30
+ const digest = sha256Hex(`${threadStr}\0${messageId}`).slice(0, 32)
31
+ return new RecordId(TABLES.THREAD_MESSAGE, digest)
32
+ }
33
+
34
+ function toThreadRef(threadId: RecordIdRef): RecordId {
35
+ return ensureRecordId(threadId, TABLES.THREAD)
36
+ }
37
+
38
+ function normalizeMessageValue(value: unknown): unknown {
39
+ if (value instanceof Date) {
40
+ return value.toISOString()
41
+ }
42
+
43
+ if (Array.isArray(value)) {
44
+ return value.map((item) => normalizeMessageValue(item))
45
+ }
46
+
47
+ if (value && typeof value === 'object') {
48
+ return Object.fromEntries(
49
+ Object.entries(value as Record<string, unknown>).map(([key, entry]) => [key, normalizeMessageValue(entry)]),
50
+ )
51
+ }
52
+
53
+ return value
54
+ }
55
+
56
+ function readPersistedMessageParts(parts: ThreadMessageRow['parts']): ChatMessage['parts'] {
57
+ return (Array.isArray(parts) ? normalizeMessageValue(parts) : []) as ChatMessage['parts']
58
+ }
59
+
60
+ function toChatMessage(row: ThreadMessageRow): ChatMessage {
61
+ const rowCreatedAt = requireTimestamp(row.createdAt)
62
+ const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
63
+
64
+ return { id: row.messageId, role: row.role, parts: readPersistedMessageParts(row.parts), metadata }
65
+ }
66
+
67
+ function cloneMessageParts(parts: ChatMessage['parts'] | undefined): NonNullable<ThreadMessageRow['parts']> {
68
+ return Array.isArray(parts)
69
+ ? (normalizeMessageValue(structuredClone(parts)) as NonNullable<ThreadMessageRow['parts']>)
70
+ : []
71
+ }
72
+
73
+ const threadPaginationConfig: CursorPaginationConfig = {
74
+ table: TABLES.THREAD_MESSAGE,
75
+ parentFilterField: 'threadId',
76
+ toRowId: toThreadMessageRowId,
77
+ parseRow: (row: unknown) => ThreadMessageRowSchema.parse(row),
78
+ toMessage: (row: unknown) => toChatMessage(ThreadMessageRowSchema.parse(row)),
79
+ queryLatest: (parentId, limit) => surql`
80
+ SELECT * FROM threadMessage
81
+ WHERE threadId = ${parentId}
82
+ ORDER BY createdAt DESC, id DESC
83
+ LIMIT ${limit}
84
+ `,
85
+ queryBefore: (parentId, cursorCreatedAt, cursorId, limit) => surql`
86
+ SELECT * FROM threadMessage
87
+ WHERE threadId = ${parentId}
88
+ AND (
89
+ createdAt < ${cursorCreatedAt}
90
+ OR (createdAt = ${cursorCreatedAt} AND id < ${cursorId})
91
+ )
92
+ ORDER BY createdAt DESC, id DESC
93
+ LIMIT ${limit}
94
+ `,
95
+ }
96
+
97
+ export function makeThreadMessageService(db: SurrealDBService) {
98
+ function upsertMessages(params: { threadId: RecordIdRef; messages: ChatMessage[] }) {
99
+ const threadId = toThreadRef(params.threadId)
100
+ return Effect.forEach(params.messages, (message) =>
101
+ Effect.gen(function* () {
102
+ const messageId = message.id.trim()
103
+ if (!messageId) {
104
+ return
105
+ }
106
+
107
+ const role = message.role
108
+ const parts = cloneMessageParts(message.parts)
109
+ if (parts.length === 0) {
110
+ if (role === 'assistant') {
111
+ return
112
+ }
113
+ return yield* new ServiceError({
114
+ message: `Refusing to persist thread message "${messageId}" with empty parts`,
115
+ })
116
+ }
117
+
118
+ const rowId = toThreadMessageRowId(threadId, messageId)
119
+ const existingRow = yield* effectTryServicePromise(
120
+ () => db.findOne(TABLES.THREAD_MESSAGE, { threadId, messageId }, ThreadMessageExistingRowSchema),
121
+ `Failed to load existing thread message "${messageId}".`,
122
+ )
123
+ const persistedCreatedAt =
124
+ existingRow === null ? requireTimestamp(message.metadata?.createdAt) : requireTimestamp(existingRow.createdAt)
125
+ const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
126
+
127
+ yield* effectTryServicePromise(
128
+ () =>
129
+ db.upsert(
130
+ TABLES.THREAD_MESSAGE,
131
+ rowId,
132
+ {
133
+ threadId,
134
+ messageId,
135
+ role,
136
+ parts,
137
+ metadata,
138
+ createdAt: existingRow ? existingRow.createdAt : unsafeDateFrom(persistedCreatedAt),
139
+ },
140
+ ThreadMessageRowSchema,
141
+ { mutation: 'content' },
142
+ ),
143
+ `Failed to upsert thread message "${messageId}".`,
144
+ )
145
+ }),
146
+ ).pipe(Effect.asVoid)
147
+ }
148
+
149
+ const service = {
150
+ upsertMessages,
151
+ upsertMessagesEffect: upsertMessages,
152
+ listMessages(threadId: RecordIdRef) {
153
+ const threadRef = toThreadRef(threadId)
154
+ return effectTryServicePromise(
155
+ () =>
156
+ db.query<unknown>(surql`
157
+ SELECT * FROM threadMessage
158
+ WHERE threadId = ${threadRef}
159
+ ORDER BY createdAt ASC, id ASC
160
+ `),
161
+ 'Failed to list thread messages.',
162
+ ).pipe(
163
+ Effect.map((rows) => rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))),
164
+ )
165
+ },
166
+ listMessagesEffect(threadId: RecordIdRef) {
167
+ return service.listMessages(threadId)
168
+ },
169
+
170
+ listMessageHistoryPage(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
171
+ const threadRef = toThreadRef(params.threadId)
172
+ return listMessageHistoryPageEffect(db, threadPaginationConfig, {
173
+ parentId: threadRef,
174
+ take: params.take,
175
+ beforeMessageId: params.beforeMessageId,
176
+ })
177
+ },
178
+ listMessageHistoryPageEffect(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
179
+ return service.listMessageHistoryPage(params)
180
+ },
181
+
182
+ listMessagesAfterCursor(threadId: RecordIdRef, afterMessageId?: string) {
183
+ const threadRef = toThreadRef(threadId)
184
+ const cursorMessageId = afterMessageId?.trim()
185
+ if (!cursorMessageId) {
186
+ return service.listMessages(threadRef)
187
+ }
188
+
189
+ return Effect.gen(function* () {
190
+ const cursorRow = yield* effectTryServicePromise(
191
+ () => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef, messageId: cursorMessageId }, CursorRowSchema),
192
+ `Failed to load cursor message "${cursorMessageId}".`,
193
+ )
194
+ if (!cursorRow) {
195
+ return yield* new ServiceError({ message: `Thread cursor message not found: ${cursorMessageId}` })
196
+ }
197
+
198
+ const cursorCreatedAt = cursorRow.createdAt
199
+ const cursorId = toThreadMessageRowId(threadRef, cursorMessageId)
200
+ const rows = yield* effectTryServicePromise(
201
+ () =>
202
+ db.query<unknown>(surql`
203
+ SELECT * FROM threadMessage
204
+ WHERE threadId = ${threadRef}
205
+ AND (
206
+ createdAt > ${cursorCreatedAt}
207
+ OR (createdAt = ${cursorCreatedAt} AND id > ${cursorId})
208
+ )
209
+ ORDER BY createdAt ASC, id ASC
210
+ `),
211
+ 'Failed to list thread messages after cursor.',
212
+ )
213
+ return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
214
+ })
215
+ },
216
+ listMessagesAfterCursorEffect(threadId: RecordIdRef, afterMessageId?: string) {
217
+ return service.listMessagesAfterCursor(threadId, afterMessageId)
218
+ },
219
+
220
+ listRecentMessages(threadId: RecordIdRef, limit: number) {
221
+ const threadRef = toThreadRef(threadId)
222
+ return effectTryServicePromise(
223
+ () =>
224
+ db.query<unknown>(surql`
225
+ SELECT * FROM threadMessage
226
+ WHERE threadId = ${threadRef}
227
+ ORDER BY createdAt DESC, id DESC
228
+ LIMIT ${Math.max(1, limit)}
229
+ `),
230
+ 'Failed to list recent thread messages.',
231
+ ).pipe(
232
+ Effect.map((rows) =>
233
+ rows
234
+ .map((row) => ThreadMessageRowSchema.parse(row))
235
+ .reverse()
236
+ .map((row) => toChatMessage(row)),
237
+ ),
238
+ )
239
+ },
240
+ listRecentMessagesEffect(threadId: RecordIdRef, limit: number) {
241
+ return service.listRecentMessages(threadId, limit)
242
+ },
243
+
244
+ searchMessages(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
245
+ const normalizedQuery = params.query.trim().toLowerCase()
246
+ if (!normalizedQuery) return Effect.succeed([])
247
+
248
+ return Effect.gen(function* () {
249
+ const messages = yield* service.listMessages(toThreadRef(params.threadId))
250
+ return messages
251
+ .filter((message) => message.role === params.role)
252
+ .map((message) => ({
253
+ id: message.id,
254
+ role: message.role as 'user' | 'assistant',
255
+ createdAt: unsafeDateFrom(requireTimestamp(message.metadata?.createdAt)).toISOString(),
256
+ content: message.parts
257
+ .flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
258
+ .join('\n')
259
+ .trim(),
260
+ }))
261
+ .filter((item) => item.content.length > 0 && item.content.toLowerCase().includes(normalizedQuery))
262
+ .slice(-Math.max(1, params.limit))
263
+ })
264
+ },
265
+ searchMessagesEffect(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
266
+ return service.searchMessages(params)
267
+ },
268
+
269
+ addUserMessage(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
270
+ const threadRef = toThreadRef(params.threadId)
271
+ const message: ChatMessage = {
272
+ id: toMessageId(params.messageId),
273
+ role: 'user',
274
+ parts: [{ type: 'text', text: params.content }],
275
+ metadata: { createdAt: nowEpochMillis() },
276
+ }
277
+
278
+ return Effect.gen(function* () {
279
+ yield* upsertMessages({ threadId: threadRef, messages: [message] })
280
+ return message
281
+ })
282
+ },
283
+ addUserMessageEffect(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
284
+ return service.addUserMessage(params)
285
+ },
286
+
287
+ addAgentMessage(params: {
288
+ messageId: RecordIdRef
289
+ threadId: RecordIdRef
290
+ parts: ChatMessage['parts']
291
+ metadata?: ChatMessage['metadata']
292
+ }) {
293
+ const threadRef = toThreadRef(params.threadId)
294
+ const message: ChatMessage = {
295
+ id: toMessageId(params.messageId),
296
+ role: 'assistant',
297
+ parts: params.parts,
298
+ metadata: withCreatedAtMetadata(params.metadata, nowEpochMillis()),
299
+ }
300
+
301
+ return Effect.gen(function* () {
302
+ yield* upsertMessages({ threadId: threadRef, messages: [message] })
303
+ return message
304
+ })
305
+ },
306
+ addAgentMessageEffect(params: {
307
+ messageId: RecordIdRef
308
+ threadId: RecordIdRef
309
+ parts: ChatMessage['parts']
310
+ metadata?: ChatMessage['metadata']
311
+ }) {
312
+ return service.addAgentMessage(params)
313
+ },
314
+
315
+ ensureBootstrapWelcomeMessage(params: { threadId: RecordIdRef; agentId: string; text: string }) {
316
+ const threadRef = toThreadRef(params.threadId)
317
+ return Effect.gen(function* () {
318
+ const existingRow = yield* effectTryServicePromise(
319
+ () => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef }, ThreadMessageExistingRowSchema),
320
+ 'Failed to check for existing bootstrap welcome message.',
321
+ )
322
+ if (existingRow) return
323
+
324
+ const messageText = params.text.trim()
325
+ if (!messageText) return
326
+
327
+ yield* upsertMessages({
328
+ threadId: threadRef,
329
+ messages: [
330
+ {
331
+ id: Bun.randomUUIDv7(),
332
+ role: 'assistant',
333
+ parts: [{ type: 'text', text: messageText }],
334
+ metadata: {
335
+ agentId: params.agentId,
336
+ agentName: getAgentDisplayNames()[params.agentId] ?? params.agentId,
337
+ createdAt: nowEpochMillis(),
338
+ },
339
+ },
340
+ ],
341
+ })
342
+ })
343
+ },
344
+ ensureBootstrapWelcomeMessageEffect(params: { threadId: RecordIdRef; agentId: string; text: string }) {
345
+ return service.ensureBootstrapWelcomeMessage(params)
346
+ },
347
+ }
348
+
349
+ return service
350
+ }
351
+
352
+ export class ThreadMessageServiceTag extends Context.Service<
353
+ ThreadMessageServiceTag,
354
+ ReturnType<typeof makeThreadMessageService>
355
+ >()('ThreadMessageService') {}
356
+
357
+ export const ThreadMessageServiceLive = Layer.effect(
358
+ ThreadMessageServiceTag,
359
+ Effect.gen(function* () {
360
+ const db = yield* DatabaseServiceTag
361
+ return makeThreadMessageService(db)
362
+ }),
363
+ )