@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
@@ -0,0 +1,379 @@
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
+ import type { ZodTypeAny } from 'zod'
7
+
8
+ import { getAgentDisplayNames } from '../../config/agent-defaults'
9
+ import { CursorRowSchema, listMessageHistoryPageEffect } from '../../db/cursor-pagination'
10
+ import type { CursorPaginationConfig } from '../../db/cursor-pagination'
11
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
12
+ import type { RecordIdRef } from '../../db/record-id'
13
+ import type { SurrealDBService } from '../../db/service'
14
+ import { TABLES } from '../../db/tables'
15
+ import { ThreadMessageRowSchema } from '../../db/thread-message-row'
16
+ import type { ThreadMessageRow } from '../../db/thread-message-row'
17
+ import { ServiceError } from '../../effect/errors'
18
+ import { effectTryServicePromise } from '../../effect/helpers'
19
+ import { DatabaseServiceTag } from '../../effect/services'
20
+ import { sha256Hex } from '../../utils/crypto'
21
+ import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
22
+
23
+ const ThreadMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
24
+
25
+ function parseRowOrFail<TSchema extends ZodTypeAny>(
26
+ schema: TSchema,
27
+ value: unknown,
28
+ operation: string,
29
+ ): Effect.Effect<z.infer<TSchema>, ServiceError> {
30
+ return Effect.try({
31
+ try: () => schema.parse(value) as z.infer<TSchema>,
32
+ catch: (cause) => new ServiceError({ message: `Failed to parse row for ${operation}.`, cause }),
33
+ })
34
+ }
35
+
36
+ function toMessageId(value: string | RecordIdRef): string {
37
+ return recordIdToString(value, TABLES.THREAD_MESSAGE)
38
+ }
39
+
40
+ function toThreadMessageRowId(threadId: RecordIdRef, messageId: string): RecordId {
41
+ const threadStr = recordIdToString(threadId, TABLES.THREAD)
42
+ const digest = sha256Hex(`${threadStr}\0${messageId}`).slice(0, 32)
43
+ return new RecordId(TABLES.THREAD_MESSAGE, digest)
44
+ }
45
+
46
+ function toThreadRef(threadId: RecordIdRef): RecordId {
47
+ return ensureRecordId(threadId, TABLES.THREAD)
48
+ }
49
+
50
+ function normalizeMessageValue(value: unknown): unknown {
51
+ if (value instanceof Date) {
52
+ return value.toISOString()
53
+ }
54
+
55
+ if (Array.isArray(value)) {
56
+ return value.map((item) => normalizeMessageValue(item))
57
+ }
58
+
59
+ if (value && typeof value === 'object') {
60
+ return Object.fromEntries(
61
+ Object.entries(value as Record<string, unknown>).map(([key, entry]) => [key, normalizeMessageValue(entry)]),
62
+ )
63
+ }
64
+
65
+ return value
66
+ }
67
+
68
+ function readPersistedMessageParts(parts: ThreadMessageRow['parts']): ChatMessage['parts'] {
69
+ return (Array.isArray(parts) ? normalizeMessageValue(parts) : []) as ChatMessage['parts']
70
+ }
71
+
72
+ function toChatMessage(row: ThreadMessageRow): ChatMessage {
73
+ const rowCreatedAt = requireTimestamp(row.createdAt)
74
+ const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
75
+
76
+ return { id: row.messageId, role: row.role, parts: readPersistedMessageParts(row.parts), metadata }
77
+ }
78
+
79
+ function cloneMessageParts(parts: ChatMessage['parts'] | undefined): NonNullable<ThreadMessageRow['parts']> {
80
+ return Array.isArray(parts)
81
+ ? (normalizeMessageValue(structuredClone(parts)) as NonNullable<ThreadMessageRow['parts']>)
82
+ : []
83
+ }
84
+
85
+ const threadPaginationConfig: CursorPaginationConfig = {
86
+ table: TABLES.THREAD_MESSAGE,
87
+ parentFilterField: 'threadId',
88
+ toRowId: toThreadMessageRowId,
89
+ parseRow: (row: unknown) => ThreadMessageRowSchema.parse(row),
90
+ toMessage: (row: unknown) => toChatMessage(ThreadMessageRowSchema.parse(row)),
91
+ queryLatest: (parentId, limit) => surql`
92
+ SELECT * FROM threadMessage
93
+ WHERE threadId = ${parentId}
94
+ ORDER BY createdAt DESC, id DESC
95
+ LIMIT ${limit}
96
+ `,
97
+ queryBefore: (parentId, cursorCreatedAt, cursorId, limit) => surql`
98
+ SELECT * FROM threadMessage
99
+ WHERE threadId = ${parentId}
100
+ AND (
101
+ createdAt < ${cursorCreatedAt}
102
+ OR (createdAt = ${cursorCreatedAt} AND id < ${cursorId})
103
+ )
104
+ ORDER BY createdAt DESC, id DESC
105
+ LIMIT ${limit}
106
+ `,
107
+ }
108
+
109
+ export function makeThreadMessageService(db: SurrealDBService) {
110
+ function upsertMessages(params: { threadId: RecordIdRef; messages: ChatMessage[] }) {
111
+ const threadId = toThreadRef(params.threadId)
112
+ return Effect.forEach(params.messages, (message) =>
113
+ Effect.gen(function* () {
114
+ const messageId = message.id.trim()
115
+ if (!messageId) {
116
+ return
117
+ }
118
+
119
+ const role = message.role
120
+ const parts = cloneMessageParts(message.parts)
121
+ if (parts.length === 0) {
122
+ if (role === 'assistant') {
123
+ return
124
+ }
125
+ return yield* new ServiceError({
126
+ message: `Refusing to persist thread message "${messageId}" with empty parts`,
127
+ })
128
+ }
129
+
130
+ const rowId = toThreadMessageRowId(threadId, messageId)
131
+ const existingRow = yield* effectTryServicePromise(
132
+ () => db.findOne(TABLES.THREAD_MESSAGE, { threadId, messageId }, ThreadMessageExistingRowSchema),
133
+ `Failed to load existing thread message "${messageId}".`,
134
+ )
135
+ const persistedCreatedAt =
136
+ existingRow === null ? requireTimestamp(message.metadata?.createdAt) : requireTimestamp(existingRow.createdAt)
137
+ const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
138
+
139
+ yield* effectTryServicePromise(
140
+ () =>
141
+ db.upsert(
142
+ TABLES.THREAD_MESSAGE,
143
+ rowId,
144
+ {
145
+ threadId,
146
+ messageId,
147
+ role,
148
+ parts,
149
+ metadata,
150
+ createdAt: existingRow ? existingRow.createdAt : unsafeDateFrom(persistedCreatedAt),
151
+ },
152
+ ThreadMessageRowSchema,
153
+ { mutation: 'content' },
154
+ ),
155
+ `Failed to upsert thread message "${messageId}".`,
156
+ )
157
+ }),
158
+ ).pipe(Effect.asVoid)
159
+ }
160
+
161
+ const service = {
162
+ upsertMessages,
163
+ upsertMessagesEffect: upsertMessages,
164
+ listMessages(threadId: RecordIdRef) {
165
+ const threadRef = toThreadRef(threadId)
166
+ return effectTryServicePromise(
167
+ () =>
168
+ db.query<unknown>(surql`
169
+ SELECT * FROM threadMessage
170
+ WHERE threadId = ${threadRef}
171
+ ORDER BY createdAt ASC, id ASC
172
+ `),
173
+ 'Failed to list thread messages.',
174
+ ).pipe(
175
+ Effect.flatMap((rows) =>
176
+ Effect.forEach(rows, (row) => parseRowOrFail(ThreadMessageRowSchema, row, 'listMessages')),
177
+ ),
178
+ Effect.map((rows) => rows.map((row) => toChatMessage(row))),
179
+ )
180
+ },
181
+ listMessagesEffect(threadId: RecordIdRef) {
182
+ return service.listMessages(threadId)
183
+ },
184
+
185
+ listMessageHistoryPage(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
186
+ const threadRef = toThreadRef(params.threadId)
187
+ return listMessageHistoryPageEffect(db, threadPaginationConfig, {
188
+ parentId: threadRef,
189
+ take: params.take,
190
+ beforeMessageId: params.beforeMessageId,
191
+ })
192
+ },
193
+ listMessageHistoryPageEffect(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
194
+ return service.listMessageHistoryPage(params)
195
+ },
196
+
197
+ listMessagesAfterCursor(threadId: RecordIdRef, afterMessageId?: string) {
198
+ const threadRef = toThreadRef(threadId)
199
+ const cursorMessageId = afterMessageId?.trim()
200
+ if (!cursorMessageId) {
201
+ return service.listMessages(threadRef)
202
+ }
203
+
204
+ return Effect.gen(function* () {
205
+ const cursorRow = yield* effectTryServicePromise(
206
+ () => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef, messageId: cursorMessageId }, CursorRowSchema),
207
+ `Failed to load cursor message "${cursorMessageId}".`,
208
+ )
209
+ if (!cursorRow) {
210
+ return yield* new ServiceError({ message: `Thread cursor message not found: ${cursorMessageId}` })
211
+ }
212
+
213
+ const cursorCreatedAt = cursorRow.createdAt
214
+ const cursorId = toThreadMessageRowId(threadRef, cursorMessageId)
215
+ const rows = yield* effectTryServicePromise(
216
+ () =>
217
+ db.query<unknown>(surql`
218
+ SELECT * FROM threadMessage
219
+ WHERE threadId = ${threadRef}
220
+ AND (
221
+ createdAt > ${cursorCreatedAt}
222
+ OR (createdAt = ${cursorCreatedAt} AND id > ${cursorId})
223
+ )
224
+ ORDER BY createdAt ASC, id ASC
225
+ `),
226
+ 'Failed to list thread messages after cursor.',
227
+ )
228
+ const parsedRows = yield* Effect.forEach(rows, (row) =>
229
+ parseRowOrFail(ThreadMessageRowSchema, row, 'listMessagesAfterCursor'),
230
+ )
231
+ return parsedRows.map((row) => toChatMessage(row))
232
+ })
233
+ },
234
+ listMessagesAfterCursorEffect(threadId: RecordIdRef, afterMessageId?: string) {
235
+ return service.listMessagesAfterCursor(threadId, afterMessageId)
236
+ },
237
+
238
+ listRecentMessages(threadId: RecordIdRef, limit: number) {
239
+ const threadRef = toThreadRef(threadId)
240
+ return effectTryServicePromise(
241
+ () =>
242
+ db.query<unknown>(surql`
243
+ SELECT * FROM threadMessage
244
+ WHERE threadId = ${threadRef}
245
+ ORDER BY createdAt DESC, id DESC
246
+ LIMIT ${Math.max(1, limit)}
247
+ `),
248
+ 'Failed to list recent thread messages.',
249
+ ).pipe(
250
+ Effect.flatMap((rows) =>
251
+ Effect.forEach(rows, (row) => parseRowOrFail(ThreadMessageRowSchema, row, 'listRecentMessages')),
252
+ ),
253
+ Effect.map((rows) => rows.reverse().map((row) => toChatMessage(row))),
254
+ )
255
+ },
256
+ listRecentMessagesEffect(threadId: RecordIdRef, limit: number) {
257
+ return service.listRecentMessages(threadId, limit)
258
+ },
259
+
260
+ searchMessages(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
261
+ const normalizedQuery = params.query.trim().toLowerCase()
262
+ if (!normalizedQuery) return Effect.succeed([])
263
+
264
+ return Effect.gen(function* () {
265
+ const messages = yield* service.listMessages(toThreadRef(params.threadId))
266
+ return messages
267
+ .filter((message) => message.role === params.role)
268
+ .map((message) => ({
269
+ id: message.id,
270
+ role: message.role as 'user' | 'assistant',
271
+ createdAt: unsafeDateFrom(requireTimestamp(message.metadata?.createdAt)).toISOString(),
272
+ content: message.parts
273
+ .flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
274
+ .join('\n')
275
+ .trim(),
276
+ }))
277
+ .filter((item) => item.content.length > 0 && item.content.toLowerCase().includes(normalizedQuery))
278
+ .slice(-Math.max(1, params.limit))
279
+ })
280
+ },
281
+ searchMessagesEffect(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
282
+ return service.searchMessages(params)
283
+ },
284
+
285
+ addUserMessage(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
286
+ const threadRef = toThreadRef(params.threadId)
287
+ const message: ChatMessage = {
288
+ id: toMessageId(params.messageId),
289
+ role: 'user',
290
+ parts: [{ type: 'text', text: params.content }],
291
+ metadata: { createdAt: nowEpochMillis() },
292
+ }
293
+
294
+ return Effect.gen(function* () {
295
+ yield* upsertMessages({ threadId: threadRef, messages: [message] })
296
+ return message
297
+ })
298
+ },
299
+ addUserMessageEffect(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
300
+ return service.addUserMessage(params)
301
+ },
302
+
303
+ addAgentMessage(params: {
304
+ messageId: RecordIdRef
305
+ threadId: RecordIdRef
306
+ parts: ChatMessage['parts']
307
+ metadata?: ChatMessage['metadata']
308
+ }) {
309
+ const threadRef = toThreadRef(params.threadId)
310
+ const message: ChatMessage = {
311
+ id: toMessageId(params.messageId),
312
+ role: 'assistant',
313
+ parts: params.parts,
314
+ metadata: withCreatedAtMetadata(params.metadata, nowEpochMillis()),
315
+ }
316
+
317
+ return Effect.gen(function* () {
318
+ yield* upsertMessages({ threadId: threadRef, messages: [message] })
319
+ return message
320
+ })
321
+ },
322
+ addAgentMessageEffect(params: {
323
+ messageId: RecordIdRef
324
+ threadId: RecordIdRef
325
+ parts: ChatMessage['parts']
326
+ metadata?: ChatMessage['metadata']
327
+ }) {
328
+ return service.addAgentMessage(params)
329
+ },
330
+
331
+ ensureBootstrapWelcomeMessage(params: { threadId: RecordIdRef; agentId: string; text: string }) {
332
+ const threadRef = toThreadRef(params.threadId)
333
+ return Effect.gen(function* () {
334
+ const existingRow = yield* effectTryServicePromise(
335
+ () => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef }, ThreadMessageExistingRowSchema),
336
+ 'Failed to check for existing bootstrap welcome message.',
337
+ )
338
+ if (existingRow) return
339
+
340
+ const messageText = params.text.trim()
341
+ if (!messageText) return
342
+
343
+ yield* upsertMessages({
344
+ threadId: threadRef,
345
+ messages: [
346
+ {
347
+ id: Bun.randomUUIDv7(),
348
+ role: 'assistant',
349
+ parts: [{ type: 'text', text: messageText }],
350
+ metadata: {
351
+ agentId: params.agentId,
352
+ agentName: getAgentDisplayNames()[params.agentId] ?? params.agentId,
353
+ createdAt: nowEpochMillis(),
354
+ },
355
+ },
356
+ ],
357
+ })
358
+ })
359
+ },
360
+ ensureBootstrapWelcomeMessageEffect(params: { threadId: RecordIdRef; agentId: string; text: string }) {
361
+ return service.ensureBootstrapWelcomeMessage(params)
362
+ },
363
+ }
364
+
365
+ return service
366
+ }
367
+
368
+ export class ThreadMessageServiceTag extends Context.Service<
369
+ ThreadMessageServiceTag,
370
+ ReturnType<typeof makeThreadMessageService>
371
+ >()('@lota-sdk/core/ThreadMessageService') {}
372
+
373
+ export const ThreadMessageServiceLive = Layer.effect(
374
+ ThreadMessageServiceTag,
375
+ Effect.gen(function* () {
376
+ const db = yield* DatabaseServiceTag
377
+ return makeThreadMessageService(db)
378
+ }),
379
+ )
@@ -0,0 +1,155 @@
1
+ import { Duration, Effect, Schedule } from 'effect'
2
+
3
+ import type { RecordIdInput, RecordIdRef } from '../../db/record-id'
4
+ import { ensureRecordId, isRecordIdInput, recordIdToString } from '../../db/record-id'
5
+ import type { SurrealDBService } from '../../db/service'
6
+ import { TABLES } from '../../db/tables'
7
+ import { BadRequestError, DatabaseError, NotFoundError } from '../../effect/errors'
8
+ import { ThreadSchema } from './thread.types'
9
+ import type { ThreadRecord } from './thread.types'
10
+
11
+ const THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS = 20
12
+ const THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS = 100
13
+
14
+ function requireThreadIdEffect(id: unknown): Effect.Effect<RecordIdInput, BadRequestError> {
15
+ return isRecordIdInput(id)
16
+ ? Effect.succeed(id)
17
+ : Effect.fail(new BadRequestError({ message: `Invalid record id for table ${TABLES.THREAD}` }))
18
+ }
19
+
20
+ type UniqueThreadLookupParams = {
21
+ type: 'default' | 'thread'
22
+ organizationId: RecordIdRef
23
+ userId: RecordIdRef
24
+ agentId?: string
25
+ threadType?: string
26
+ }
27
+
28
+ function buildExistingThreadLookupMessage(params: UniqueThreadLookupParams): string {
29
+ const scope = {
30
+ organizationId: recordIdToString(params.organizationId, TABLES.ORGANIZATION),
31
+ userId: recordIdToString(params.userId, TABLES.USER),
32
+ ...(params.agentId ? { agentId: params.agentId } : {}),
33
+ ...(params.threadType ? { threadType: params.threadType } : {}),
34
+ }
35
+ return `Thread lookup failed after duplicate ${params.type} thread create: ${JSON.stringify(scope)}`
36
+ }
37
+ type ThreadOrderKey = Extract<keyof ThreadRecord, string>
38
+
39
+ function mapThreadStoreError(message: string) {
40
+ return Effect.mapError((cause: unknown) => new DatabaseError({ message, cause }))
41
+ }
42
+
43
+ export interface ThreadRecordStore {
44
+ findById(id: unknown): Effect.Effect<ThreadRecord | null, DatabaseError | BadRequestError>
45
+ getById(id: unknown): Effect.Effect<ThreadRecord, DatabaseError | BadRequestError | NotFoundError>
46
+ findAll(
47
+ filter?: Record<string, unknown>,
48
+ options?: { limit?: number; offset?: number; orderBy?: ThreadOrderKey; orderDir?: 'ASC' | 'DESC' },
49
+ ): Effect.Effect<ThreadRecord[], DatabaseError>
50
+ create(data: Record<string, unknown>): Effect.Effect<ThreadRecord, DatabaseError>
51
+ update(id: unknown, data: Record<string, unknown>): Effect.Effect<ThreadRecord, DatabaseError | NotFoundError>
52
+ deleteById(id: unknown): Effect.Effect<void, DatabaseError | NotFoundError>
53
+ findThreadByUniqueLookup(params: UniqueThreadLookupParams): Effect.Effect<ThreadRecord | null, DatabaseError>
54
+ waitForExistingThread(params: UniqueThreadLookupParams): Effect.Effect<ThreadRecord, NotFoundError | DatabaseError>
55
+ }
56
+
57
+ export function createThreadRecordStore(deps: { db: SurrealDBService }): ThreadRecordStore {
58
+ const { db } = deps
59
+ function findById(id: unknown): Effect.Effect<ThreadRecord | null, DatabaseError | BadRequestError> {
60
+ return Effect.gen(function* () {
61
+ const threadId = yield* requireThreadIdEffect(id)
62
+ return yield* db
63
+ .findOne(TABLES.THREAD, { id: ensureRecordId(threadId, TABLES.THREAD) }, ThreadSchema)
64
+ .pipe(mapThreadStoreError('Failed to load thread record.'))
65
+ })
66
+ }
67
+
68
+ function getById(id: unknown): Effect.Effect<ThreadRecord, DatabaseError | BadRequestError | NotFoundError> {
69
+ return Effect.gen(function* () {
70
+ const record = yield* findById(id)
71
+ if (!record) {
72
+ return yield* new NotFoundError({ resource: TABLES.THREAD, message: `${TABLES.THREAD} record not found` })
73
+ }
74
+ return record
75
+ })
76
+ }
77
+
78
+ function findAll(
79
+ filter: Record<string, unknown> = {},
80
+ options?: { limit?: number; offset?: number; orderBy?: ThreadOrderKey; orderDir?: 'ASC' | 'DESC' },
81
+ ): Effect.Effect<ThreadRecord[], DatabaseError> {
82
+ return db
83
+ .findMany(TABLES.THREAD, filter, ThreadSchema, options)
84
+ .pipe(mapThreadStoreError('Failed to list threads.'))
85
+ }
86
+
87
+ function create(data: Record<string, unknown>): Effect.Effect<ThreadRecord, DatabaseError> {
88
+ return db.create(TABLES.THREAD, data, ThreadSchema).pipe(mapThreadStoreError('Failed to create thread record.'))
89
+ }
90
+
91
+ function update(
92
+ id: unknown,
93
+ data: Record<string, unknown>,
94
+ ): Effect.Effect<ThreadRecord, DatabaseError | NotFoundError> {
95
+ return Effect.gen(function* () {
96
+ const record = yield* db
97
+ .update(TABLES.THREAD, id, data, ThreadSchema)
98
+ .pipe(mapThreadStoreError('Failed to update thread record.'))
99
+ if (!record) {
100
+ return yield* new NotFoundError({ resource: TABLES.THREAD, message: `${TABLES.THREAD} record not found` })
101
+ }
102
+ return record
103
+ })
104
+ }
105
+
106
+ function deleteById(id: unknown): Effect.Effect<void, DatabaseError | NotFoundError> {
107
+ return Effect.gen(function* () {
108
+ const deleted = yield* db
109
+ .deleteById(TABLES.THREAD, id)
110
+ .pipe(mapThreadStoreError('Failed to delete thread record.'))
111
+ if (!deleted) {
112
+ return yield* new NotFoundError({ resource: TABLES.THREAD, message: `${TABLES.THREAD} record not found` })
113
+ }
114
+ })
115
+ }
116
+
117
+ function findThreadByUniqueLookup(
118
+ params: UniqueThreadLookupParams,
119
+ ): Effect.Effect<ThreadRecord | null, DatabaseError> {
120
+ return db
121
+ .findOne(
122
+ TABLES.THREAD,
123
+ {
124
+ type: params.type,
125
+ organizationId: params.organizationId,
126
+ userId: params.userId,
127
+ ...(params.agentId ? { agentId: params.agentId } : {}),
128
+ ...(params.threadType ? { threadType: params.threadType } : {}),
129
+ },
130
+ ThreadSchema,
131
+ )
132
+ .pipe(mapThreadStoreError('Failed to find thread by unique lookup.'))
133
+ }
134
+
135
+ function waitForExistingThread(
136
+ params: UniqueThreadLookupParams,
137
+ ): Effect.Effect<ThreadRecord, NotFoundError | DatabaseError> {
138
+ const poll = findThreadByUniqueLookup(params).pipe(
139
+ Effect.flatMap((result) => (result ? Effect.succeed(result) : Effect.fail('not_found' as const))),
140
+ )
141
+
142
+ return Effect.retry(poll, {
143
+ times: THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS - 1,
144
+ schedule: Schedule.fixed(Duration.millis(THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS)),
145
+ }).pipe(
146
+ Effect.mapError((error) =>
147
+ error === 'not_found'
148
+ ? new NotFoundError({ resource: TABLES.THREAD, message: buildExistingThreadLookupMessage(params) })
149
+ : error,
150
+ ),
151
+ )
152
+ }
153
+
154
+ return { findById, getById, findAll, create, update, deleteById, findThreadByUniqueLookup, waitForExistingThread }
155
+ }
@@ -0,0 +1,74 @@
1
+ import { THREAD } from '@lota-sdk/shared'
2
+ import { Context, Schema, Effect, Layer } from 'effect'
3
+
4
+ import { chatLogger } from '../../config/logger'
5
+ import type { RecordIdRef } from '../../db/record-id'
6
+ import type { HelperModelRuntime } from '../../runtime/helper-model'
7
+ import { HelperModelTag } from '../../runtime/helper-model'
8
+ import { deriveTitle, limitTitleWords, normalizeTitle } from '../../runtime/title-helpers'
9
+ import {
10
+ createThreadTitleGeneratorAgent,
11
+ THREAD_TITLE_GENERATOR_PROMPT,
12
+ } from '../../system-agents/title-generator.agent'
13
+ import type { makeThreadService } from './thread.service'
14
+ import { ThreadServiceTag } from './thread.service'
15
+
16
+ const THREAD_TITLE_TIMEOUT_MS = 30_000
17
+
18
+ class ThreadTitleError extends Schema.TaggedErrorClass<ThreadTitleError>()('ThreadTitleError', {
19
+ message: Schema.String,
20
+ cause: Schema.optional(Schema.Defect),
21
+ }) {}
22
+
23
+ export function makeThreadTitleService(
24
+ threadService: ReturnType<typeof makeThreadService>,
25
+ helperModelRuntime: HelperModelRuntime,
26
+ ) {
27
+ return {
28
+ generateAndPersistTitle(threadId: RecordIdRef, sourceText: string) {
29
+ return Effect.gen(function* () {
30
+ const generatedTitle = yield* Effect.tryPromise({
31
+ try: () =>
32
+ helperModelRuntime.generateHelperText({
33
+ tag: 'thread-title',
34
+ createAgent: createThreadTitleGeneratorAgent,
35
+ defaultSystemPrompt: THREAD_TITLE_GENERATOR_PROMPT,
36
+ timeoutMs: THREAD_TITLE_TIMEOUT_MS,
37
+ messages: [{ role: 'user', content: sourceText }],
38
+ }),
39
+ catch: (error) => new ThreadTitleError({ message: 'Failed to generate thread title.', cause: error }),
40
+ }).pipe(
41
+ Effect.map((title) => normalizeTitle(title)),
42
+ Effect.catch((error) =>
43
+ Effect.sync(() => {
44
+ chatLogger.warn`Failed to generate thread title via LLM (non-fatal): ${error}`
45
+ return ''
46
+ }),
47
+ ),
48
+ )
49
+ const title = generatedTitle || limitTitleWords(deriveTitle(sourceText || THREAD.DEFAULT_TITLE))
50
+ yield* threadService
51
+ .update(threadId, { title, nameGenerated: true })
52
+ .pipe(
53
+ Effect.mapError(
54
+ (error) => new ThreadTitleError({ message: 'Failed to persist generated thread title.', cause: error }),
55
+ ),
56
+ )
57
+ })
58
+ },
59
+ }
60
+ }
61
+
62
+ export class ThreadTitleServiceTag extends Context.Service<
63
+ ThreadTitleServiceTag,
64
+ ReturnType<typeof makeThreadTitleService>
65
+ >()('@lota-sdk/core/ThreadTitleService') {}
66
+
67
+ export const ThreadTitleServiceLive = Layer.effect(
68
+ ThreadTitleServiceTag,
69
+ Effect.gen(function* () {
70
+ const threadService = yield* ThreadServiceTag
71
+ const helperModelRuntime = yield* HelperModelTag
72
+ return makeThreadTitleService(threadService, helperModelRuntime)
73
+ }),
74
+ )