@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
@@ -1,8 +1,12 @@
1
1
  import { inferContentType } from '@lota-sdk/shared'
2
2
  import { S3Client } from 'bun'
3
+ import { Context, Schema, Effect, Layer } from 'effect'
3
4
 
4
5
  import { serverLogger } from '../config/logger'
5
- import { getRuntimeConfig } from '../runtime/runtime-config'
6
+ import { BadRequestError, ForbiddenError } from '../effect/errors'
7
+ import { RuntimeConfigServiceTag } from '../effect/services'
8
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
9
+ import { getErrorMessage } from '../utils/errors'
6
10
  import { readString } from '../utils/string'
7
11
  import {
8
12
  extractAttachmentText,
@@ -10,12 +14,7 @@ import {
10
14
  isPdfAttachmentFile,
11
15
  splitExtractedTextIntoPages,
12
16
  } from './attachment-parser'
13
- import type {
14
- MessagePartLike,
15
- ReadableUploadMetadata,
16
- ReadableUploadPageMode,
17
- ReadableUploadPagePart,
18
- } from './attachment-types'
17
+ import type { MessagePartLike, ReadableUploadMetadata } from './attachment-types'
19
18
  import {
20
19
  buildOrganizationDocumentStorageKey,
21
20
  buildUploadStorageKey,
@@ -37,161 +36,173 @@ export type UploadedThreadAttachment = {
37
36
 
38
37
  type AttachmentContextCandidate = { storageKey: string; name: string; contentType: string; sizeBytes: number | null }
39
38
 
40
- export class AttachmentStorageService {
41
- private readonly client: S3Client
42
-
43
- constructor() {
44
- const config = getRuntimeConfig()
45
- this.client = new S3Client({
46
- accessKeyId: config.s3.accessKeyId,
47
- secretAccessKey: config.s3.secretAccessKey,
48
- bucket: config.s3.bucket,
49
- endpoint: config.s3.endpoint,
50
- region: config.s3.region,
51
- })
52
- }
39
+ class AttachmentStorageError extends Schema.TaggedErrorClass<AttachmentStorageError>()('AttachmentStorageError', {
40
+ operation: Schema.String,
41
+ message: Schema.String,
42
+ cause: Schema.Defect,
43
+ }) {}
53
44
 
54
- getAttachmentUrl(storageKey: string): string {
55
- return this.client.file(storageKey).presign({ expiresIn: getRuntimeConfig().s3.attachmentUrlExpiresIn })
56
- }
45
+ function toAttachmentStorageError(operation: string, cause: unknown): AttachmentStorageError {
46
+ return new AttachmentStorageError({ operation, message: getErrorMessage(cause), cause })
47
+ }
57
48
 
58
- async writeOrganizationDocument({
59
- orgId,
60
- namespace,
61
- relativePath,
62
- content,
63
- contentType,
64
- }: {
65
- orgId: string
66
- namespace: string
67
- relativePath: string
68
- content: string
69
- contentType: string
70
- }): Promise<{ storageKey: string; sizeBytes: number }> {
71
- const storageKey = buildOrganizationDocumentStorageKey({ orgId, namespace, relativePath })
72
- const sizeBytes = Buffer.byteLength(content, 'utf8')
49
+ export function makeAttachmentStorageService(config: ResolvedLotaRuntimeConfig) {
50
+ const client = new S3Client({
51
+ accessKeyId: config.s3.accessKeyId,
52
+ secretAccessKey: config.s3.secretAccessKey,
53
+ bucket: config.s3.bucket,
54
+ endpoint: config.s3.endpoint,
55
+ region: config.s3.region,
56
+ })
57
+
58
+ function getAttachmentUrl(storageKey: string): string {
59
+ return client.file(storageKey).presign({ expiresIn: config.s3.attachmentUrlExpiresIn })
60
+ }
73
61
 
74
- await this.client.file(storageKey).write(new Blob([content]), { type: contentType })
62
+ function readFilenameFromStorageKey(storageKey: string): string | null {
63
+ const slashIndex = storageKey.lastIndexOf('/')
64
+ if (slashIndex < 0) return null
65
+ const rawName = storageKey.slice(slashIndex + 1).trim()
66
+ if (!rawName) return null
75
67
 
76
- return { storageKey, sizeBytes }
68
+ const dashIndex = rawName.indexOf('-')
69
+ if (dashIndex <= 0 || dashIndex === rawName.length - 1) return rawName
70
+ return rawName.slice(dashIndex + 1)
77
71
  }
78
72
 
79
- async uploadOrganizationDocument({
80
- file,
73
+ function collectAttachmentContextCandidates({
74
+ parts,
81
75
  orgId,
82
- namespace,
83
- relativePath,
76
+ userId,
84
77
  }: {
85
- file: File
78
+ parts: readonly MessagePartLike[]
86
79
  orgId: string
87
- namespace: string
88
- relativePath: string
89
- }): Promise<UploadedThreadAttachment> {
90
- const filename = file.name || 'document'
91
- const mediaType = file.type || inferContentType(filename)
92
- const storageKey = buildOrganizationDocumentStorageKey({
93
- orgId,
94
- namespace,
95
- relativePath: relativePath.trim().length > 0 ? relativePath : filename,
96
- })
80
+ userId: string
81
+ }): AttachmentContextCandidate[] {
82
+ const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
83
+ const candidates: AttachmentContextCandidate[] = []
84
+
85
+ for (const part of parts) {
86
+ if (part.type !== 'file') continue
97
87
 
98
- await this.client.file(storageKey).write(file, { type: mediaType })
88
+ const providerMetadata = readRecord(part.providerMetadata)
89
+ const lotaMetadata = readRecord(providerMetadata?.lota)
90
+ const storageKey = readString(lotaMetadata?.attachmentStorageKey)
91
+ if (!storageKey || !storageKey.startsWith(storagePrefix)) continue
99
92
 
100
- return {
101
- filename,
102
- mediaType,
103
- sizeBytes: file.size,
104
- storageKey,
105
- url: this.getAttachmentUrl(storageKey),
106
- expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
93
+ const name = readString(part.filename) ?? readFilenameFromStorageKey(storageKey) ?? 'Attachment'
94
+ const contentType = readString(part.mediaType) ?? inferContentType(name)
95
+ const sizeBytes = readNonNegativeInteger(lotaMetadata?.attachmentSizeBytes)
96
+ candidates.push({ storageKey, name, contentType, sizeBytes })
107
97
  }
98
+
99
+ return candidates
108
100
  }
109
101
 
110
- async uploadThreadAttachment({
111
- file,
102
+ function assertUploadOwnership({
103
+ storageKey,
112
104
  orgId,
113
105
  userId,
114
106
  }: {
115
- file: File
107
+ storageKey: string
116
108
  orgId: string
117
109
  userId: string
118
- }): Promise<UploadedThreadAttachment> {
119
- const filename = file.name || 'attachment'
120
- const mediaType = file.type || inferContentType(filename)
121
- const sizeBytes = file.size
122
- const storageKey = buildUploadStorageKey({ orgId, userId, filename, uploadId: Bun.randomUUIDv7() })
123
-
124
- await this.client.file(storageKey).write(file, { type: mediaType })
125
-
126
- return {
127
- filename,
128
- mediaType,
129
- sizeBytes,
130
- storageKey,
131
- url: this.getAttachmentUrl(storageKey),
132
- expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
110
+ }): Effect.Effect<void, ForbiddenError> {
111
+ const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
112
+ if (!storageKey.startsWith(storagePrefix)) {
113
+ return Effect.fail(
114
+ new ForbiddenError({ message: 'Upload does not belong to the current organization/user scope.' }),
115
+ )
133
116
  }
117
+ return Effect.void
134
118
  }
135
119
 
136
- hydrateSignedFileUrlsInMessageParts({
137
- parts,
120
+ function writeOrganizationDocumentEffect({
138
121
  orgId,
139
- userId,
122
+ namespace,
123
+ relativePath,
124
+ content,
125
+ contentType,
140
126
  }: {
141
- parts: readonly MessagePartLike[]
142
127
  orgId: string
143
- userId: string
144
- }): MessagePartLike[] {
145
- const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
146
-
147
- return parts.map((part) => {
148
- if (part.type !== 'file') return part
128
+ namespace: string
129
+ relativePath: string
130
+ content: string
131
+ contentType: string
132
+ }) {
133
+ return Effect.gen(function* () {
134
+ const storageKey = buildOrganizationDocumentStorageKey({ orgId, namespace, relativePath })
135
+ const sizeBytes = Buffer.byteLength(content, 'utf8')
149
136
 
150
- const providerMetadata = readRecord(part.providerMetadata)
151
- const lotaMetadata = readRecord(providerMetadata?.lota)
152
- const storageKey = lotaMetadata?.attachmentStorageKey
153
- if (typeof storageKey !== 'string' || !storageKey.startsWith(storagePrefix)) return part
137
+ yield* Effect.tryPromise({
138
+ try: () => client.file(storageKey).write(new Blob([content]), { type: contentType }),
139
+ catch: (error) => toAttachmentStorageError('writeOrganizationDocument', error),
140
+ })
154
141
 
155
- return { ...part, url: this.getAttachmentUrl(storageKey) }
142
+ return { storageKey, sizeBytes }
156
143
  })
157
144
  }
158
145
 
159
- listReadableUploadsFromMessages({
160
- messages,
146
+ function uploadOrganizationDocumentEffect({
147
+ file,
161
148
  orgId,
162
- userId,
149
+ namespace,
150
+ relativePath,
163
151
  }: {
164
- messages: ReadonlyArray<{ parts: readonly MessagePartLike[] }>
152
+ file: File
165
153
  orgId: string
166
- userId: string
167
- }): ReadableUploadMetadata[] {
168
- const uploadsByStorageKey = new Map<
169
- string,
170
- { storageKey: string; filename: string; mediaType: string; sizeBytes: number | null }
171
- >()
172
-
173
- for (const message of messages) {
174
- const candidates = this.collectAttachmentContextCandidates({ parts: message.parts, orgId, userId })
175
- for (const candidate of candidates) {
176
- if (uploadsByStorageKey.has(candidate.storageKey)) continue
177
- uploadsByStorageKey.set(candidate.storageKey, {
178
- storageKey: candidate.storageKey,
179
- filename: candidate.name,
180
- mediaType: candidate.contentType,
181
- sizeBytes: candidate.sizeBytes,
182
- })
154
+ namespace: string
155
+ relativePath: string
156
+ }) {
157
+ return Effect.gen(function* () {
158
+ const filename = file.name || 'document'
159
+ const mediaType = file.type || inferContentType(filename)
160
+ const storageKey = buildOrganizationDocumentStorageKey({
161
+ orgId,
162
+ namespace,
163
+ relativePath: relativePath.trim().length > 0 ? relativePath : filename,
164
+ })
165
+
166
+ yield* Effect.tryPromise({
167
+ try: () => client.file(storageKey).write(file, { type: mediaType }),
168
+ catch: (error) => toAttachmentStorageError('uploadOrganizationDocument', error),
169
+ })
170
+
171
+ return {
172
+ filename,
173
+ mediaType,
174
+ sizeBytes: file.size,
175
+ storageKey,
176
+ url: getAttachmentUrl(storageKey),
177
+ expiresInSeconds: config.s3.attachmentUrlExpiresIn,
183
178
  }
184
- }
179
+ })
180
+ }
181
+
182
+ function uploadThreadAttachmentEffect({ file, orgId, userId }: { file: File; orgId: string; userId: string }) {
183
+ return Effect.gen(function* () {
184
+ const filename = file.name || 'attachment'
185
+ const mediaType = file.type || inferContentType(filename)
186
+ const sizeBytes = file.size
187
+ const storageKey = buildUploadStorageKey({ orgId, userId, filename, uploadId: Bun.randomUUIDv7() })
188
+
189
+ yield* Effect.tryPromise({
190
+ try: () => client.file(storageKey).write(file, { type: mediaType }),
191
+ catch: (error) => toAttachmentStorageError('uploadThreadAttachment', error),
192
+ })
185
193
 
186
- return [...uploadsByStorageKey.values()].map((upload) => ({
187
- storageKey: upload.storageKey,
188
- filename: upload.filename,
189
- mediaType: upload.mediaType,
190
- sizeBytes: upload.sizeBytes,
191
- }))
194
+ return {
195
+ filename,
196
+ mediaType,
197
+ sizeBytes,
198
+ storageKey,
199
+ url: getAttachmentUrl(storageKey),
200
+ expiresInSeconds: config.s3.attachmentUrlExpiresIn,
201
+ }
202
+ })
192
203
  }
193
204
 
194
- async extractStoredAttachmentText({
205
+ function extractStoredAttachmentTextEffect({
195
206
  storageKey,
196
207
  name,
197
208
  contentType,
@@ -199,18 +210,30 @@ export class AttachmentStorageService {
199
210
  storageKey: string
200
211
  name: string
201
212
  contentType: string
202
- }): Promise<string> {
203
- try {
204
- const s3File = this.client.file(storageKey)
205
- const fileLike = new File([await s3File.arrayBuffer()], name, { type: contentType })
206
- return await extractAttachmentText(fileLike)
207
- } catch (error) {
208
- serverLogger.warn`Failed to extract stored attachment text: ${error}`
209
- return ''
210
- }
213
+ }) {
214
+ return Effect.gen(function* () {
215
+ const s3File = client.file(storageKey)
216
+ const bytes = yield* Effect.tryPromise({
217
+ try: () => s3File.arrayBuffer(),
218
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentText:read', error),
219
+ })
220
+ const fileLike = new File([bytes], name, { type: contentType })
221
+
222
+ return yield* Effect.tryPromise({
223
+ try: () => extractAttachmentText(fileLike),
224
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentText:extract', error),
225
+ })
226
+ }).pipe(
227
+ Effect.catch((error) =>
228
+ Effect.sync(() => {
229
+ serverLogger.warn`Failed to extract stored attachment text: ${error}`
230
+ return ''
231
+ }),
232
+ ),
233
+ )
211
234
  }
212
235
 
213
- async extractStoredAttachmentPages({
236
+ function extractStoredAttachmentPagesEffect({
214
237
  storageKey,
215
238
  name,
216
239
  contentType,
@@ -218,25 +241,39 @@ export class AttachmentStorageService {
218
241
  storageKey: string
219
242
  name: string
220
243
  contentType: string
221
- }): Promise<{ pageMode: ReadableUploadPageMode; pages: string[] }> {
222
- try {
223
- const s3File = this.client.file(storageKey)
224
- const fileLike = new File([await s3File.arrayBuffer()], name, { type: contentType })
244
+ }) {
245
+ return Effect.gen(function* () {
246
+ const s3File = client.file(storageKey)
247
+ const bytes = yield* Effect.tryPromise({
248
+ try: () => s3File.arrayBuffer(),
249
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentPages:read', error),
250
+ })
251
+ const fileLike = new File([bytes], name, { type: contentType })
225
252
 
226
253
  if (isPdfAttachmentFile(fileLike)) {
227
- const pages = await extractPdfPages(fileLike)
228
- return { pageMode: 'pdf', pages }
254
+ const pages = yield* Effect.tryPromise({
255
+ try: () => extractPdfPages(fileLike),
256
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentPages:pdf', error),
257
+ })
258
+ return { pageMode: 'pdf' as const, pages }
229
259
  }
230
260
 
231
- const extractedText = await extractAttachmentText(fileLike)
232
- return { pageMode: 'logical', pages: splitExtractedTextIntoPages(extractedText) }
233
- } catch (error) {
234
- serverLogger.warn`Failed to extract attachment pages from storage: ${error}`
235
- return { pageMode: 'logical', pages: [] }
236
- }
261
+ const extractedText = yield* Effect.tryPromise({
262
+ try: () => extractAttachmentText(fileLike),
263
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentPages:text', error),
264
+ })
265
+ return { pageMode: 'logical' as const, pages: splitExtractedTextIntoPages(extractedText) }
266
+ }).pipe(
267
+ Effect.catch((error) =>
268
+ Effect.sync(() => {
269
+ serverLogger.warn`Failed to extract attachment pages from storage: ${error}`
270
+ return { pageMode: 'logical' as const, pages: [] }
271
+ }),
272
+ ),
273
+ )
237
274
  }
238
275
 
239
- async readFilePartsFromUpload({
276
+ function readFilePartsFromUploadEffect({
240
277
  upload,
241
278
  orgId,
242
279
  userId,
@@ -248,145 +285,167 @@ export class AttachmentStorageService {
248
285
  userId: string
249
286
  part?: number
250
287
  pagesPerPart?: number
251
- }): Promise<{
252
- pageMode: ReadableUploadPageMode
253
- totalPages: number
254
- fullPageLength: number
255
- currentPart: number
256
- totalParts: number
257
- hasNextPart: boolean
258
- hasPreviousPart: boolean
259
- pagesPerPart: number
260
- pageStart: number
261
- pageEnd: number
262
- data: ReadableUploadPagePart[]
263
- }> {
264
- this.assertUploadOwnership({ storageKey: upload.storageKey, orgId, userId })
265
-
266
- const parsed = await this.extractStoredAttachmentPages({
267
- storageKey: upload.storageKey,
268
- name: upload.filename,
269
- contentType: upload.mediaType,
270
- })
271
- const totalPages = parsed.pages.length
272
- if (totalPages === 0) {
273
- return {
274
- pageMode: parsed.pageMode,
275
- totalPages: 0,
276
- fullPageLength: 0,
277
- currentPart: 1,
278
- totalParts: 0,
279
- hasNextPart: false,
280
- hasPreviousPart: false,
281
- pagesPerPart,
282
- pageStart: 1,
283
- pageEnd: 0,
284
- data: [],
288
+ }) {
289
+ return Effect.gen(function* () {
290
+ yield* assertUploadOwnership({ storageKey: upload.storageKey, orgId, userId })
291
+
292
+ const parsed = yield* extractStoredAttachmentPagesEffect({
293
+ storageKey: upload.storageKey,
294
+ name: upload.filename,
295
+ contentType: upload.mediaType,
296
+ })
297
+ const totalPages = parsed.pages.length
298
+ if (totalPages === 0) {
299
+ return {
300
+ pageMode: parsed.pageMode,
301
+ totalPages: 0,
302
+ fullPageLength: 0,
303
+ currentPart: 1,
304
+ totalParts: 0,
305
+ hasNextPart: false,
306
+ hasPreviousPart: false,
307
+ pagesPerPart,
308
+ pageStart: 1,
309
+ pageEnd: 0,
310
+ data: [],
311
+ }
285
312
  }
286
- }
287
-
288
- if (!Number.isInteger(pagesPerPart) || pagesPerPart <= 0) {
289
- throw new Error('pagesPerPart must be a positive integer.')
290
- }
291
- if (!Number.isInteger(part) || part <= 0) {
292
- throw new Error('part must be an integer greater than or equal to 1.')
293
- }
294
313
 
295
- const totalParts = Math.ceil(totalPages / pagesPerPart)
296
- if (part > totalParts) {
297
- throw new Error(`part ${part} exceeds total parts ${totalParts}.`)
298
- }
299
-
300
- const pageStart = (part - 1) * pagesPerPart + 1
301
- const pageEnd = Math.min(pageStart + pagesPerPart - 1, totalPages)
302
-
303
- const data = parsed.pages
304
- .slice(pageStart - 1, pageEnd)
305
- .map((text, index) => ({ pageNumber: pageStart + index, text, charCount: text.length }))
306
-
307
- return {
308
- pageMode: parsed.pageMode,
309
- totalPages,
310
- fullPageLength: totalPages,
311
- currentPart: part,
312
- totalParts,
313
- hasNextPart: part < totalParts,
314
- hasPreviousPart: part > 1,
315
- pagesPerPart,
316
- pageStart,
317
- pageEnd,
318
- data,
319
- }
320
- }
314
+ if (!Number.isInteger(pagesPerPart) || pagesPerPart <= 0) {
315
+ return yield* new BadRequestError({ message: 'pagesPerPart must be a positive integer.' })
316
+ }
317
+ if (!Number.isInteger(part) || part <= 0) {
318
+ return yield* new BadRequestError({ message: 'part must be an integer greater than or equal to 1.' })
319
+ }
321
320
 
322
- private assertUploadOwnership({
323
- storageKey,
324
- orgId,
325
- userId,
326
- }: {
327
- storageKey: string
328
- orgId: string
329
- userId: string
330
- }): void {
331
- const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
332
- if (!storageKey.startsWith(storagePrefix)) {
333
- throw new Error('Upload does not belong to the current organization/user scope.')
334
- }
335
- }
321
+ const totalParts = Math.ceil(totalPages / pagesPerPart)
322
+ if (part > totalParts) {
323
+ return yield* new BadRequestError({ message: `part ${part} exceeds total parts ${totalParts}.` })
324
+ }
336
325
 
337
- private collectAttachmentContextCandidates({
338
- parts,
339
- orgId,
340
- userId,
341
- }: {
342
- parts: readonly MessagePartLike[]
343
- orgId: string
344
- userId: string
345
- }): AttachmentContextCandidate[] {
346
- const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
347
- const candidates: AttachmentContextCandidate[] = []
326
+ const pageStart = (part - 1) * pagesPerPart + 1
327
+ const pageEnd = Math.min(pageStart + pagesPerPart - 1, totalPages)
348
328
 
349
- for (const part of parts) {
350
- if (part.type !== 'file') continue
329
+ const data = parsed.pages
330
+ .slice(pageStart - 1, pageEnd)
331
+ .map((text, index) => ({ pageNumber: pageStart + index, text, charCount: text.length }))
351
332
 
352
- const providerMetadata = readRecord(part.providerMetadata)
353
- const lotaMetadata = readRecord(providerMetadata?.lota)
354
- const storageKey = readString(lotaMetadata?.attachmentStorageKey)
355
- if (!storageKey || !storageKey.startsWith(storagePrefix)) continue
333
+ return {
334
+ pageMode: parsed.pageMode,
335
+ totalPages,
336
+ fullPageLength: totalPages,
337
+ currentPart: part,
338
+ totalParts,
339
+ hasNextPart: part < totalParts,
340
+ hasPreviousPart: part > 1,
341
+ pagesPerPart,
342
+ pageStart,
343
+ pageEnd,
344
+ data,
345
+ }
346
+ })
347
+ }
356
348
 
357
- const name = readString(part.filename) ?? this.readFilenameFromStorageKey(storageKey) ?? 'Attachment'
358
- const contentType = readString(part.mediaType) ?? inferContentType(name)
359
- const sizeBytes = readNonNegativeInteger(lotaMetadata?.attachmentSizeBytes)
360
- candidates.push({ storageKey, name, contentType, sizeBytes })
361
- }
349
+ return {
350
+ getAttachmentUrl,
362
351
 
363
- return candidates
364
- }
352
+ writeOrganizationDocument: (params: {
353
+ orgId: string
354
+ namespace: string
355
+ relativePath: string
356
+ content: string
357
+ contentType: string
358
+ }) => writeOrganizationDocumentEffect(params),
365
359
 
366
- private readFilenameFromStorageKey(storageKey: string): string | null {
367
- const slashIndex = storageKey.lastIndexOf('/')
368
- if (slashIndex < 0) return null
369
- const rawName = storageKey.slice(slashIndex + 1).trim()
370
- if (!rawName) return null
360
+ uploadOrganizationDocument: (params: { file: File; orgId: string; namespace: string; relativePath: string }) =>
361
+ uploadOrganizationDocumentEffect(params),
371
362
 
372
- const dashIndex = rawName.indexOf('-')
373
- if (dashIndex <= 0 || dashIndex === rawName.length - 1) return rawName
374
- return rawName.slice(dashIndex + 1)
375
- }
376
- }
363
+ uploadThreadAttachment: (params: { file: File; orgId: string; userId: string }) =>
364
+ uploadThreadAttachmentEffect(params),
377
365
 
378
- let _attachmentStorageService: AttachmentStorageService | undefined
366
+ hydrateSignedFileUrlsInMessageParts<TPart extends MessagePartLike>({
367
+ parts,
368
+ orgId,
369
+ userId,
370
+ }: {
371
+ parts: readonly TPart[]
372
+ orgId: string
373
+ userId: string
374
+ }): TPart[] {
375
+ const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
376
+
377
+ return parts.map<TPart>((part) => {
378
+ if (part.type !== 'file') return part
379
+
380
+ const providerMetadata = readRecord(part.providerMetadata)
381
+ const lotaMetadata = readRecord(providerMetadata?.lota)
382
+ const storageKey = lotaMetadata?.attachmentStorageKey
383
+ if (typeof storageKey !== 'string' || !storageKey.startsWith(storagePrefix)) return part
384
+
385
+ return { ...part, url: getAttachmentUrl(storageKey) }
386
+ })
387
+ },
388
+
389
+ listReadableUploadsFromMessages<TPart extends MessagePartLike>({
390
+ messages,
391
+ orgId,
392
+ userId,
393
+ }: {
394
+ messages: ReadonlyArray<{ parts: readonly TPart[] }>
395
+ orgId: string
396
+ userId: string
397
+ }): ReadableUploadMetadata[] {
398
+ const uploadsByStorageKey = new Map<
399
+ string,
400
+ { storageKey: string; filename: string; mediaType: string; sizeBytes: number | null }
401
+ >()
402
+
403
+ for (const message of messages) {
404
+ const candidates = collectAttachmentContextCandidates({ parts: message.parts, orgId, userId })
405
+ for (const candidate of candidates) {
406
+ if (uploadsByStorageKey.has(candidate.storageKey)) continue
407
+ uploadsByStorageKey.set(candidate.storageKey, {
408
+ storageKey: candidate.storageKey,
409
+ filename: candidate.name,
410
+ mediaType: candidate.contentType,
411
+ sizeBytes: candidate.sizeBytes,
412
+ })
413
+ }
414
+ }
379
415
 
380
- /** @lintignore */
381
- export function getAttachmentStorageService(): AttachmentStorageService {
382
- if (!_attachmentStorageService) {
383
- _attachmentStorageService = new AttachmentStorageService()
416
+ return [...uploadsByStorageKey.values()].map((upload) => ({
417
+ storageKey: upload.storageKey,
418
+ filename: upload.filename,
419
+ mediaType: upload.mediaType,
420
+ sizeBytes: upload.sizeBytes,
421
+ }))
422
+ },
423
+
424
+ extractStoredAttachmentText: (params: { storageKey: string; name: string; contentType: string }) =>
425
+ extractStoredAttachmentTextEffect(params),
426
+
427
+ extractStoredAttachmentPages: (params: { storageKey: string; name: string; contentType: string }) =>
428
+ extractStoredAttachmentPagesEffect(params),
429
+
430
+ readFilePartsFromUpload: (params: {
431
+ upload: ReadableUploadMetadata
432
+ orgId: string
433
+ userId: string
434
+ part?: number
435
+ pagesPerPart?: number
436
+ }) => readFilePartsFromUploadEffect(params),
384
437
  }
385
- return _attachmentStorageService
386
438
  }
387
439
 
388
- export const attachmentStorageService = new Proxy({} as AttachmentStorageService, {
389
- get(_target, prop: string): unknown {
390
- return Reflect.get(getAttachmentStorageService(), prop)
391
- },
392
- })
440
+ export class AttachmentStorageServiceTag extends Context.Service<
441
+ AttachmentStorageServiceTag,
442
+ ReturnType<typeof makeAttachmentStorageService>
443
+ >()('@lota-sdk/core/AttachmentStorageService') {}
444
+
445
+ export const AttachmentStorageServiceLive = Layer.effect(
446
+ AttachmentStorageServiceTag,
447
+ Effect.gen(function* () {
448
+ const config = yield* RuntimeConfigServiceTag
449
+ return makeAttachmentStorageService(config)
450
+ }),
451
+ )