@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
package/src/db/startup.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { recordIdSchema } from '@lota-sdk/shared'
2
+ import { Duration, Effect, Match, Schedule } from 'effect'
2
3
  import { BoundQuery, RecordId } from 'surrealdb'
3
4
  import { z } from 'zod'
4
5
 
6
+ import { DatabaseError } from '../effect/errors'
7
+ import { effectTryPromise } from '../effect/helpers'
8
+ import { nowDate } from '../utils/date-time'
5
9
  import { getErrorMessage } from '../utils/errors'
6
10
  import type { SurrealDBService, SurrealDatabaseLogger } from './service'
11
+ import type { SurrealDBError } from './service-normalization'
7
12
  import { TABLES } from './tables'
8
13
 
9
14
  const DATABASE_BOOTSTRAP_KEY = 'database-schema-ready'
@@ -21,48 +26,67 @@ const RuntimeBootstrapRecordSchema = z.object({
21
26
 
22
27
  type StartupLogger = Pick<SurrealDatabaseLogger, 'info' | 'warn' | 'error'>
23
28
 
29
+ type BootstrapWaitFailure =
30
+ | { _tag: 'schema-not-ready'; currentFingerprint: string }
31
+ | { _tag: 'read-failed'; cause: unknown }
32
+
24
33
  function shouldLogRetry(attempt: number): boolean {
25
34
  return attempt === 1 || attempt % RETRY_LOG_INTERVAL === 0
26
35
  }
27
36
 
28
- export async function connectWithStartupRetry(params: {
29
- connect: () => Promise<void>
37
+ function connectWithStartupRetryEffectInternal(params: {
38
+ connect: () => PromiseLike<void> | Effect.Effect<void, SurrealDBError>
30
39
  label: string
31
40
  logger?: StartupLogger
32
41
  retryDelayMs?: number
33
42
  maxWaitMs?: number
34
- }): Promise<void> {
43
+ }): Effect.Effect<void, DatabaseError> {
35
44
  const retryDelayMs = params.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS
36
45
  const maxWaitMs = params.maxWaitMs ?? DEFAULT_MAX_WAIT_MS
37
- const startedAt = Date.now()
38
46
 
39
47
  let attempt = 0
40
- let lastError: unknown = null
41
-
42
- while (Date.now() - startedAt <= maxWaitMs) {
43
- attempt += 1
44
-
45
- try {
46
- await params.connect()
47
- return
48
- } catch (error) {
49
- lastError = error
50
- if (shouldLogRetry(attempt)) {
51
- params.logger?.warn?.(
52
- `Waiting for ${params.label} (${attempt}, elapsed=${Date.now() - startedAt}ms): ${getErrorMessage(error)}`,
53
- )
54
- }
55
- await Bun.sleep(retryDelayMs)
56
- }
57
- }
58
48
 
59
- params.logger?.error?.(`Timed out waiting for ${params.label}: ${getErrorMessage(lastError)}`)
60
- throw lastError instanceof Error ? lastError : new Error(`Timed out waiting for ${params.label}`)
49
+ const connectEffect = effectTryPromise(
50
+ () => params.connect(),
51
+ (error) => new DatabaseError({ message: `Waiting for ${params.label}: ${getErrorMessage(error)}`, cause: error }),
52
+ )
53
+
54
+ return connectEffect.pipe(
55
+ Effect.tapError((error) =>
56
+ Effect.sync(() => {
57
+ attempt++
58
+ if (shouldLogRetry(attempt)) {
59
+ params.logger?.warn?.(error.message)
60
+ }
61
+ }),
62
+ ),
63
+ Effect.retry({
64
+ times: Math.max(0, Math.ceil(maxWaitMs / retryDelayMs) - 1),
65
+ schedule: Schedule.fixed(Duration.millis(retryDelayMs)),
66
+ }),
67
+ Effect.mapError((error) => {
68
+ params.logger?.error?.(`Timed out waiting for ${params.label}: ${getErrorMessage(error)}`)
69
+ return new DatabaseError({
70
+ message: `Timed out waiting for ${params.label}: ${getErrorMessage(error)}`,
71
+ cause: error,
72
+ })
73
+ }),
74
+ )
61
75
  }
62
76
 
63
- async function readDatabaseBootstrapRecord(
77
+ export function connectWithStartupRetry(params: {
78
+ connect: () => PromiseLike<void> | Effect.Effect<void, SurrealDBError>
79
+ label: string
80
+ logger?: StartupLogger
81
+ retryDelayMs?: number
82
+ maxWaitMs?: number
83
+ }): Promise<void> {
84
+ return Effect.runPromise(connectWithStartupRetryEffectInternal(params))
85
+ }
86
+
87
+ function readDatabaseBootstrapRecord(
64
88
  databaseService: SurrealDBService,
65
- ): Promise<z.infer<typeof RuntimeBootstrapRecordSchema> | null> {
89
+ ): Effect.Effect<z.infer<typeof RuntimeBootstrapRecordSchema> | null, SurrealDBError> {
66
90
  return databaseService.queryOne(
67
91
  new BoundQuery(
68
92
  `SELECT *
@@ -75,74 +99,135 @@ async function readDatabaseBootstrapRecord(
75
99
  )
76
100
  }
77
101
 
78
- export async function waitForDatabaseBootstrap(params: {
102
+ function waitForDatabaseBootstrapEffectInternal(params: {
79
103
  databaseService: SurrealDBService
80
104
  expectedFingerprint?: string | null
81
105
  label: string
82
106
  logger?: StartupLogger
83
- connect?: () => Promise<void>
107
+ connect?: () => PromiseLike<void> | Effect.Effect<void, SurrealDBError>
84
108
  retryDelayMs?: number
85
109
  maxWaitMs?: number
86
- }): Promise<void> {
110
+ }): Effect.Effect<void, DatabaseError> {
87
111
  const expectedFingerprint = params.expectedFingerprint?.trim()
88
112
  if (!expectedFingerprint) {
89
- return
113
+ return Effect.void
90
114
  }
91
115
 
92
116
  const retryDelayMs = params.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS
93
117
  const maxWaitMs = params.maxWaitMs ?? DEFAULT_MAX_WAIT_MS
94
- const startedAt = Date.now()
95
-
96
118
  let attempt = 0
97
- let lastError: unknown = null
98
119
 
99
- while (Date.now() - startedAt <= maxWaitMs) {
100
- attempt += 1
120
+ const readBootstrapRecord = Effect.gen(function* () {
121
+ const connect = params.connect
122
+ if (connect) {
123
+ yield* effectTryPromise(
124
+ () => connect(),
125
+ (error): BootstrapWaitFailure => ({ _tag: 'read-failed', cause: error }),
126
+ )
127
+ }
101
128
 
102
- try {
103
- if (params.connect) {
104
- await params.connect()
105
- }
129
+ return yield* effectTryPromise(
130
+ () => readDatabaseBootstrapRecord(params.databaseService),
131
+ (error): BootstrapWaitFailure => ({ _tag: 'read-failed', cause: error }),
132
+ )
133
+ })
106
134
 
107
- const record = await readDatabaseBootstrapRecord(params.databaseService)
135
+ const waitForExpectedFingerprint = readBootstrapRecord.pipe(
136
+ Effect.flatMap((record) => {
108
137
  if (record?.schemaFingerprint === expectedFingerprint) {
109
- return
138
+ return Effect.void
110
139
  }
111
140
 
112
- if (shouldLogRetry(attempt)) {
113
- const currentFingerprint =
114
- typeof record?.schemaFingerprint === 'string' && record.schemaFingerprint.length > 0
115
- ? record.schemaFingerprint
116
- : 'missing'
117
- params.logger?.info?.(
118
- `Waiting for ${params.label} schema readiness (${attempt}, expected=${expectedFingerprint}, current=${currentFingerprint})`,
119
- )
120
- }
121
- } catch (error) {
122
- lastError = error
123
- if (shouldLogRetry(attempt)) {
124
- params.logger?.warn?.(`Waiting for ${params.label} schema readiness (${attempt}): ${getErrorMessage(error)}`)
125
- }
126
- }
141
+ const currentFingerprint =
142
+ typeof record?.schemaFingerprint === 'string' && record.schemaFingerprint.length > 0
143
+ ? record.schemaFingerprint
144
+ : 'missing'
145
+ return Effect.fail<BootstrapWaitFailure>({ _tag: 'schema-not-ready', currentFingerprint })
146
+ }),
147
+ )
127
148
 
128
- await Bun.sleep(retryDelayMs)
129
- }
149
+ const logFailure = Match.type<BootstrapWaitFailure>().pipe(
150
+ Match.tag('schema-not-ready', (failure) => {
151
+ params.logger?.info?.(
152
+ `Waiting for ${params.label} schema readiness (${attempt}, expected=${expectedFingerprint}, current=${failure.currentFingerprint})`,
153
+ )
154
+ }),
155
+ Match.tag('read-failed', (failure) => {
156
+ params.logger?.warn?.(
157
+ `Waiting for ${params.label} schema readiness (${attempt}): ${getErrorMessage(failure.cause)}`,
158
+ )
159
+ }),
160
+ Match.exhaustive,
161
+ )
130
162
 
131
- if (lastError instanceof Error) {
132
- throw lastError
133
- }
163
+ const toTimeoutError = Match.type<BootstrapWaitFailure>().pipe(
164
+ Match.tag(
165
+ 'schema-not-ready',
166
+ () => new DatabaseError({ message: `Timed out waiting for ${params.label} schema readiness` }),
167
+ ),
168
+ Match.tag(
169
+ 'read-failed',
170
+ (failure) =>
171
+ new DatabaseError({ message: `Timed out waiting for ${params.label} schema readiness`, cause: failure.cause }),
172
+ ),
173
+ Match.exhaustive,
174
+ )
134
175
 
135
- throw new Error(`Timed out waiting for ${params.label} schema readiness`)
176
+ return waitForExpectedFingerprint.pipe(
177
+ Effect.tapError((failure) =>
178
+ Effect.sync(() => {
179
+ attempt++
180
+ if (!shouldLogRetry(attempt)) return
181
+ logFailure(failure)
182
+ }),
183
+ ),
184
+ Effect.retry({
185
+ times: Math.max(0, Math.ceil(maxWaitMs / retryDelayMs) - 1),
186
+ schedule: Schedule.fixed(Duration.millis(retryDelayMs)),
187
+ }),
188
+ Effect.mapError(toTimeoutError),
189
+ )
136
190
  }
137
191
 
138
- export async function publishDatabaseBootstrap(params: {
192
+ export function waitForDatabaseBootstrap(params: {
193
+ databaseService: SurrealDBService
194
+ expectedFingerprint?: string | null
195
+ label: string
196
+ logger?: StartupLogger
197
+ connect?: () => PromiseLike<void> | Effect.Effect<void, SurrealDBError>
198
+ retryDelayMs?: number
199
+ maxWaitMs?: number
200
+ }): Promise<void> {
201
+ return Effect.runPromise(waitForDatabaseBootstrapEffectInternal(params))
202
+ }
203
+
204
+ export function publishDatabaseBootstrap(params: {
139
205
  databaseService: SurrealDBService
140
206
  schemaFingerprint: string
141
207
  }): Promise<void> {
142
- await params.databaseService.upsert(
143
- TABLES.RUNTIME_BOOTSTRAP,
144
- new RecordId(TABLES.RUNTIME_BOOTSTRAP, DATABASE_BOOTSTRAP_KEY),
145
- { key: DATABASE_BOOTSTRAP_KEY, schemaFingerprint: params.schemaFingerprint, readyAt: new Date() },
146
- RuntimeBootstrapRecordSchema,
208
+ return Effect.runPromise(publishDatabaseBootstrapEffect(params))
209
+ }
210
+
211
+ export function publishDatabaseBootstrapEffect(params: {
212
+ databaseService: SurrealDBService
213
+ schemaFingerprint: string
214
+ }): Effect.Effect<void, DatabaseError> {
215
+ return Effect.asVoid(
216
+ params.databaseService
217
+ .upsert(
218
+ TABLES.RUNTIME_BOOTSTRAP,
219
+ new RecordId(TABLES.RUNTIME_BOOTSTRAP, DATABASE_BOOTSTRAP_KEY),
220
+ { key: DATABASE_BOOTSTRAP_KEY, schemaFingerprint: params.schemaFingerprint, readyAt: nowDate() },
221
+ RuntimeBootstrapRecordSchema,
222
+ )
223
+ .pipe(
224
+ Effect.mapError(
225
+ (error) =>
226
+ new DatabaseError({
227
+ message: `Failed to publish database bootstrap: ${getErrorMessage(error)}`,
228
+ cause: error,
229
+ }),
230
+ ),
231
+ ),
147
232
  )
148
233
  }
@@ -0,0 +1,15 @@
1
+ export function isRetriableTransactionConflict(error: unknown): boolean {
2
+ if (!(error instanceof Error)) {
3
+ return false
4
+ }
5
+
6
+ const message = error.message.toLowerCase()
7
+ return (
8
+ message.includes('transaction conflict') ||
9
+ message.includes('transaction read conflict') ||
10
+ message.includes('read or write conflict') ||
11
+ message.includes('write conflict') ||
12
+ message.includes('resource busy') ||
13
+ message.includes('this transaction can be retried')
14
+ )
15
+ }
@@ -0,0 +1,96 @@
1
+ import { Effect } from 'effect'
2
+
3
+ export type AwaitableEffect<A, E> = Effect.Effect<A, E, never> & Promise<A>
4
+
5
+ export type AwaitableValue<T> = T extends (...args: infer TArgs) => Effect.Effect<infer A, infer E, never>
6
+ ? (...args: TArgs) => AwaitableEffect<A, E>
7
+ : T extends (...args: infer TArgs) => infer TResult
8
+ ? (...args: TArgs) => TResult
9
+ : T
10
+
11
+ type AwaitableServiceMethods<T> = {
12
+ [K in keyof T]: AwaitableValue<T[K]>
13
+ }
14
+
15
+ export type AwaitableService<T> = AwaitableServiceMethods<T>
16
+
17
+ export type MaybeAwaitableService<T extends object> = T | AwaitableService<T>
18
+
19
+ type AwaitableEffectOptions = { runPromise?: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A> }
20
+
21
+ type Callable = (...args: Array<unknown>) => unknown
22
+
23
+ function isCallable(value: unknown): value is Callable {
24
+ return typeof value === 'function'
25
+ }
26
+
27
+ function isRuntimeEffect(value: unknown): value is Effect.Effect<unknown, never, never> {
28
+ return Effect.isEffect(value)
29
+ }
30
+
31
+ /**
32
+ * Wraps an Effect so it can be `await`-ed as if it were a Promise, while still
33
+ * passing `Effect.isEffect` checks and exposing the underlying Effect properties.
34
+ *
35
+ * Implementation: returns a `Proxy` around the Effect that only intercepts the
36
+ * PromiseLike methods (`then`/`catch`/`finally`) plus `Symbol.toStringTag`. The
37
+ * Promise is created lazily on first `await` and cached for the wrapper's
38
+ * lifetime so repeated awaits share a result. The original Effect object is
39
+ * never mutated.
40
+ */
41
+ export function toAwaitableEffect<A, E>(
42
+ effect: Effect.Effect<A, E, never>,
43
+ options?: AwaitableEffectOptions,
44
+ ): AwaitableEffect<A, E> {
45
+ const runPromise = options?.runPromise ?? Effect.runPromise
46
+ let cachedPromise: Promise<A> | undefined
47
+ const getPromise = (): Promise<A> => (cachedPromise ??= runPromise(effect))
48
+
49
+ const then: Promise<A>['then'] = (onFulfilled, onRejected) => getPromise().then(onFulfilled, onRejected)
50
+ const catchMethod: Promise<A>['catch'] = (onRejected) => getPromise().catch(onRejected)
51
+ const finallyMethod: Promise<A>['finally'] = (onFinally) => getPromise().finally(onFinally)
52
+
53
+ return new Proxy(effect as object, {
54
+ get(target, property, receiver) {
55
+ if (property === 'then') return then
56
+ if (property === 'catch') return catchMethod
57
+ if (property === 'finally') return finallyMethod
58
+ if (property === Symbol.toStringTag) return 'Promise'
59
+ return Reflect.get(target, property, receiver) as unknown
60
+ },
61
+ }) as AwaitableEffect<A, E>
62
+ }
63
+
64
+ /**
65
+ * Returns a proxy over a service whose Effect-returning methods are
66
+ * transparently wrapped with `toAwaitableEffect`. Non-Effect return values
67
+ * pass through untouched.
68
+ */
69
+ export function toAwaitableService<T extends object>(
70
+ service: T,
71
+ options?: AwaitableEffectOptions,
72
+ ): AwaitableService<T> {
73
+ const wrappedMethods = new Map<PropertyKey, Callable>()
74
+
75
+ return new Proxy(service, {
76
+ get(target, property, receiver) {
77
+ const value = Reflect.get(target, property, receiver)
78
+ if (!isCallable(value)) {
79
+ return value
80
+ }
81
+
82
+ const existing = wrappedMethods.get(property)
83
+ if (existing) {
84
+ return existing
85
+ }
86
+
87
+ const wrapped: Callable = (...args) => {
88
+ const result = value.apply(target, args)
89
+ return isRuntimeEffect(result) ? toAwaitableEffect(result, options) : result
90
+ }
91
+
92
+ wrappedMethods.set(property, wrapped)
93
+ return wrapped
94
+ },
95
+ }) as AwaitableService<T>
96
+ }
@@ -0,0 +1,121 @@
1
+ import { Schema } from 'effect'
2
+
3
+ const ValidationIssueSchema = Schema.Struct({ path: Schema.String, message: Schema.String })
4
+
5
+ export type ValidationIssue = typeof ValidationIssueSchema.Type
6
+
7
+ export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()('DatabaseError', {
8
+ message: Schema.String,
9
+ query: Schema.optional(Schema.String),
10
+ cause: Schema.optional(Schema.Defect),
11
+ }) {}
12
+
13
+ export class ServiceError extends Schema.TaggedErrorClass<ServiceError>()('ServiceError', {
14
+ message: Schema.String,
15
+ cause: Schema.optional(Schema.Defect),
16
+ }) {}
17
+
18
+ export class BaseServicePersistenceError extends Schema.TaggedErrorClass<BaseServicePersistenceError>()(
19
+ 'BaseServicePersistenceError',
20
+ { message: Schema.String, cause: Schema.Defect },
21
+ ) {}
22
+
23
+ export class RedisError extends Schema.TaggedErrorClass<RedisError>()('RedisError', {
24
+ message: Schema.String,
25
+ cause: Schema.optional(Schema.Defect),
26
+ }) {}
27
+
28
+ export class TimeoutError extends Schema.TaggedErrorClass<TimeoutError>()('TimeoutError', {
29
+ operation: Schema.String,
30
+ ms: Schema.Number,
31
+ }) {}
32
+
33
+ export class LockAcquisitionError extends Schema.TaggedErrorClass<LockAcquisitionError>()('LockAcquisitionError', {
34
+ lockKey: Schema.String,
35
+ maxWaitMs: Schema.Number,
36
+ }) {}
37
+
38
+ export class LockLostError extends Schema.TaggedErrorClass<LockLostError>()('LockLostError', {
39
+ lockKey: Schema.String,
40
+ }) {}
41
+
42
+ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()('NotFoundError', {
43
+ resource: Schema.String,
44
+ id: Schema.optional(Schema.String),
45
+ message: Schema.String,
46
+ }) {}
47
+
48
+ export class BadRequestError extends Schema.TaggedErrorClass<BadRequestError>()('BadRequestError', {
49
+ message: Schema.String,
50
+ }) {}
51
+
52
+ export class ConflictError extends Schema.TaggedErrorClass<ConflictError>()('ConflictError', {
53
+ message: Schema.String,
54
+ }) {}
55
+
56
+ export class ForbiddenError extends Schema.TaggedErrorClass<ForbiddenError>()('ForbiddenError', {
57
+ message: Schema.String,
58
+ }) {}
59
+
60
+ export class ValidationError extends Schema.TaggedErrorClass<ValidationError>()('ValidationError', {
61
+ message: Schema.String,
62
+ issues: Schema.optional(Schema.Array(ValidationIssueSchema)),
63
+ }) {}
64
+
65
+ export class AiGenerationError extends Schema.TaggedErrorClass<AiGenerationError>()('AiGenerationError', {
66
+ message: Schema.String,
67
+ source: Schema.String,
68
+ status: Schema.optional(Schema.Number),
69
+ rateLimited: Schema.optional(Schema.Boolean),
70
+ retryAfter: Schema.optional(Schema.String),
71
+ providerData: Schema.optional(Schema.String),
72
+ responseBody: Schema.optional(Schema.String),
73
+ url: Schema.optional(Schema.String),
74
+ }) {}
75
+
76
+ export class ThreadTurnError extends Schema.TaggedErrorClass<ThreadTurnError>()('ThreadTurnError', {
77
+ message: Schema.String,
78
+ reason: Schema.Literals(['bad-request', 'conflict']),
79
+ }) {}
80
+
81
+ export class ActiveThreadRunConflictError extends Schema.TaggedErrorClass<ActiveThreadRunConflictError>()(
82
+ 'ActiveThreadRunConflictError',
83
+ { threadId: Schema.String, activeRunId: Schema.String, message: Schema.String },
84
+ ) {}
85
+
86
+ export class ConfigurationError extends Schema.TaggedErrorClass<ConfigurationError>()('ConfigurationError', {
87
+ message: Schema.String,
88
+ key: Schema.optional(Schema.String),
89
+ }) {}
90
+
91
+ const EFFECT_ERROR_CLASSES = [
92
+ ActiveThreadRunConflictError,
93
+ AiGenerationError,
94
+ BadRequestError,
95
+ BaseServicePersistenceError,
96
+ ConfigurationError,
97
+ ConflictError,
98
+ DatabaseError,
99
+ ForbiddenError,
100
+ LockAcquisitionError,
101
+ LockLostError,
102
+ NotFoundError,
103
+ RedisError,
104
+ ServiceError,
105
+ ThreadTurnError,
106
+ TimeoutError,
107
+ ValidationError,
108
+ ] as const
109
+
110
+ export type EffectError = InstanceType<(typeof EFFECT_ERROR_CLASSES)[number]>
111
+
112
+ const EFFECT_ERROR_TAGS = new Set(EFFECT_ERROR_CLASSES.map((cls) => cls.name))
113
+
114
+ export function isEffectError(error: unknown): error is EffectError {
115
+ if (!error || typeof error !== 'object') {
116
+ return false
117
+ }
118
+
119
+ const tag = (error as { _tag?: unknown })._tag
120
+ return typeof tag === 'string' && EFFECT_ERROR_TAGS.has(tag)
121
+ }
@@ -0,0 +1,123 @@
1
+ import { Cause, Effect } from 'effect'
2
+
3
+ import { ServiceError } from './errors'
4
+
5
+ export function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
6
+ if (typeof value !== 'object' && typeof value !== 'function') {
7
+ return false
8
+ }
9
+
10
+ if (value === null) {
11
+ return false
12
+ }
13
+
14
+ return 'then' in value && typeof value.then === 'function'
15
+ }
16
+
17
+ type PromiseEffectEvaluator<A, E = never, R = never> =
18
+ | (() => PromiseLike<A> | Effect.Effect<A, E, R>)
19
+ | ((signal: AbortSignal) => PromiseLike<A> | Effect.Effect<A, E, R>)
20
+
21
+ function invokePromiseEffectEvaluator<A, E, R>(
22
+ evaluate: PromiseEffectEvaluator<A, E, R>,
23
+ ): PromiseLike<A> | Effect.Effect<A, E, R> {
24
+ return (evaluate as () => PromiseLike<A> | Effect.Effect<A, E, R>)()
25
+ }
26
+
27
+ export function effectTryPromise<A, E = never, R = never>(
28
+ evaluate: PromiseEffectEvaluator<A, E, R>,
29
+ ): Effect.Effect<A, Cause.UnknownError, R>
30
+ export function effectTryPromise<A, E1, E2, R>(
31
+ evaluate: PromiseEffectEvaluator<A, E1, R>,
32
+ onError: (cause: unknown) => E2,
33
+ ): Effect.Effect<A, E2, R>
34
+ export function effectTryPromise<A, E1, E2, R>(
35
+ evaluate: PromiseEffectEvaluator<A, E1, R>,
36
+ onError?: (cause: unknown) => E2,
37
+ ): Effect.Effect<A, E2 | Cause.UnknownError, R> {
38
+ return Effect.suspend(() => {
39
+ try {
40
+ const value = invokePromiseEffectEvaluator(evaluate)
41
+ if (Effect.isEffect(value)) {
42
+ if (onError) {
43
+ return value.pipe(Effect.mapError((cause) => onError(cause)))
44
+ }
45
+
46
+ return value.pipe(Effect.mapError((cause) => new Cause.UnknownError(cause)))
47
+ }
48
+
49
+ return onError
50
+ ? Effect.tryPromise({ try: () => value, catch: onError })
51
+ : Effect.tryPromise({ try: () => value, catch: (cause) => new Cause.UnknownError(cause) })
52
+ } catch (cause) {
53
+ return Effect.fail(onError ? onError(cause) : new Cause.UnknownError(cause))
54
+ }
55
+ })
56
+ }
57
+
58
+ export function effectTryServicePromise<A, E = never, R = never>(
59
+ evaluate: PromiseEffectEvaluator<A, E, R>,
60
+ message: string,
61
+ ): Effect.Effect<A, ServiceError, R> {
62
+ return Effect.suspend(() => {
63
+ try {
64
+ const value = invokePromiseEffectEvaluator(evaluate)
65
+ if (Effect.isEffect(value)) {
66
+ return value.pipe(Effect.mapError((cause) => new ServiceError({ message, cause })))
67
+ }
68
+
69
+ return Effect.tryPromise({ try: () => value, catch: (cause) => new ServiceError({ message, cause }) })
70
+ } catch (cause) {
71
+ return Effect.fail(new ServiceError({ message, cause }))
72
+ }
73
+ })
74
+ }
75
+
76
+ export function makeEffectTryPromiseWithMessage<E>(onError: (message: string, cause: unknown) => E) {
77
+ return <A, E1 = never, R = never>(
78
+ evaluate: PromiseEffectEvaluator<A, E1, R>,
79
+ message: string,
80
+ ): Effect.Effect<A, E, R> => effectTryPromise(evaluate, (cause) => onError(message, cause))
81
+ }
82
+
83
+ export function makeEffectTryPromiseWithOperation<E>(
84
+ onError: (operation: string, message: string, cause: unknown) => E,
85
+ ) {
86
+ return <A, E1 = never, R = never>(
87
+ operation: string,
88
+ message: string,
89
+ evaluate: PromiseEffectEvaluator<A, E1, R>,
90
+ ): Effect.Effect<A, E, R> => effectTryPromise(evaluate, (cause) => onError(operation, message, cause))
91
+ }
92
+
93
+ export function effectTryMaybeAsync<A>(evaluate: () => A | PromiseLike<A>): Effect.Effect<A, Cause.UnknownError, never>
94
+ export function effectTryMaybeAsync<A, E>(
95
+ evaluate: () => A | PromiseLike<A>,
96
+ onError: (cause: unknown) => E,
97
+ ): Effect.Effect<A, E, never>
98
+ export function effectTryMaybeAsync<A, E>(
99
+ evaluate: () => A | PromiseLike<A>,
100
+ onError?: (cause: unknown) => E,
101
+ ): Effect.Effect<A, E | Cause.UnknownError, never> {
102
+ return Effect.suspend(() => {
103
+ try {
104
+ const value = evaluate()
105
+ return isPromiseLike(value)
106
+ ? onError
107
+ ? effectTryPromise(() => value, onError)
108
+ : effectTryPromise(() => value)
109
+ : Effect.succeed(value)
110
+ } catch (cause) {
111
+ return Effect.fail(onError ? onError(cause) : new Cause.UnknownError(cause))
112
+ }
113
+ })
114
+ }
115
+
116
+ export function iterateEffect<A, E, R>(
117
+ initial: A,
118
+ options: { while: (state: A) => boolean; body: (state: A) => Effect.Effect<A, E, R> },
119
+ ): Effect.Effect<A, E, R> {
120
+ const step = (state: A): Effect.Effect<A, E, R> =>
121
+ options.while(state) ? options.body(state).pipe(Effect.flatMap(step)) : Effect.succeed(state)
122
+ return Effect.suspend(() => step(initial))
123
+ }
@@ -0,0 +1,24 @@
1
+ export * from './awaitable-effect'
2
+ export * from './helpers'
3
+ export * from './layers'
4
+ export * from './runtime'
5
+ export * from './services'
6
+ export * from './zod'
7
+ export {
8
+ ActiveThreadRunConflictError,
9
+ AiGenerationError,
10
+ BaseServicePersistenceError,
11
+ ConfigurationError,
12
+ ConflictError,
13
+ DatabaseError,
14
+ ForbiddenError,
15
+ LockAcquisitionError,
16
+ LockLostError,
17
+ RedisError,
18
+ ServiceError,
19
+ ThreadTurnError,
20
+ TimeoutError,
21
+ ValidationError,
22
+ isEffectError,
23
+ } from './errors'
24
+ export type { EffectError, ValidationIssue } from './errors'