@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
package/src/db/startup.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { recordIdSchema } from '@lota-sdk/shared'
2
+ import { Duration, Effect, 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
48
 
42
- while (Date.now() - startedAt <= maxWaitMs) {
43
- attempt += 1
49
+ const connectEffect = effectTryPromise(
50
+ () => params.connect(),
51
+ (error) => new DatabaseError({ message: `Waiting for ${params.label}: ${getErrorMessage(error)}`, cause: error }),
52
+ )
44
53
 
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
- }
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
+ )
75
+ }
58
76
 
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}`)
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))
61
85
  }
62
86
 
63
- async function readDatabaseBootstrapRecord(
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,124 @@ 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
+ return waitForExpectedFingerprint.pipe(
150
+ Effect.tapError((failure) =>
151
+ Effect.sync(() => {
152
+ attempt++
153
+ if (!shouldLogRetry(attempt)) return
130
154
 
131
- if (lastError instanceof Error) {
132
- throw lastError
133
- }
155
+ if (failure._tag === 'schema-not-ready') {
156
+ params.logger?.info?.(
157
+ `Waiting for ${params.label} schema readiness (${attempt}, expected=${expectedFingerprint}, current=${failure.currentFingerprint})`,
158
+ )
159
+ return
160
+ }
134
161
 
135
- throw new Error(`Timed out waiting for ${params.label} schema readiness`)
162
+ params.logger?.warn?.(
163
+ `Waiting for ${params.label} schema readiness (${attempt}): ${getErrorMessage(failure.cause)}`,
164
+ )
165
+ }),
166
+ ),
167
+ Effect.retry({
168
+ times: Math.max(0, Math.ceil(maxWaitMs / retryDelayMs) - 1),
169
+ schedule: Schedule.fixed(Duration.millis(retryDelayMs)),
170
+ }),
171
+ Effect.mapError(
172
+ (failure) =>
173
+ new DatabaseError({
174
+ message: `Timed out waiting for ${params.label} schema readiness`,
175
+ ...(failure._tag === 'read-failed' ? { cause: failure.cause } : {}),
176
+ }),
177
+ ),
178
+ )
136
179
  }
137
180
 
138
- export async function publishDatabaseBootstrap(params: {
181
+ export function waitForDatabaseBootstrap(params: {
182
+ databaseService: SurrealDBService
183
+ expectedFingerprint?: string | null
184
+ label: string
185
+ logger?: StartupLogger
186
+ connect?: () => PromiseLike<void> | Effect.Effect<void, SurrealDBError>
187
+ retryDelayMs?: number
188
+ maxWaitMs?: number
189
+ }): Promise<void> {
190
+ return Effect.runPromise(waitForDatabaseBootstrapEffectInternal(params))
191
+ }
192
+
193
+ export function publishDatabaseBootstrap(params: {
139
194
  databaseService: SurrealDBService
140
195
  schemaFingerprint: string
141
196
  }): 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,
197
+ return Effect.runPromise(publishDatabaseBootstrapEffect(params))
198
+ }
199
+
200
+ export function publishDatabaseBootstrapEffect(params: {
201
+ databaseService: SurrealDBService
202
+ schemaFingerprint: string
203
+ }): Effect.Effect<void, DatabaseError> {
204
+ return Effect.asVoid(
205
+ params.databaseService
206
+ .upsert(
207
+ TABLES.RUNTIME_BOOTSTRAP,
208
+ new RecordId(TABLES.RUNTIME_BOOTSTRAP, DATABASE_BOOTSTRAP_KEY),
209
+ { key: DATABASE_BOOTSTRAP_KEY, schemaFingerprint: params.schemaFingerprint, readyAt: nowDate() },
210
+ RuntimeBootstrapRecordSchema,
211
+ )
212
+ .pipe(
213
+ Effect.mapError(
214
+ (error) =>
215
+ new DatabaseError({
216
+ message: `Failed to publish database bootstrap: ${getErrorMessage(error)}`,
217
+ cause: error,
218
+ }),
219
+ ),
220
+ ),
147
221
  )
148
222
  }
@@ -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,87 @@
1
+ import { Effect } from 'effect'
2
+
3
+ const awaitableEffectMarker = Symbol('AwaitableEffect')
4
+
5
+ type AwaitableEffectState<A> = { promise?: Promise<A> }
6
+
7
+ export type AwaitableEffect<A, E> = Effect.Effect<A, E, never> & Promise<A>
8
+
9
+ export type AwaitableValue<T> = T extends (...args: infer TArgs) => Effect.Effect<infer A, infer E, never>
10
+ ? (...args: TArgs) => AwaitableEffect<A, E>
11
+ : T extends (...args: infer TArgs) => infer TResult
12
+ ? (...args: TArgs) => TResult
13
+ : T
14
+
15
+ type AwaitableServiceMethods<T> = {
16
+ [K in keyof T]: AwaitableValue<T[K]>
17
+ }
18
+
19
+ export type AwaitableService<T> = AwaitableServiceMethods<T>
20
+
21
+ export type MaybeAwaitableService<T extends object> = T | AwaitableService<T>
22
+
23
+ type AwaitableEffectOptions = { runPromise?: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A> }
24
+
25
+ export function toAwaitableEffect<A, E>(
26
+ effect: Effect.Effect<A, E, never>,
27
+ options?: AwaitableEffectOptions,
28
+ ): AwaitableEffect<A, E> {
29
+ const existing = effect as AwaitableEffect<A, E> & { [awaitableEffectMarker]?: AwaitableEffectState<A> }
30
+
31
+ if (existing[awaitableEffectMarker]) {
32
+ return existing
33
+ }
34
+
35
+ const state: AwaitableEffectState<A> = {}
36
+ const runPromise = options?.runPromise ?? Effect.runPromise
37
+ const ensurePromise = () => (state.promise ??= runPromise(effect))
38
+ const candidate = Object.create(
39
+ Object.getPrototypeOf(effect),
40
+ Object.getOwnPropertyDescriptors(effect),
41
+ ) as AwaitableEffect<A, E> & { [awaitableEffectMarker]?: AwaitableEffectState<A> }
42
+
43
+ const then: Promise<A>['then'] = (onfulfilled, onrejected) => ensurePromise().then(onfulfilled, onrejected)
44
+ const catchMethod: Promise<A>['catch'] = (onrejected) => ensurePromise().catch(onrejected)
45
+ const finallyMethod: Promise<A>['finally'] = (onfinally) => ensurePromise().finally(onfinally)
46
+
47
+ void Object.defineProperties(candidate, {
48
+ then: { value: then, configurable: true },
49
+ catch: { value: catchMethod, configurable: true },
50
+ finally: { value: finallyMethod, configurable: true },
51
+ [Symbol.toStringTag]: { value: 'Promise', configurable: true },
52
+ [awaitableEffectMarker]: { value: state },
53
+ })
54
+
55
+ return candidate
56
+ }
57
+
58
+ export function toAwaitableService<T extends object>(
59
+ service: T,
60
+ options?: AwaitableEffectOptions,
61
+ ): AwaitableService<T> {
62
+ const wrappedMethods = new Map<PropertyKey, unknown>()
63
+
64
+ return new Proxy(service, {
65
+ get(target, property, receiver) {
66
+ const value = Reflect.get(target, property, receiver)
67
+ if (typeof value !== 'function') {
68
+ return value
69
+ }
70
+
71
+ const existing = wrappedMethods.get(property)
72
+ if (existing !== undefined) {
73
+ return existing
74
+ }
75
+
76
+ const wrapped = (...args: unknown[]) => {
77
+ const result = Reflect.apply(value, target, args)
78
+ return Effect.isEffect(result)
79
+ ? toAwaitableEffect(result as Effect.Effect<unknown, unknown, never>, options)
80
+ : result
81
+ }
82
+
83
+ wrappedMethods.set(property, wrapped)
84
+ return wrapped
85
+ },
86
+ }) as AwaitableService<T>
87
+ }
@@ -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,98 @@
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
+ export function effectTryPromise<A, E = never, R = never>(
22
+ evaluate: PromiseEffectEvaluator<A, E, R>,
23
+ ): Effect.Effect<A, Cause.UnknownError, R>
24
+ export function effectTryPromise<A, E1, E2, R>(
25
+ evaluate: PromiseEffectEvaluator<A, E1, R>,
26
+ onError: (cause: unknown) => E2,
27
+ ): Effect.Effect<A, E2, R>
28
+ export function effectTryPromise<A, E1, E2, R>(
29
+ evaluate: PromiseEffectEvaluator<A, E1, R>,
30
+ onError?: (cause: unknown) => E2,
31
+ ): Effect.Effect<A, E2 | Cause.UnknownError, R> {
32
+ return Effect.suspend(() => {
33
+ try {
34
+ const value = (evaluate as () => PromiseLike<A> | Effect.Effect<A, E1, R>)()
35
+ if (Effect.isEffect(value)) {
36
+ if (onError) {
37
+ return value.pipe(Effect.mapError((cause) => onError(cause)))
38
+ }
39
+
40
+ return value.pipe(Effect.mapError((cause) => new Cause.UnknownError(cause)))
41
+ }
42
+
43
+ return onError
44
+ ? Effect.tryPromise({ try: () => value, catch: onError })
45
+ : Effect.tryPromise({ try: () => value, catch: (cause) => new Cause.UnknownError(cause) })
46
+ } catch (cause) {
47
+ return Effect.fail(onError ? onError(cause) : new Cause.UnknownError(cause))
48
+ }
49
+ })
50
+ }
51
+
52
+ export function effectTryServicePromise<A>(
53
+ evaluate: PromiseEffectEvaluator<A, unknown, never>,
54
+ message: string,
55
+ ): Effect.Effect<A, ServiceError, never> {
56
+ return Effect.suspend(() => {
57
+ try {
58
+ const value = (evaluate as () => PromiseLike<A> | Effect.Effect<A, unknown, never>)()
59
+ if (Effect.isEffect(value)) {
60
+ return value.pipe(Effect.mapError((cause) => new ServiceError({ message, cause })))
61
+ }
62
+
63
+ return Effect.tryPromise({ try: () => value, catch: (cause) => new ServiceError({ message, cause }) })
64
+ } catch (cause) {
65
+ return Effect.fail(new ServiceError({ message, cause }))
66
+ }
67
+ })
68
+ }
69
+
70
+ export function makeEffectTryPromiseWithMessage<E>(onError: (message: string, cause: unknown) => E) {
71
+ return <A, E1 = never, R = never>(
72
+ evaluate: PromiseEffectEvaluator<A, E1, R>,
73
+ message: string,
74
+ ): Effect.Effect<A, E, R> => effectTryPromise(evaluate, (cause) => onError(message, cause))
75
+ }
76
+
77
+ export function effectTryMaybeAsync<A>(evaluate: () => A | PromiseLike<A>): Effect.Effect<A, Cause.UnknownError, never>
78
+ export function effectTryMaybeAsync<A, E>(
79
+ evaluate: () => A | PromiseLike<A>,
80
+ onError: (cause: unknown) => E,
81
+ ): Effect.Effect<A, E, never>
82
+ export function effectTryMaybeAsync<A, E>(
83
+ evaluate: () => A | PromiseLike<A>,
84
+ onError?: (cause: unknown) => E,
85
+ ): Effect.Effect<A, E | Cause.UnknownError, never> {
86
+ return Effect.suspend(() => {
87
+ try {
88
+ const value = evaluate()
89
+ return isPromiseLike(value)
90
+ ? onError
91
+ ? effectTryPromise(() => value, onError)
92
+ : effectTryPromise(() => value)
93
+ : Effect.succeed(value)
94
+ } catch (cause) {
95
+ return Effect.fail(onError ? onError(cause) : new Cause.UnknownError(cause))
96
+ }
97
+ })
98
+ }
@@ -0,0 +1,22 @@
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
+ ConfigurationError,
10
+ ConflictError,
11
+ DatabaseError,
12
+ LockAcquisitionError,
13
+ LockLostError,
14
+ RedisError,
15
+ isEffectError,
16
+ ThreadTurnError as EffectThreadTurnError,
17
+ TimeoutError,
18
+ ValidationError as EffectValidationError,
19
+ BadRequestError as EffectBadRequestError,
20
+ NotFoundError as EffectNotFoundError,
21
+ } from './errors'
22
+ export type { EffectError, ValidationIssue } from './errors'