@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,13 +1,58 @@
1
+ import { normalizeMessageBatch } from '@lota-sdk/shared'
2
+ import { Context, Effect, Layer, Schema } from 'effect'
1
3
  import { z } from 'zod'
2
4
 
3
- import { getRedisConnection } from '../redis'
5
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
6
+ import { RedisServiceTag, RuntimeConfigServiceTag } from '../effect/services'
7
+ import type { RedisConnectionManager } from '../redis/connection'
8
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
4
9
  import type { LotaRuntimeBackgroundCursor, LotaRuntimeBackgroundCursorKind } from '../runtime/runtime-extensions'
5
10
 
11
+ export class SocialChatHistoryError extends Schema.TaggedErrorClass<SocialChatHistoryError>()(
12
+ 'SocialChatHistoryError',
13
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
14
+ ) {}
15
+
16
+ const tryRedisPromise = makeEffectTryPromiseWithMessage(
17
+ (message, cause) => new SocialChatHistoryError({ message, cause }),
18
+ )
19
+
6
20
  const DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX = 'lota:social:history'
7
21
 
8
22
  const SocialChatMessageRoleSchema = z.enum(['user', 'assistant'])
9
23
  const SocialChatSourceSchema = z.literal('social')
10
24
  const SocialChatPlatformSchema = z.literal('slack')
25
+ const SocialChatHistoryGenericPartSchema = z
26
+ .object({ type: z.string().optional(), text: z.string().optional() })
27
+ .catchall(z.unknown())
28
+ const SocialChatHistoryTextPartSchema = SocialChatHistoryGenericPartSchema.extend({
29
+ type: z.literal('text'),
30
+ text: z.string().trim().min(1),
31
+ })
32
+ const SocialChatHistoryFilePartSchema = SocialChatHistoryGenericPartSchema.extend({
33
+ type: z.literal('file'),
34
+ filename: z.string().trim().min(1),
35
+ mediaType: z.string().trim().min(1),
36
+ sizeBytes: z.number().int().nonnegative().nullable(),
37
+ storageKey: z.string().trim().min(1),
38
+ })
39
+ const SocialChatHistoryPartSchema = z.union([
40
+ SocialChatHistoryTextPartSchema,
41
+ SocialChatHistoryFilePartSchema,
42
+ SocialChatHistoryGenericPartSchema,
43
+ ])
44
+ const SocialChatHistoryMetadataSchema = z
45
+ .object({
46
+ platform: SocialChatPlatformSchema,
47
+ channelId: z.string().trim().min(1),
48
+ threadId: z.string().trim().min(1),
49
+ messageId: z.string().trim().min(1),
50
+ authorId: z.string().trim().min(1).optional(),
51
+ authorName: z.string().trim().min(1).optional(),
52
+ agentId: z.string().trim().min(1).optional(),
53
+ agentName: z.string().trim().min(1).optional(),
54
+ })
55
+ .catchall(z.unknown())
11
56
 
12
57
  const SocialChatHistoryMessageSchema = z.object({
13
58
  source: SocialChatSourceSchema,
@@ -18,15 +63,17 @@ const SocialChatHistoryMessageSchema = z.object({
18
63
  threadId: z.string().trim().min(1),
19
64
  messageId: z.string().trim().min(1),
20
65
  role: SocialChatMessageRoleSchema,
21
- parts: z.array(z.record(z.string(), z.unknown())),
22
- metadata: z.record(z.string(), z.unknown()).optional(),
66
+ parts: z.array(SocialChatHistoryPartSchema),
67
+ metadata: SocialChatHistoryMetadataSchema.optional(),
23
68
  cursor: z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }),
24
69
  })
70
+ const SocialChatHistoryCursorSchema = z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) })
71
+ const decodeJsonStringEffect = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)
25
72
 
73
+ export type SocialChatHistoryPart = z.infer<typeof SocialChatHistoryPartSchema>
74
+ export type SocialChatHistoryMetadata = z.infer<typeof SocialChatHistoryMetadataSchema>
26
75
  export type SocialChatHistoryMessage = z.infer<typeof SocialChatHistoryMessageSchema>
27
76
 
28
- let socialChatHistoryPrefix = DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
29
-
30
77
  function trimConfiguredPrefix(value: string | undefined): string {
31
78
  const normalized = value?.trim()
32
79
  return normalized && normalized.length > 0 ? normalized : DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
@@ -38,160 +85,181 @@ function compareCursorOrder(left: LotaRuntimeBackgroundCursor, right: LotaRuntim
38
85
  return left.id.localeCompare(right.id)
39
86
  }
40
87
 
41
- class SocialChatHistoryService {
42
- configure(params?: { keyPrefix?: string }): void {
43
- socialChatHistoryPrefix = trimConfiguredPrefix(params?.keyPrefix)
44
- }
88
+ export function makeSocialChatHistoryService(
89
+ redis: RedisConnectionManager,
90
+ config: ResolvedLotaRuntimeConfig | undefined,
91
+ ) {
92
+ const resolvePrefix = (): string => trimConfiguredPrefix(config?.socialChat?.historyRedisKeyPrefix)
45
93
 
46
- private messageStorageKey(message: {
94
+ const messageStorageKey = (message: {
47
95
  platform: 'slack'
48
96
  workspaceId: string
49
97
  threadId: string
50
98
  messageId: string
51
- }): string {
52
- return `${socialChatHistoryPrefix}:message:${message.platform}:${message.workspaceId}:${message.threadId}:${message.messageId}`
53
- }
99
+ }): string =>
100
+ `${resolvePrefix()}:message:${message.platform}:${message.workspaceId}:${message.threadId}:${message.messageId}`
54
101
 
55
- private threadIndexKey(workspaceId: string, threadId: string): string {
56
- return `${socialChatHistoryPrefix}:thread:${workspaceId}:${threadId}`
57
- }
102
+ const threadIndexKey = (workspaceId: string, threadId: string): string =>
103
+ `${resolvePrefix()}:thread:${workspaceId}:${threadId}`
58
104
 
59
- private workspaceIndexKey(workspaceId: string): string {
60
- return `${socialChatHistoryPrefix}:workspace:${workspaceId}`
61
- }
105
+ const workspaceIndexKey = (workspaceId: string): string => `${resolvePrefix()}:workspace:${workspaceId}`
62
106
 
63
- private cursorKey(kind: LotaRuntimeBackgroundCursorKind, workspaceId: string): string {
64
- return `${socialChatHistoryPrefix}:cursor:${kind}:${workspaceId}`
65
- }
107
+ const cursorKey = (kind: LotaRuntimeBackgroundCursorKind, workspaceId: string): string =>
108
+ `${resolvePrefix()}:cursor:${kind}:${workspaceId}`
66
109
 
67
- private serializeMessage(message: SocialChatHistoryMessage): string {
68
- return JSON.stringify({
69
- ...message,
70
- cursor: { ...message.cursor, createdAt: message.cursor.createdAt.toISOString() },
71
- })
72
- }
110
+ const serializeMessage = (message: SocialChatHistoryMessage): string =>
111
+ JSON.stringify({ ...message, cursor: { ...message.cursor, createdAt: message.cursor.createdAt.toISOString() } })
73
112
 
74
- private parseStoredMessage(value: string | null): SocialChatHistoryMessage | null {
75
- if (!value) return null
76
- try {
77
- const parsed = SocialChatHistoryMessageSchema.safeParse(JSON.parse(value))
78
- return parsed.success ? parsed.data : null
79
- } catch {
80
- return null
81
- }
82
- }
113
+ const parseStoredMessageEffect = (value: string | null): Effect.Effect<SocialChatHistoryMessage | null> => {
114
+ if (!value) return Effect.succeed(null)
83
115
 
84
- private serializeCursor(cursor: LotaRuntimeBackgroundCursor): string {
85
- return JSON.stringify({ ...cursor, createdAt: cursor.createdAt.toISOString() })
116
+ return decodeJsonStringEffect(value).pipe(
117
+ Effect.map((parsed) => {
118
+ const validated = SocialChatHistoryMessageSchema.safeParse(parsed)
119
+ return validated.success ? validated.data : null
120
+ }),
121
+ Effect.catch(() => Effect.succeed(null)),
122
+ )
86
123
  }
87
124
 
88
- private parseCursor(value: string | null): LotaRuntimeBackgroundCursor | null {
89
- if (!value) return null
90
- try {
91
- const parsed = z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }).safeParse(JSON.parse(value))
92
- return parsed.success ? parsed.data : null
93
- } catch {
94
- return null
95
- }
125
+ const serializeCursor = (cursor: LotaRuntimeBackgroundCursor): string =>
126
+ JSON.stringify({ ...cursor, createdAt: cursor.createdAt.toISOString() })
127
+
128
+ const parseCursorEffect = (value: string | null): Effect.Effect<LotaRuntimeBackgroundCursor | null> => {
129
+ if (!value) return Effect.succeed(null)
130
+
131
+ return decodeJsonStringEffect(value).pipe(
132
+ Effect.map((parsed) => {
133
+ const validated = SocialChatHistoryCursorSchema.safeParse(parsed)
134
+ return validated.success ? validated.data : null
135
+ }),
136
+ Effect.catch(() => Effect.succeed(null)),
137
+ )
96
138
  }
97
139
 
98
- async upsertMessages(messages: SocialChatHistoryMessage[]): Promise<SocialChatHistoryMessage[]> {
99
- if (messages.length === 0) return []
140
+ const upsertMessagesEffect = (messages: SocialChatHistoryMessage[]) =>
141
+ Effect.gen(function* () {
142
+ if (messages.length === 0) return [] as SocialChatHistoryMessage[]
100
143
 
101
- const redis = getRedisConnection()
102
- const normalizedMessages = messages.map((message) => SocialChatHistoryMessageSchema.parse(message))
103
- const multi = redis.multi()
144
+ const conn = redis.getConnection()
145
+ const normalizedMessages = normalizeMessageBatch(messages, {
146
+ parseMessage: (message) => SocialChatHistoryMessageSchema.parse(message),
147
+ compareMessages: (left, right) => compareCursorOrder(left.cursor, right.cursor),
148
+ getMessageId: (message) => message.messageId,
149
+ })
150
+ const multi = conn.multi()
104
151
 
105
- for (const message of normalizedMessages) {
106
- const storageKey = this.messageStorageKey(message)
107
- const score = message.cursor.createdAt.getTime()
108
- multi.set(storageKey, this.serializeMessage(message))
109
- multi.zadd(this.threadIndexKey(message.workspaceId, message.threadId), score, storageKey)
110
- multi.zadd(this.workspaceIndexKey(message.workspaceId), score, storageKey)
111
- }
152
+ for (const message of normalizedMessages) {
153
+ const storageKey = messageStorageKey(message)
154
+ const score = message.cursor.createdAt.getTime()
155
+ multi.set(storageKey, serializeMessage(message))
156
+ multi.zadd(threadIndexKey(message.workspaceId, message.threadId), score, storageKey)
157
+ multi.zadd(workspaceIndexKey(message.workspaceId), score, storageKey)
158
+ }
112
159
 
113
- await multi.exec()
114
- return normalizedMessages
115
- }
160
+ yield* tryRedisPromise(() => multi.exec(), 'Failed to upsert social chat messages.')
161
+ return normalizedMessages
162
+ })
116
163
 
117
- async listThreadMessages(params: { workspaceId: string; threadId: string }): Promise<SocialChatHistoryMessage[]> {
118
- const redis = getRedisConnection()
119
- const storageKeys = await redis.zrange(this.threadIndexKey(params.workspaceId, params.threadId), 0, -1)
120
- if (storageKeys.length === 0) return []
164
+ const listThreadMessagesEffect = (params: { workspaceId: string; threadId: string }) =>
165
+ Effect.gen(function* () {
166
+ const conn = redis.getConnection()
167
+ const storageKeys = yield* tryRedisPromise(
168
+ () => conn.zrange(threadIndexKey(params.workspaceId, params.threadId), 0, -1),
169
+ 'Failed to list thread message keys.',
170
+ )
171
+ if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
121
172
 
122
- const storedValues = await redis.mget(storageKeys)
123
- return storedValues
124
- .map((value) => this.parseStoredMessage(value))
125
- .filter((message): message is SocialChatHistoryMessage => message !== null)
126
- .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
127
- }
173
+ const storedValues = yield* tryRedisPromise(() => conn.mget(storageKeys), 'Failed to read thread messages.')
174
+ const parsedMessages = yield* Effect.forEach(storedValues, parseStoredMessageEffect)
175
+ return parsedMessages
176
+ .filter((message): message is SocialChatHistoryMessage => message !== null)
177
+ .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
178
+ })
128
179
 
129
- async listWorkspaceMessages(params: {
180
+ const listWorkspaceMessagesEffect = (params: {
130
181
  workspaceId: string
131
182
  cursor: LotaRuntimeBackgroundCursor | null
132
183
  onboardingCutoff: Date | null
133
- }): Promise<SocialChatHistoryMessage[]> {
134
- const redis = getRedisConnection()
135
- const indexKey = this.workspaceIndexKey(params.workspaceId)
136
- const scoreStart =
137
- params.cursor?.createdAt.getTime() ??
138
- (params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
139
- const storageKeys =
140
- params.cursor || params.onboardingCutoff
141
- ? await redis.zrangebyscore(indexKey, scoreStart, '+inf')
142
- : await redis.zrange(indexKey, 0, -1)
143
-
144
- if (storageKeys.length === 0) return []
145
-
146
- const storedValues = await redis.mget(storageKeys)
147
- return storedValues
148
- .map((value) => this.parseStoredMessage(value))
149
- .filter((message): message is SocialChatHistoryMessage => message !== null)
150
- .filter((message) => {
151
- if (params.cursor) {
152
- return compareCursorOrder(message.cursor, params.cursor) > 0
153
- }
154
- if (params.onboardingCutoff) {
155
- return message.cursor.createdAt.getTime() > params.onboardingCutoff.getTime()
156
- }
157
- return true
158
- })
159
- .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
160
- }
184
+ }) =>
185
+ Effect.gen(function* () {
186
+ const conn = redis.getConnection()
187
+ const indexKey = workspaceIndexKey(params.workspaceId)
188
+ const scoreStart =
189
+ params.cursor?.createdAt.getTime() ??
190
+ (params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
191
+ const storageKeys = yield* tryRedisPromise(
192
+ () =>
193
+ params.cursor || params.onboardingCutoff
194
+ ? conn.zrangebyscore(indexKey, scoreStart, '+inf')
195
+ : conn.zrange(indexKey, 0, -1),
196
+ 'Failed to list workspace message keys.',
197
+ )
198
+ if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
199
+
200
+ const storedValues = yield* tryRedisPromise(() => conn.mget(storageKeys), 'Failed to read workspace messages.')
201
+ const parsedMessages = yield* Effect.forEach(storedValues, parseStoredMessageEffect)
202
+ return parsedMessages
203
+ .filter((message): message is SocialChatHistoryMessage => message !== null)
204
+ .filter((message) => {
205
+ if (params.cursor) {
206
+ return compareCursorOrder(message.cursor, params.cursor) > 0
207
+ }
208
+ if (params.onboardingCutoff) {
209
+ return message.cursor.createdAt.getTime() > params.onboardingCutoff.getTime()
210
+ }
211
+ return true
212
+ })
213
+ .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
214
+ })
161
215
 
162
- async hasWorkspaceMessages(params: {
216
+ const hasWorkspaceMessagesEffect = (params: {
163
217
  workspaceId: string
164
218
  cursor: LotaRuntimeBackgroundCursor | null
165
219
  onboardingCutoff: Date | null
166
- }): Promise<boolean> {
167
- const messages = await this.listWorkspaceMessages({
220
+ }) =>
221
+ listWorkspaceMessagesEffect({
168
222
  workspaceId: params.workspaceId,
169
223
  cursor: params.cursor,
170
224
  onboardingCutoff: params.onboardingCutoff,
171
- })
172
- return messages.length > 0
173
- }
225
+ }).pipe(Effect.map((messages) => messages.length > 0))
174
226
 
175
- async getBackgroundCursor(
176
- kind: LotaRuntimeBackgroundCursorKind,
177
- workspaceId: string,
178
- ): Promise<LotaRuntimeBackgroundCursor | null> {
179
- const redis = getRedisConnection()
180
- return this.parseCursor(await redis.get(this.cursorKey(kind, workspaceId)))
181
- }
227
+ const getBackgroundCursorEffect = (kind: LotaRuntimeBackgroundCursorKind, workspaceId: string) =>
228
+ tryRedisPromise(
229
+ () => redis.getConnection().get(cursorKey(kind, workspaceId)),
230
+ 'Failed to read background cursor.',
231
+ ).pipe(Effect.flatMap((value) => parseCursorEffect(value)))
182
232
 
183
- async setBackgroundCursor(
233
+ const setBackgroundCursorEffect = (
184
234
  kind: LotaRuntimeBackgroundCursorKind,
185
235
  workspaceId: string,
186
236
  cursor: LotaRuntimeBackgroundCursor,
187
- ): Promise<void> {
188
- const redis = getRedisConnection()
189
- await redis.set(this.cursorKey(kind, workspaceId), this.serializeCursor(cursor))
237
+ ) =>
238
+ tryRedisPromise(
239
+ () => redis.getConnection().set(cursorKey(kind, workspaceId), serializeCursor(cursor)),
240
+ 'Failed to write background cursor.',
241
+ ).pipe(Effect.asVoid)
242
+
243
+ return {
244
+ upsertMessages: upsertMessagesEffect,
245
+ listThreadMessages: listThreadMessagesEffect,
246
+ listWorkspaceMessages: listWorkspaceMessagesEffect,
247
+ hasWorkspaceMessages: hasWorkspaceMessagesEffect,
248
+ getBackgroundCursor: getBackgroundCursorEffect,
249
+ setBackgroundCursor: setBackgroundCursorEffect,
190
250
  }
191
251
  }
192
252
 
193
- export const socialChatHistoryService = new SocialChatHistoryService()
253
+ export class SocialChatHistoryServiceTag extends Context.Service<
254
+ SocialChatHistoryServiceTag,
255
+ ReturnType<typeof makeSocialChatHistoryService>
256
+ >()('@lota-sdk/core/SocialChatHistoryService') {}
194
257
 
195
- export function configureSocialChatHistory(params?: { keyPrefix?: string }): void {
196
- socialChatHistoryService.configure(params)
197
- }
258
+ export const SocialChatHistoryServiceLive = Layer.effect(
259
+ SocialChatHistoryServiceTag,
260
+ Effect.gen(function* () {
261
+ const redis = yield* RedisServiceTag
262
+ const config = yield* RuntimeConfigServiceTag
263
+ return makeSocialChatHistoryService(redis, config)
264
+ }),
265
+ )
@@ -1,27 +1,38 @@
1
1
  import type { OwnershipDispatchContext, PlanNodeResult, PlanNodeSpec, SystemPlanNodeOwner } from '@lota-sdk/shared'
2
+ import { Context, Effect, Layer } from 'effect'
2
3
 
4
+ import { BadRequestError, ServiceError } from '../effect/errors'
5
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
6
+ import { RuntimeConfigServiceTag } from '../effect/services'
7
+
8
+ const trySystemExecutorPromise = makeEffectTryPromiseWithMessage(
9
+ (message, cause) => new ServiceError({ message, cause }),
10
+ )
3
11
  import type { SystemNodeExecutor, PluginNodeExecutionParams } from '../runtime/plugin-types'
4
- import { getRuntimeConfig } from '../runtime/runtime-config'
5
- import type { PlanValidationIssueInput } from './plan-validator.service'
12
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
13
+ import type { PlanValidationIssueInput } from './plan/plan-validator.service'
6
14
 
7
15
  const BUILT_IN_SYSTEM_EXECUTORS = Object.freeze({
8
16
  'plan-runtime': {
9
17
  supportedOperations: ['echo-input'],
10
- async executeNode(params: PluginNodeExecutionParams): Promise<PlanNodeResult> {
11
- return { notes: 'System echo-input completed.', structuredOutput: structuredClone(params.inputs), artifacts: [] }
18
+ executeNode(params: PluginNodeExecutionParams): Promise<PlanNodeResult> {
19
+ return Promise.resolve({
20
+ notes: 'System echo-input completed.',
21
+ structuredOutput: structuredClone(params.inputs),
22
+ artifacts: [],
23
+ })
12
24
  },
13
25
  } satisfies SystemNodeExecutor,
14
26
  })
15
27
 
16
- export function getBuiltInSystemExecutors(): Record<string, SystemNodeExecutor> {
17
- return Object.fromEntries(Object.entries(BUILT_IN_SYSTEM_EXECUTORS))
28
+ function isSystemOwner(
29
+ owner: PlanNodeSpec['owner'],
30
+ ): owner is Extract<PlanNodeSpec['owner'], { executorType: 'system' }> {
31
+ return owner.executorType === 'system'
18
32
  }
19
33
 
20
- function getSystemExecutors() {
21
- return (getRuntimeConfig().systemExecutors ?? getBuiltInSystemExecutors()) as Record<
22
- string,
23
- SystemNodeExecutor | undefined
24
- >
34
+ export function getBuiltInSystemExecutors(): Record<string, SystemNodeExecutor> {
35
+ return Object.fromEntries(Object.entries(BUILT_IN_SYSTEM_EXECUTORS))
25
36
  }
26
37
 
27
38
  function buildSystemExecutionParams(params: {
@@ -45,61 +56,85 @@ function buildSystemExecutionParams(params: {
45
56
  }
46
57
  }
47
58
 
48
- class SystemExecutorService {
49
- validateOwner(owner: SystemPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
50
- const executor = getSystemExecutors()[owner.ref]
51
- if (!executor) {
52
- return [
53
- {
54
- severity: 'blocking',
55
- code: 'system_executor_missing',
56
- message: `Node "${nodeId}" references unknown system executor "${owner.ref}".`,
57
- nodeId,
58
- detail: { systemRef: owner.ref },
59
- },
60
- ]
61
- }
59
+ export function makeSystemExecutorService(config: ResolvedLotaRuntimeConfig) {
60
+ function getSystemExecutors() {
61
+ return (config.systemExecutors ?? getBuiltInSystemExecutors()) as Record<string, SystemNodeExecutor | undefined>
62
+ }
62
63
 
63
- if (!executor.supportedOperations.includes(owner.operation)) {
64
- return [
65
- {
66
- severity: 'blocking',
67
- code: 'system_operation_missing',
68
- message: `System executor "${owner.ref}" does not support operation "${owner.operation}".`,
69
- nodeId,
70
- detail: { systemRef: owner.ref, operation: owner.operation },
71
- },
72
- ]
73
- }
64
+ return {
65
+ validateOwner(owner: SystemPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
66
+ const executor = getSystemExecutors()[owner.ref]
67
+ if (!executor) {
68
+ return [
69
+ {
70
+ severity: 'blocking',
71
+ code: 'system_executor_missing',
72
+ message: `Node "${nodeId}" references unknown system executor "${owner.ref}".`,
73
+ nodeId,
74
+ detail: { systemRef: owner.ref },
75
+ },
76
+ ]
77
+ }
74
78
 
75
- return []
76
- }
79
+ if (!executor.supportedOperations.includes(owner.operation)) {
80
+ return [
81
+ {
82
+ severity: 'blocking',
83
+ code: 'system_operation_missing',
84
+ message: `System executor "${owner.ref}" does not support operation "${owner.operation}".`,
85
+ nodeId,
86
+ detail: { systemRef: owner.ref, operation: owner.operation },
87
+ },
88
+ ]
89
+ }
77
90
 
78
- async executeNode(params: {
79
- nodeSpec: PlanNodeSpec
80
- resolvedInput: Record<string, unknown>
81
- context: OwnershipDispatchContext
82
- }): Promise<PlanNodeResult> {
83
- if (params.nodeSpec.owner.executorType !== 'system') {
84
- throw new Error(`SystemExecutor cannot execute owner type "${params.nodeSpec.owner.executorType}".`)
85
- }
91
+ return []
92
+ },
86
93
 
87
- const executor = getSystemExecutors()[params.nodeSpec.owner.ref]
88
- if (!executor || !executor.supportedOperations.includes(params.nodeSpec.owner.operation)) {
89
- throw new Error(
90
- `System executor ${params.nodeSpec.owner.ref}.${params.nodeSpec.owner.operation} is not registered.`,
91
- )
92
- }
94
+ executeNode: Effect.fn('SystemExecutor.executeNode')(function* (params: {
95
+ nodeSpec: PlanNodeSpec
96
+ resolvedInput: Record<string, unknown>
97
+ context: OwnershipDispatchContext
98
+ }) {
99
+ const owner = params.nodeSpec.owner
100
+ if (!isSystemOwner(owner)) {
101
+ return yield* new BadRequestError({
102
+ message: `SystemExecutor cannot execute owner type "${owner.executorType}".`,
103
+ })
104
+ }
93
105
 
94
- return executor.executeNode(
95
- buildSystemExecutionParams({
96
- owner: params.nodeSpec.owner,
97
- nodeSpec: params.nodeSpec,
98
- resolvedInput: params.resolvedInput,
99
- context: params.context,
100
- }),
101
- )
106
+ const executor = getSystemExecutors()[owner.ref]
107
+ if (!executor || !executor.supportedOperations.includes(owner.operation)) {
108
+ return yield* new BadRequestError({
109
+ message: `System executor ${owner.ref}.${owner.operation} is not registered.`,
110
+ })
111
+ }
112
+
113
+ return yield* trySystemExecutorPromise(
114
+ () =>
115
+ executor.executeNode(
116
+ buildSystemExecutionParams({
117
+ owner,
118
+ nodeSpec: params.nodeSpec,
119
+ resolvedInput: params.resolvedInput,
120
+ context: params.context,
121
+ }),
122
+ ),
123
+ `System executor "${owner.ref}.${owner.operation}" failed.`,
124
+ ).pipe(Effect.withSpan('SystemExecutor.invoke'))
125
+ }),
102
126
  }
103
127
  }
104
128
 
105
- export const systemExecutorService = new SystemExecutorService()
129
+ export class SystemExecutorServiceTag extends Context.Service<
130
+ SystemExecutorServiceTag,
131
+ ReturnType<typeof makeSystemExecutorService>
132
+ >()('@lota-sdk/core/SystemExecutorService') {}
133
+
134
+ export const SystemExecutorServiceLive = Layer.effect(
135
+ SystemExecutorServiceTag,
136
+ Effect.gen(function* () {
137
+ const runtimeConfig = yield* RuntimeConfigServiceTag
138
+ return makeSystemExecutorService(runtimeConfig)
139
+ }),
140
+ )