@lota-sdk/core 0.4.8 → 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/thread-defaults.ts +33 -21
  9. package/src/create-runtime.ts +725 -387
  10. package/src/db/base.service.ts +52 -28
  11. package/src/db/cursor-pagination.ts +71 -30
  12. package/src/db/memory-store.helpers.ts +4 -7
  13. package/src/db/memory-store.ts +856 -598
  14. package/src/db/memory.ts +398 -275
  15. package/src/db/record-id.ts +32 -10
  16. package/src/db/schema-fingerprint.ts +30 -12
  17. package/src/db/service-normalization.ts +255 -0
  18. package/src/db/service.ts +726 -761
  19. package/src/db/startup.ts +140 -66
  20. package/src/db/transaction-conflict.ts +15 -0
  21. package/src/effect/awaitable-effect.ts +87 -0
  22. package/src/effect/errors.ts +121 -0
  23. package/src/effect/helpers.ts +98 -0
  24. package/src/effect/index.ts +22 -0
  25. package/src/effect/layers.ts +228 -0
  26. package/src/effect/runtime-ref.ts +25 -0
  27. package/src/effect/runtime.ts +31 -0
  28. package/src/effect/services.ts +57 -0
  29. package/src/effect/zod.ts +43 -0
  30. package/src/embeddings/provider.ts +122 -76
  31. package/src/index.ts +46 -1
  32. package/src/openrouter/direct-provider.ts +11 -35
  33. package/src/queues/autonomous-job.queue.ts +130 -74
  34. package/src/queues/context-compaction.queue.ts +60 -15
  35. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  36. package/src/queues/document-processor.queue.ts +52 -77
  37. package/src/queues/memory-consolidation.queue.ts +47 -32
  38. package/src/queues/organization-learning.queue.ts +13 -4
  39. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  40. package/src/queues/plan-scheduler.queue.ts +107 -31
  41. package/src/queues/post-chat-memory.queue.ts +66 -24
  42. package/src/queues/queue-factory.ts +142 -52
  43. package/src/queues/standalone-worker.ts +39 -0
  44. package/src/queues/title-generation.queue.ts +54 -9
  45. package/src/redis/connection.ts +84 -32
  46. package/src/redis/index.ts +6 -8
  47. package/src/redis/org-memory-lock.ts +60 -27
  48. package/src/redis/redis-lease-lock.ts +200 -121
  49. package/src/redis/runtime-connection.ts +10 -0
  50. package/src/redis/stream-context.ts +84 -46
  51. package/src/runtime/agent-identity-overrides.ts +2 -2
  52. package/src/runtime/agent-runtime-policy.ts +4 -1
  53. package/src/runtime/agent-stream-helpers.ts +20 -9
  54. package/src/runtime/chat-run-orchestration.ts +102 -19
  55. package/src/runtime/chat-run-registry.ts +36 -2
  56. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  57. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  58. package/src/runtime/execution-plan-visibility.ts +2 -2
  59. package/src/runtime/execution-plan.ts +42 -15
  60. package/src/runtime/graph-designer.ts +11 -7
  61. package/src/runtime/helper-model.ts +135 -48
  62. package/src/runtime/index.ts +7 -7
  63. package/src/runtime/indexed-repositories-policy.ts +3 -3
  64. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  65. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  66. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  67. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  68. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  69. package/src/runtime/plugin-resolution.ts +144 -24
  70. package/src/runtime/plugin-types.ts +9 -1
  71. package/src/runtime/post-turn-side-effects.ts +197 -130
  72. package/src/runtime/retrieval-adapters.ts +38 -4
  73. package/src/runtime/runtime-config.ts +150 -61
  74. package/src/runtime/runtime-extensions.ts +21 -34
  75. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  76. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  77. package/src/runtime/social-chat/social-chat.ts +594 -0
  78. package/src/runtime/specialist-runner.ts +36 -10
  79. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  80. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  81. package/src/runtime/thread-chat-helpers.ts +2 -2
  82. package/src/runtime/thread-plan-turn.ts +2 -1
  83. package/src/runtime/thread-turn-context.ts +172 -94
  84. package/src/runtime/turn-lifecycle.ts +93 -27
  85. package/src/services/agent-activity.service.ts +287 -203
  86. package/src/services/agent-executor.service.ts +329 -217
  87. package/src/services/artifact.service.ts +225 -148
  88. package/src/services/attachment.service.ts +137 -115
  89. package/src/services/autonomous-job.service.ts +888 -491
  90. package/src/services/chat-run-registry.service.ts +11 -1
  91. package/src/services/context-compaction.service.ts +136 -86
  92. package/src/services/document-chunk.service.ts +162 -90
  93. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  94. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  95. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  96. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  97. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  98. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  99. package/src/services/feedback-loop.service.ts +132 -76
  100. package/src/services/global-orchestrator.service.ts +80 -170
  101. package/src/services/graph-full-routing.ts +182 -0
  102. package/src/services/index.ts +18 -21
  103. package/src/services/institutional-memory.service.ts +220 -123
  104. package/src/services/learned-skill.service.ts +364 -259
  105. package/src/services/memory/memory-conversation.ts +95 -0
  106. package/src/services/memory/memory-org-memory.ts +39 -0
  107. package/src/services/memory/memory-preseeded.ts +80 -0
  108. package/src/services/memory/memory-rerank.ts +297 -0
  109. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  110. package/src/services/memory/memory.service.ts +692 -0
  111. package/src/services/memory/rerank.service.ts +209 -0
  112. package/src/services/monitoring-window.service.ts +92 -70
  113. package/src/services/mutating-approval.service.ts +62 -53
  114. package/src/services/node-workspace.service.ts +141 -98
  115. package/src/services/notification.service.ts +17 -16
  116. package/src/services/organization-member.service.ts +120 -66
  117. package/src/services/organization.service.ts +144 -51
  118. package/src/services/ownership-dispatcher.service.ts +415 -264
  119. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  120. package/src/services/plan/plan-agent-query.service.ts +322 -0
  121. package/src/services/plan/plan-approval.service.ts +102 -0
  122. package/src/services/plan/plan-artifact.service.ts +60 -0
  123. package/src/services/plan/plan-builder.service.ts +76 -0
  124. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  125. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  126. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  127. package/src/services/plan/plan-coordination.service.ts +181 -0
  128. package/src/services/plan/plan-cycle.service.ts +398 -0
  129. package/src/services/plan/plan-deadline.service.ts +547 -0
  130. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  131. package/src/services/plan/plan-executor-context.ts +35 -0
  132. package/src/services/plan/plan-executor-graph.ts +475 -0
  133. package/src/services/plan/plan-executor-helpers.ts +322 -0
  134. package/src/services/plan/plan-executor-persistence.ts +209 -0
  135. package/src/services/plan/plan-executor.service.ts +1654 -0
  136. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  137. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  138. package/src/services/plan/plan-run-serialization.ts +15 -0
  139. package/src/services/plan/plan-run.service.ts +644 -0
  140. package/src/services/plan/plan-scheduler.service.ts +385 -0
  141. package/src/services/plan/plan-template.service.ts +224 -0
  142. package/src/services/plan/plan-transaction-events.ts +33 -0
  143. package/src/services/plan/plan-validator.service.ts +907 -0
  144. package/src/services/plan/plan-workspace.service.ts +125 -0
  145. package/src/services/plugin-executor.service.ts +97 -68
  146. package/src/services/quality-metrics.service.ts +112 -94
  147. package/src/services/queue-job.service.ts +296 -230
  148. package/src/services/recent-activity-title.service.ts +65 -36
  149. package/src/services/recent-activity.service.ts +274 -259
  150. package/src/services/skill-resolver.service.ts +38 -12
  151. package/src/services/social-chat-history.service.ts +176 -125
  152. package/src/services/system-executor.service.ts +91 -61
  153. package/src/services/thread/thread-active-run.ts +203 -0
  154. package/src/services/thread/thread-bootstrap.ts +369 -0
  155. package/src/services/thread/thread-listing.ts +198 -0
  156. package/src/services/thread/thread-memory-block.ts +117 -0
  157. package/src/services/thread/thread-message.service.ts +363 -0
  158. package/src/services/thread/thread-record-store.ts +155 -0
  159. package/src/services/thread/thread-title.service.ts +74 -0
  160. package/src/services/thread/thread-turn-execution.ts +280 -0
  161. package/src/services/thread/thread-turn-message-context.ts +73 -0
  162. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  163. package/src/services/thread/thread-turn-streaming.ts +402 -0
  164. package/src/services/thread/thread-turn-tracing.ts +35 -0
  165. package/src/services/thread/thread-turn.ts +343 -0
  166. package/src/services/thread/thread.service.ts +335 -0
  167. package/src/services/user.service.ts +82 -32
  168. package/src/services/write-intent-validator.service.ts +63 -51
  169. package/src/storage/attachment-parser.ts +69 -27
  170. package/src/storage/attachment-storage.service.ts +331 -275
  171. package/src/storage/generated-document-storage.service.ts +66 -34
  172. package/src/system-agents/agent-result.ts +3 -1
  173. package/src/system-agents/context-compaction.agent.ts +2 -2
  174. package/src/system-agents/delegated-agent-factory.ts +159 -90
  175. package/src/system-agents/memory-reranker.agent.ts +2 -2
  176. package/src/system-agents/memory.agent.ts +2 -2
  177. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  178. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  179. package/src/system-agents/skill-extractor.agent.ts +2 -2
  180. package/src/system-agents/skill-manager.agent.ts +2 -2
  181. package/src/system-agents/thread-router.agent.ts +157 -113
  182. package/src/system-agents/title-generator.agent.ts +2 -2
  183. package/src/tools/execution-plan.tool.ts +220 -161
  184. package/src/tools/fetch-webpage.tool.ts +21 -17
  185. package/src/tools/firecrawl-client.ts +16 -6
  186. package/src/tools/index.ts +1 -0
  187. package/src/tools/memory-block.tool.ts +14 -6
  188. package/src/tools/plan-approval.tool.ts +49 -47
  189. package/src/tools/read-file-parts.tool.ts +44 -33
  190. package/src/tools/remember-memory.tool.ts +65 -45
  191. package/src/tools/search-web.tool.ts +26 -22
  192. package/src/tools/search.tool.ts +41 -29
  193. package/src/tools/team-think.tool.ts +124 -83
  194. package/src/tools/user-questions.tool.ts +4 -3
  195. package/src/tools/web-tool-shared.ts +6 -0
  196. package/src/utils/async.ts +17 -23
  197. package/src/utils/crypto.ts +21 -0
  198. package/src/utils/date-time.ts +40 -1
  199. package/src/utils/errors.ts +95 -16
  200. package/src/utils/hono-error-handler.ts +24 -39
  201. package/src/utils/index.ts +2 -1
  202. package/src/utils/null-proto-record.ts +41 -0
  203. package/src/utils/sse-keepalive.ts +124 -21
  204. package/src/workers/bootstrap.ts +186 -51
  205. package/src/workers/memory-consolidation.worker.ts +325 -237
  206. package/src/workers/organization-learning.worker.ts +50 -16
  207. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  208. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  209. package/src/workers/skill-extraction.runner.ts +176 -93
  210. package/src/workers/utils/file-section-chunker.ts +8 -10
  211. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  212. package/src/workers/utils/repomix-file-sections.ts +2 -2
  213. package/src/workers/utils/thread-message-query.ts +97 -38
  214. package/src/workers/worker-utils.ts +56 -31
  215. package/src/config/debug-logger.ts +0 -47
  216. package/src/redis/connection-accessor.ts +0 -26
  217. package/src/runtime/context-compaction-runtime.ts +0 -87
  218. package/src/runtime/social-chat-agent-runner.ts +0 -118
  219. package/src/runtime/social-chat.ts +0 -516
  220. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  221. package/src/services/adaptive-playbook.service.ts +0 -152
  222. package/src/services/artifact-provenance.service.ts +0 -172
  223. package/src/services/chat-attachments.service.ts +0 -17
  224. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  225. package/src/services/execution-plan.service.ts +0 -1118
  226. package/src/services/memory.service.ts +0 -914
  227. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  228. package/src/services/plan-agent-query.service.ts +0 -267
  229. package/src/services/plan-approval.service.ts +0 -83
  230. package/src/services/plan-artifact.service.ts +0 -50
  231. package/src/services/plan-builder.service.ts +0 -67
  232. package/src/services/plan-checkpoint.service.ts +0 -81
  233. package/src/services/plan-completion-side-effects.ts +0 -80
  234. package/src/services/plan-coordination.service.ts +0 -157
  235. package/src/services/plan-cycle.service.ts +0 -284
  236. package/src/services/plan-deadline.service.ts +0 -430
  237. package/src/services/plan-event-delivery.service.ts +0 -166
  238. package/src/services/plan-executor.service.ts +0 -1950
  239. package/src/services/plan-run.service.ts +0 -515
  240. package/src/services/plan-scheduler.service.ts +0 -240
  241. package/src/services/plan-template.service.ts +0 -177
  242. package/src/services/plan-validator.service.ts +0 -818
  243. package/src/services/plan-workspace.service.ts +0 -83
  244. package/src/services/rerank.service.ts +0 -156
  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
@@ -1,6 +1,10 @@
1
+ import { normalizeMessageBatch } from '@lota-sdk/shared'
2
+ import { Context, Effect, Layer } from 'effect'
1
3
  import { z } from 'zod'
2
4
 
3
- import { getRedisConnection } from '../redis'
5
+ import { RedisServiceTag, RuntimeConfigServiceTag } from '../effect/services'
6
+ import type { RedisConnectionManager } from '../redis/connection'
7
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
4
8
  import type { LotaRuntimeBackgroundCursor, LotaRuntimeBackgroundCursorKind } from '../runtime/runtime-extensions'
5
9
 
6
10
  const DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX = 'lota:social:history'
@@ -8,6 +12,37 @@ const DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX = 'lota:social:history'
8
12
  const SocialChatMessageRoleSchema = z.enum(['user', 'assistant'])
9
13
  const SocialChatSourceSchema = z.literal('social')
10
14
  const SocialChatPlatformSchema = z.literal('slack')
15
+ const SocialChatHistoryGenericPartSchema = z
16
+ .object({ type: z.string().optional(), text: z.string().optional() })
17
+ .catchall(z.unknown())
18
+ const SocialChatHistoryTextPartSchema = SocialChatHistoryGenericPartSchema.extend({
19
+ type: z.literal('text'),
20
+ text: z.string().trim().min(1),
21
+ })
22
+ const SocialChatHistoryFilePartSchema = SocialChatHistoryGenericPartSchema.extend({
23
+ type: z.literal('file'),
24
+ filename: z.string().trim().min(1),
25
+ mediaType: z.string().trim().min(1),
26
+ sizeBytes: z.number().int().nonnegative().nullable(),
27
+ storageKey: z.string().trim().min(1),
28
+ })
29
+ const SocialChatHistoryPartSchema = z.union([
30
+ SocialChatHistoryTextPartSchema,
31
+ SocialChatHistoryFilePartSchema,
32
+ SocialChatHistoryGenericPartSchema,
33
+ ])
34
+ const SocialChatHistoryMetadataSchema = z
35
+ .object({
36
+ platform: SocialChatPlatformSchema,
37
+ channelId: z.string().trim().min(1),
38
+ threadId: z.string().trim().min(1),
39
+ messageId: z.string().trim().min(1),
40
+ authorId: z.string().trim().min(1).optional(),
41
+ authorName: z.string().trim().min(1).optional(),
42
+ agentId: z.string().trim().min(1).optional(),
43
+ agentName: z.string().trim().min(1).optional(),
44
+ })
45
+ .catchall(z.unknown())
11
46
 
12
47
  const SocialChatHistoryMessageSchema = z.object({
13
48
  source: SocialChatSourceSchema,
@@ -18,15 +53,15 @@ const SocialChatHistoryMessageSchema = z.object({
18
53
  threadId: z.string().trim().min(1),
19
54
  messageId: z.string().trim().min(1),
20
55
  role: SocialChatMessageRoleSchema,
21
- parts: z.array(z.record(z.string(), z.unknown())),
22
- metadata: z.record(z.string(), z.unknown()).optional(),
56
+ parts: z.array(SocialChatHistoryPartSchema),
57
+ metadata: SocialChatHistoryMetadataSchema.optional(),
23
58
  cursor: z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }),
24
59
  })
25
60
 
61
+ export type SocialChatHistoryPart = z.infer<typeof SocialChatHistoryPartSchema>
62
+ export type SocialChatHistoryMetadata = z.infer<typeof SocialChatHistoryMetadataSchema>
26
63
  export type SocialChatHistoryMessage = z.infer<typeof SocialChatHistoryMessageSchema>
27
64
 
28
- let socialChatHistoryPrefix = DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
29
-
30
65
  function trimConfiguredPrefix(value: string | undefined): string {
31
66
  const normalized = value?.trim()
32
67
  return normalized && normalized.length > 0 ? normalized : DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
@@ -38,160 +73,176 @@ function compareCursorOrder(left: LotaRuntimeBackgroundCursor, right: LotaRuntim
38
73
  return left.id.localeCompare(right.id)
39
74
  }
40
75
 
41
- class SocialChatHistoryService {
42
- configure(params?: { keyPrefix?: string }): void {
43
- socialChatHistoryPrefix = trimConfiguredPrefix(params?.keyPrefix)
44
- }
76
+ export function makeSocialChatHistoryService(
77
+ redis: RedisConnectionManager,
78
+ config: ResolvedLotaRuntimeConfig | undefined,
79
+ ) {
80
+ const resolvePrefix = (): string => trimConfiguredPrefix(config?.socialChat?.historyRedisKeyPrefix)
45
81
 
46
- private messageStorageKey(message: {
82
+ const messageStorageKey = (message: {
47
83
  platform: 'slack'
48
84
  workspaceId: string
49
85
  threadId: string
50
86
  messageId: string
51
- }): string {
52
- return `${socialChatHistoryPrefix}:message:${message.platform}:${message.workspaceId}:${message.threadId}:${message.messageId}`
53
- }
87
+ }): string =>
88
+ `${resolvePrefix()}:message:${message.platform}:${message.workspaceId}:${message.threadId}:${message.messageId}`
54
89
 
55
- private threadIndexKey(workspaceId: string, threadId: string): string {
56
- return `${socialChatHistoryPrefix}:thread:${workspaceId}:${threadId}`
57
- }
90
+ const threadIndexKey = (workspaceId: string, threadId: string): string =>
91
+ `${resolvePrefix()}:thread:${workspaceId}:${threadId}`
58
92
 
59
- private workspaceIndexKey(workspaceId: string): string {
60
- return `${socialChatHistoryPrefix}:workspace:${workspaceId}`
61
- }
93
+ const workspaceIndexKey = (workspaceId: string): string => `${resolvePrefix()}:workspace:${workspaceId}`
62
94
 
63
- private cursorKey(kind: LotaRuntimeBackgroundCursorKind, workspaceId: string): string {
64
- return `${socialChatHistoryPrefix}:cursor:${kind}:${workspaceId}`
65
- }
95
+ const cursorKey = (kind: LotaRuntimeBackgroundCursorKind, workspaceId: string): string =>
96
+ `${resolvePrefix()}:cursor:${kind}:${workspaceId}`
66
97
 
67
- private serializeMessage(message: SocialChatHistoryMessage): string {
68
- return JSON.stringify({
69
- ...message,
70
- cursor: { ...message.cursor, createdAt: message.cursor.createdAt.toISOString() },
71
- })
72
- }
98
+ const serializeMessage = (message: SocialChatHistoryMessage): string =>
99
+ JSON.stringify({ ...message, cursor: { ...message.cursor, createdAt: message.cursor.createdAt.toISOString() } })
73
100
 
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
- }
101
+ const parseStoredMessageEffect = (value: string | null): Effect.Effect<SocialChatHistoryMessage | null> => {
102
+ if (!value) return Effect.succeed(null)
83
103
 
84
- private serializeCursor(cursor: LotaRuntimeBackgroundCursor): string {
85
- return JSON.stringify({ ...cursor, createdAt: cursor.createdAt.toISOString() })
104
+ return Effect.try({ try: (): unknown => JSON.parse(value), catch: (cause) => cause }).pipe(
105
+ Effect.map((parsed) => {
106
+ const validated = SocialChatHistoryMessageSchema.safeParse(parsed)
107
+ return validated.success ? validated.data : null
108
+ }),
109
+ Effect.catch(() => Effect.succeed(null)),
110
+ )
86
111
  }
87
112
 
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
- }
96
- }
113
+ const serializeCursor = (cursor: LotaRuntimeBackgroundCursor): string =>
114
+ JSON.stringify({ ...cursor, createdAt: cursor.createdAt.toISOString() })
97
115
 
98
- async upsertMessages(messages: SocialChatHistoryMessage[]): Promise<SocialChatHistoryMessage[]> {
99
- if (messages.length === 0) return []
116
+ const parseCursorEffect = (value: string | null): Effect.Effect<LotaRuntimeBackgroundCursor | null> => {
117
+ if (!value) return Effect.succeed(null)
100
118
 
101
- const redis = getRedisConnection()
102
- const normalizedMessages = messages.map((message) => SocialChatHistoryMessageSchema.parse(message))
103
- const multi = redis.multi()
104
-
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
- }
112
-
113
- await multi.exec()
114
- return normalizedMessages
119
+ return Effect.try({ try: (): unknown => JSON.parse(value), catch: (cause) => cause }).pipe(
120
+ Effect.map((parsed) => {
121
+ const validated = z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }).safeParse(parsed)
122
+ return validated.success ? validated.data : null
123
+ }),
124
+ Effect.catch(() => Effect.succeed(null)),
125
+ )
115
126
  }
116
127
 
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 []
128
+ const upsertMessagesEffect = (messages: SocialChatHistoryMessage[]) =>
129
+ Effect.gen(function* () {
130
+ if (messages.length === 0) return [] as SocialChatHistoryMessage[]
121
131
 
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
- }
132
+ const conn = redis.getConnection()
133
+ const normalizedMessages = normalizeMessageBatch(messages, {
134
+ parseMessage: (message) => SocialChatHistoryMessageSchema.parse(message),
135
+ compareMessages: (left, right) => compareCursorOrder(left.cursor, right.cursor),
136
+ getMessageId: (message) => message.messageId,
137
+ })
138
+ const multi = conn.multi()
139
+
140
+ for (const message of normalizedMessages) {
141
+ const storageKey = messageStorageKey(message)
142
+ const score = message.cursor.createdAt.getTime()
143
+ multi.set(storageKey, serializeMessage(message))
144
+ multi.zadd(threadIndexKey(message.workspaceId, message.threadId), score, storageKey)
145
+ multi.zadd(workspaceIndexKey(message.workspaceId), score, storageKey)
146
+ }
147
+
148
+ yield* Effect.tryPromise(() => multi.exec())
149
+ return normalizedMessages
150
+ })
151
+
152
+ const listThreadMessagesEffect = (params: { workspaceId: string; threadId: string }) =>
153
+ Effect.gen(function* () {
154
+ const conn = redis.getConnection()
155
+ const storageKeys = yield* Effect.tryPromise(() =>
156
+ conn.zrange(threadIndexKey(params.workspaceId, params.threadId), 0, -1),
157
+ )
158
+ if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
159
+
160
+ const storedValues = yield* Effect.tryPromise(() => conn.mget(storageKeys))
161
+ const parsedMessages = yield* Effect.forEach(storedValues, parseStoredMessageEffect)
162
+ return parsedMessages
163
+ .filter((message): message is SocialChatHistoryMessage => message !== null)
164
+ .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
165
+ })
128
166
 
129
- async listWorkspaceMessages(params: {
167
+ const listWorkspaceMessagesEffect = (params: {
130
168
  workspaceId: string
131
169
  cursor: LotaRuntimeBackgroundCursor | null
132
170
  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
- }
171
+ }) =>
172
+ Effect.gen(function* () {
173
+ const conn = redis.getConnection()
174
+ const indexKey = workspaceIndexKey(params.workspaceId)
175
+ const scoreStart =
176
+ params.cursor?.createdAt.getTime() ??
177
+ (params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
178
+ const storageKeys = yield* Effect.tryPromise(() =>
179
+ params.cursor || params.onboardingCutoff
180
+ ? conn.zrangebyscore(indexKey, scoreStart, '+inf')
181
+ : conn.zrange(indexKey, 0, -1),
182
+ )
183
+ if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
184
+
185
+ const storedValues = yield* Effect.tryPromise(() => conn.mget(storageKeys))
186
+ const parsedMessages = yield* Effect.forEach(storedValues, parseStoredMessageEffect)
187
+ return parsedMessages
188
+ .filter((message): message is SocialChatHistoryMessage => message !== null)
189
+ .filter((message) => {
190
+ if (params.cursor) {
191
+ return compareCursorOrder(message.cursor, params.cursor) > 0
192
+ }
193
+ if (params.onboardingCutoff) {
194
+ return message.cursor.createdAt.getTime() > params.onboardingCutoff.getTime()
195
+ }
196
+ return true
197
+ })
198
+ .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
199
+ })
161
200
 
162
- async hasWorkspaceMessages(params: {
201
+ const hasWorkspaceMessagesEffect = (params: {
163
202
  workspaceId: string
164
203
  cursor: LotaRuntimeBackgroundCursor | null
165
204
  onboardingCutoff: Date | null
166
- }): Promise<boolean> {
167
- const messages = await this.listWorkspaceMessages({
205
+ }) =>
206
+ listWorkspaceMessagesEffect({
168
207
  workspaceId: params.workspaceId,
169
208
  cursor: params.cursor,
170
209
  onboardingCutoff: params.onboardingCutoff,
171
- })
172
- return messages.length > 0
173
- }
210
+ }).pipe(Effect.map((messages) => messages.length > 0))
174
211
 
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
- }
212
+ const getBackgroundCursorEffect = (kind: LotaRuntimeBackgroundCursorKind, workspaceId: string) =>
213
+ Effect.tryPromise(() => redis.getConnection().get(cursorKey(kind, workspaceId))).pipe(
214
+ Effect.flatMap((value) => parseCursorEffect(value)),
215
+ )
182
216
 
183
- async setBackgroundCursor(
217
+ const setBackgroundCursorEffect = (
184
218
  kind: LotaRuntimeBackgroundCursorKind,
185
219
  workspaceId: string,
186
220
  cursor: LotaRuntimeBackgroundCursor,
187
- ): Promise<void> {
188
- const redis = getRedisConnection()
189
- await redis.set(this.cursorKey(kind, workspaceId), this.serializeCursor(cursor))
221
+ ) =>
222
+ Effect.tryPromise(() => redis.getConnection().set(cursorKey(kind, workspaceId), serializeCursor(cursor))).pipe(
223
+ Effect.asVoid,
224
+ )
225
+
226
+ return {
227
+ upsertMessages: upsertMessagesEffect,
228
+ listThreadMessages: listThreadMessagesEffect,
229
+ listWorkspaceMessages: listWorkspaceMessagesEffect,
230
+ hasWorkspaceMessages: hasWorkspaceMessagesEffect,
231
+ getBackgroundCursor: getBackgroundCursorEffect,
232
+ setBackgroundCursor: setBackgroundCursorEffect,
190
233
  }
191
234
  }
192
235
 
193
- export const socialChatHistoryService = new SocialChatHistoryService()
194
-
195
- export function configureSocialChatHistory(params?: { keyPrefix?: string }): void {
196
- socialChatHistoryService.configure(params)
197
- }
236
+ export class SocialChatHistoryServiceTag extends Context.Service<
237
+ SocialChatHistoryServiceTag,
238
+ ReturnType<typeof makeSocialChatHistoryService>
239
+ >()('SocialChatHistoryService') {}
240
+
241
+ export const SocialChatHistoryServiceLive = Layer.effect(
242
+ SocialChatHistoryServiceTag,
243
+ Effect.gen(function* () {
244
+ const redis = yield* RedisServiceTag
245
+ const config = yield* RuntimeConfigServiceTag
246
+ return makeSocialChatHistoryService(redis, config)
247
+ }),
248
+ )
@@ -1,27 +1,33 @@
1
1
  import type { OwnershipDispatchContext, PlanNodeResult, PlanNodeSpec, SystemPlanNodeOwner } from '@lota-sdk/shared'
2
+ import { Context, Effect, Layer } from 'effect'
2
3
 
4
+ import { BadRequestError } from '../effect/errors'
5
+ import { RuntimeConfigServiceTag } from '../effect/services'
3
6
  import type { SystemNodeExecutor, PluginNodeExecutionParams } from '../runtime/plugin-types'
4
- import { getRuntimeConfig } from '../runtime/runtime-config'
5
- import type { PlanValidationIssueInput } from './plan-validator.service'
7
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
8
+ import type { PlanValidationIssueInput } from './plan/plan-validator.service'
6
9
 
7
10
  const BUILT_IN_SYSTEM_EXECUTORS = Object.freeze({
8
11
  'plan-runtime': {
9
12
  supportedOperations: ['echo-input'],
10
- async executeNode(params: PluginNodeExecutionParams): Promise<PlanNodeResult> {
11
- return { notes: 'System echo-input completed.', structuredOutput: structuredClone(params.inputs), artifacts: [] }
13
+ executeNode(params: PluginNodeExecutionParams): Promise<PlanNodeResult> {
14
+ return Promise.resolve({
15
+ notes: 'System echo-input completed.',
16
+ structuredOutput: structuredClone(params.inputs),
17
+ artifacts: [],
18
+ })
12
19
  },
13
20
  } satisfies SystemNodeExecutor,
14
21
  })
15
22
 
16
- export function getBuiltInSystemExecutors(): Record<string, SystemNodeExecutor> {
17
- return Object.fromEntries(Object.entries(BUILT_IN_SYSTEM_EXECUTORS))
23
+ function isSystemOwner(
24
+ owner: PlanNodeSpec['owner'],
25
+ ): owner is Extract<PlanNodeSpec['owner'], { executorType: 'system' }> {
26
+ return owner.executorType === 'system'
18
27
  }
19
28
 
20
- function getSystemExecutors() {
21
- return (getRuntimeConfig().systemExecutors ?? getBuiltInSystemExecutors()) as Record<
22
- string,
23
- SystemNodeExecutor | undefined
24
- >
29
+ export function getBuiltInSystemExecutors(): Record<string, SystemNodeExecutor> {
30
+ return Object.fromEntries(Object.entries(BUILT_IN_SYSTEM_EXECUTORS))
25
31
  }
26
32
 
27
33
  function buildSystemExecutionParams(params: {
@@ -45,61 +51,85 @@ function buildSystemExecutionParams(params: {
45
51
  }
46
52
  }
47
53
 
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
- }
54
+ export function makeSystemExecutorService(config: ResolvedLotaRuntimeConfig) {
55
+ function getSystemExecutors() {
56
+ return (config.systemExecutors ?? getBuiltInSystemExecutors()) as Record<string, SystemNodeExecutor | undefined>
57
+ }
62
58
 
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
- }
59
+ return {
60
+ validateOwner(owner: SystemPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
61
+ const executor = getSystemExecutors()[owner.ref]
62
+ if (!executor) {
63
+ return [
64
+ {
65
+ severity: 'blocking',
66
+ code: 'system_executor_missing',
67
+ message: `Node "${nodeId}" references unknown system executor "${owner.ref}".`,
68
+ nodeId,
69
+ detail: { systemRef: owner.ref },
70
+ },
71
+ ]
72
+ }
74
73
 
75
- return []
76
- }
74
+ if (!executor.supportedOperations.includes(owner.operation)) {
75
+ return [
76
+ {
77
+ severity: 'blocking',
78
+ code: 'system_operation_missing',
79
+ message: `System executor "${owner.ref}" does not support operation "${owner.operation}".`,
80
+ nodeId,
81
+ detail: { systemRef: owner.ref, operation: owner.operation },
82
+ },
83
+ ]
84
+ }
85
+
86
+ return []
87
+ },
77
88
 
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
- }
89
+ executeNode(params: {
90
+ nodeSpec: PlanNodeSpec
91
+ resolvedInput: Record<string, unknown>
92
+ context: OwnershipDispatchContext
93
+ }) {
94
+ return Effect.gen(function* () {
95
+ const owner = params.nodeSpec.owner
96
+ if (!isSystemOwner(owner)) {
97
+ return yield* new BadRequestError({
98
+ message: `SystemExecutor cannot execute owner type "${owner.executorType}".`,
99
+ })
100
+ }
86
101
 
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
- }
102
+ const executor = getSystemExecutors()[owner.ref]
103
+ if (!executor || !executor.supportedOperations.includes(owner.operation)) {
104
+ return yield* new BadRequestError({
105
+ message: `System executor ${owner.ref}.${owner.operation} is not registered.`,
106
+ })
107
+ }
93
108
 
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
- )
109
+ return yield* Effect.tryPromise(() =>
110
+ executor.executeNode(
111
+ buildSystemExecutionParams({
112
+ owner,
113
+ nodeSpec: params.nodeSpec,
114
+ resolvedInput: params.resolvedInput,
115
+ context: params.context,
116
+ }),
117
+ ),
118
+ )
119
+ })
120
+ },
102
121
  }
103
122
  }
104
123
 
105
- export const systemExecutorService = new SystemExecutorService()
124
+ export class SystemExecutorServiceTag extends Context.Service<
125
+ SystemExecutorServiceTag,
126
+ ReturnType<typeof makeSystemExecutorService>
127
+ >()('SystemExecutorService') {}
128
+
129
+ export const SystemExecutorServiceLive = Layer.effect(
130
+ SystemExecutorServiceTag,
131
+ Effect.gen(function* () {
132
+ const runtimeConfig = yield* RuntimeConfigServiceTag
133
+ return makeSystemExecutorService(runtimeConfig)
134
+ }),
135
+ )