@lota-sdk/core 0.4.9 → 0.4.11

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 (182) hide show
  1. package/package.json +2 -2
  2. package/src/ai/embedding-cache.ts +3 -1
  3. package/src/ai-gateway/ai-gateway.ts +164 -82
  4. package/src/ai-gateway/index.ts +16 -1
  5. package/src/config/agent-defaults.ts +4 -107
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/config/background-processing.ts +1 -1
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +22 -25
  10. package/src/config/thread-defaults.ts +1 -10
  11. package/src/create-runtime.ts +145 -670
  12. package/src/db/base.service.ts +30 -38
  13. package/src/db/memory-query-builder.ts +2 -1
  14. package/src/db/memory-store.ts +29 -20
  15. package/src/db/memory.ts +188 -195
  16. package/src/db/service-normalization.ts +97 -64
  17. package/src/db/service.ts +496 -384
  18. package/src/db/startup.ts +30 -19
  19. package/src/effect/helpers.ts +30 -5
  20. package/src/effect/index.ts +7 -7
  21. package/src/effect/layers.ts +75 -72
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -71
  24. package/src/index.ts +13 -12
  25. package/src/queues/autonomous-job.queue.ts +177 -143
  26. package/src/queues/context-compaction.queue.ts +41 -39
  27. package/src/queues/delayed-node-promotion.queue.ts +61 -42
  28. package/src/queues/document-processor.queue.ts +5 -3
  29. package/src/queues/index.ts +1 -0
  30. package/src/queues/memory-consolidation.queue.ts +79 -53
  31. package/src/queues/organization-learning.queue.ts +70 -33
  32. package/src/queues/plan-agent-heartbeat.queue.ts +111 -83
  33. package/src/queues/plan-scheduler.queue.ts +101 -97
  34. package/src/queues/post-chat-memory.queue.ts +56 -46
  35. package/src/queues/queue-factory.ts +146 -69
  36. package/src/queues/queues.service.ts +61 -0
  37. package/src/queues/title-generation.queue.ts +44 -44
  38. package/src/redis/connection.ts +181 -164
  39. package/src/redis/org-memory-lock.ts +24 -9
  40. package/src/redis/redis-lease-lock.ts +8 -1
  41. package/src/redis/stream-context.ts +17 -9
  42. package/src/runtime/agent-identity-overrides.ts +7 -3
  43. package/src/runtime/agent-runtime-policy.ts +10 -5
  44. package/src/runtime/agent-stream-helpers.ts +24 -15
  45. package/src/runtime/chat-run-orchestration.ts +1 -1
  46. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  47. package/src/runtime/context-compaction/context-compaction.ts +131 -85
  48. package/src/runtime/domain-layer.ts +203 -0
  49. package/src/runtime/execution-plan-visibility.ts +5 -2
  50. package/src/runtime/graph-designer.ts +0 -14
  51. package/src/runtime/helper-model.ts +8 -4
  52. package/src/runtime/index.ts +1 -1
  53. package/src/runtime/indexed-repositories-policy.ts +2 -6
  54. package/src/runtime/memory/memory-block.ts +19 -9
  55. package/src/runtime/memory/memory-pipeline.ts +53 -66
  56. package/src/runtime/memory/memory-scope.ts +33 -29
  57. package/src/runtime/plugin-resolution.ts +58 -62
  58. package/src/runtime/post-turn-side-effects.ts +139 -161
  59. package/src/runtime/retrieval-adapters.ts +4 -4
  60. package/src/runtime/runtime-config.ts +3 -9
  61. package/src/runtime/runtime-extensions.ts +0 -43
  62. package/src/runtime/runtime-lifecycle.ts +124 -0
  63. package/src/runtime/runtime-services.ts +455 -0
  64. package/src/runtime/runtime-worker-registry.ts +113 -30
  65. package/src/runtime/social-chat/social-chat-agent-runner.ts +13 -8
  66. package/src/runtime/social-chat/social-chat-history.ts +24 -13
  67. package/src/runtime/social-chat/social-chat.ts +420 -369
  68. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +64 -57
  69. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  70. package/src/runtime/thread-chat-helpers.ts +18 -9
  71. package/src/runtime/thread-turn-context.ts +28 -74
  72. package/src/runtime/turn-lifecycle.ts +6 -14
  73. package/src/services/agent-activity.service.ts +169 -176
  74. package/src/services/agent-executor.service.ts +207 -196
  75. package/src/services/artifact.service.ts +10 -5
  76. package/src/services/attachment.service.ts +16 -48
  77. package/src/services/autonomous-job.service.ts +81 -87
  78. package/src/services/background-work.service.ts +54 -0
  79. package/src/services/chat-run-registry.service.ts +3 -1
  80. package/src/services/context-compaction.service.ts +8 -10
  81. package/src/services/document-chunk.service.ts +8 -17
  82. package/src/services/execution-plan/execution-plan-graph.ts +122 -109
  83. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  84. package/src/services/execution-plan/execution-plan.service.ts +68 -51
  85. package/src/services/feedback-loop.service.ts +1 -1
  86. package/src/services/global-orchestrator.service.ts +49 -15
  87. package/src/services/graph-full-routing.ts +49 -37
  88. package/src/services/index.ts +1 -0
  89. package/src/services/institutional-memory.service.ts +8 -17
  90. package/src/services/learned-skill.service.ts +38 -35
  91. package/src/services/memory/memory-conversation.ts +10 -5
  92. package/src/services/memory/memory-errors.ts +27 -0
  93. package/src/services/memory/memory-org-memory.ts +14 -3
  94. package/src/services/memory/memory-preseeded.ts +10 -4
  95. package/src/services/memory/memory-utils.ts +2 -1
  96. package/src/services/memory/memory.service.ts +37 -52
  97. package/src/services/memory/rerank.service.ts +3 -11
  98. package/src/services/monitoring-window.service.ts +1 -1
  99. package/src/services/mutating-approval.service.ts +1 -1
  100. package/src/services/node-workspace.service.ts +2 -2
  101. package/src/services/notification.service.ts +16 -4
  102. package/src/services/organization-member.service.ts +1 -1
  103. package/src/services/organization.service.ts +34 -51
  104. package/src/services/ownership-dispatcher.service.ts +148 -95
  105. package/src/services/plan/plan-agent-heartbeat.service.ts +30 -16
  106. package/src/services/plan/plan-agent-query.service.ts +13 -9
  107. package/src/services/plan/plan-approval.service.ts +52 -48
  108. package/src/services/plan/plan-artifact.service.ts +2 -2
  109. package/src/services/plan/plan-builder.service.ts +2 -2
  110. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  111. package/src/services/plan/plan-compiler.service.ts +1 -1
  112. package/src/services/plan/plan-completion-side-effects.ts +99 -113
  113. package/src/services/plan/plan-coordination.service.ts +1 -1
  114. package/src/services/plan/plan-cycle.service.ts +171 -202
  115. package/src/services/plan/plan-deadline.service.ts +304 -307
  116. package/src/services/plan/plan-event-delivery.service.ts +84 -72
  117. package/src/services/plan/plan-executor-context.ts +2 -0
  118. package/src/services/plan/plan-executor-graph.ts +375 -353
  119. package/src/services/plan/plan-executor-helpers.ts +60 -75
  120. package/src/services/plan/plan-executor.service.ts +494 -489
  121. package/src/services/plan/plan-run.service.ts +12 -19
  122. package/src/services/plan/plan-scheduler.service.ts +89 -82
  123. package/src/services/plan/plan-template.service.ts +1 -1
  124. package/src/services/plan/plan-transaction-events.ts +8 -5
  125. package/src/services/plan/plan-validator.service.ts +1 -1
  126. package/src/services/plan/plan-workspace.service.ts +17 -11
  127. package/src/services/plugin-executor.service.ts +26 -21
  128. package/src/services/quality-metrics.service.ts +1 -1
  129. package/src/services/queue-job.service.ts +8 -17
  130. package/src/services/recent-activity-title.service.ts +22 -10
  131. package/src/services/recent-activity.service.ts +1 -1
  132. package/src/services/skill-resolver.service.ts +1 -1
  133. package/src/services/social-chat-history.service.ts +37 -20
  134. package/src/services/system-executor.service.ts +25 -20
  135. package/src/services/thread/thread-bootstrap.ts +37 -19
  136. package/src/services/thread/thread-listing.ts +2 -1
  137. package/src/services/thread/thread-memory-block.ts +18 -5
  138. package/src/services/thread/thread-message.service.ts +30 -13
  139. package/src/services/thread/thread-title.service.ts +1 -1
  140. package/src/services/thread/thread-turn-execution.ts +87 -83
  141. package/src/services/thread/thread-turn-preparation.service.ts +65 -40
  142. package/src/services/thread/thread-turn-streaming.ts +32 -36
  143. package/src/services/thread/thread-turn.ts +43 -29
  144. package/src/services/thread/thread.service.ts +32 -8
  145. package/src/services/user.service.ts +1 -1
  146. package/src/services/write-intent-validator.service.ts +1 -1
  147. package/src/storage/attachment-storage.service.ts +7 -4
  148. package/src/storage/generated-document-storage.service.ts +1 -1
  149. package/src/system-agents/context-compaction.agent.ts +1 -1
  150. package/src/system-agents/helper-agent-options.ts +1 -1
  151. package/src/system-agents/memory-reranker.agent.ts +1 -1
  152. package/src/system-agents/memory.agent.ts +1 -1
  153. package/src/system-agents/recent-activity-title-refiner.agent.ts +9 -6
  154. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  155. package/src/system-agents/skill-extractor.agent.ts +1 -1
  156. package/src/system-agents/skill-manager.agent.ts +1 -1
  157. package/src/system-agents/thread-router.agent.ts +23 -20
  158. package/src/system-agents/title-generator.agent.ts +1 -1
  159. package/src/tools/execution-plan.tool.ts +36 -20
  160. package/src/tools/fetch-webpage.tool.ts +30 -22
  161. package/src/tools/firecrawl-client.ts +1 -6
  162. package/src/tools/plan-approval.tool.ts +9 -1
  163. package/src/tools/remember-memory.tool.ts +3 -6
  164. package/src/tools/research-topic.tool.ts +12 -3
  165. package/src/tools/search-web.tool.ts +26 -18
  166. package/src/tools/search.tool.ts +4 -5
  167. package/src/tools/team-think.tool.ts +139 -121
  168. package/src/utils/async.ts +15 -6
  169. package/src/utils/errors.ts +27 -15
  170. package/src/workers/bootstrap.ts +34 -58
  171. package/src/workers/memory-consolidation.worker.ts +4 -1
  172. package/src/workers/organization-learning.worker.ts +16 -3
  173. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  174. package/src/workers/regular-chat-memory-digest.runner.ts +46 -29
  175. package/src/workers/skill-extraction.runner.ts +13 -15
  176. package/src/workers/worker-utils.ts +14 -8
  177. package/src/config/search.ts +0 -3
  178. package/src/effect/awaitable-effect.ts +0 -87
  179. package/src/effect/runtime-ref.ts +0 -25
  180. package/src/effect/runtime.ts +0 -31
  181. package/src/redis/runtime-connection.ts +0 -10
  182. package/src/runtime/agent-types.ts +0 -1
@@ -33,7 +33,7 @@ export function makeSkillResolverService(deps: {
33
33
  }
34
34
 
35
35
  export class SkillResolverServiceTag extends Context.Service<SkillResolverServiceTag, SkillResolverService>()(
36
- 'SkillResolverService',
36
+ '@lota-sdk/core/SkillResolverService',
37
37
  ) {}
38
38
 
39
39
  export const SkillResolverServiceLive = Layer.effect(
@@ -1,12 +1,22 @@
1
1
  import { normalizeMessageBatch } from '@lota-sdk/shared'
2
- import { Context, Effect, Layer } from 'effect'
2
+ import { Context, Effect, Layer, Schema } from 'effect'
3
3
  import { z } from 'zod'
4
4
 
5
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
5
6
  import { RedisServiceTag, RuntimeConfigServiceTag } from '../effect/services'
6
7
  import type { RedisConnectionManager } from '../redis/connection'
7
8
  import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
8
9
  import type { LotaRuntimeBackgroundCursor, LotaRuntimeBackgroundCursorKind } from '../runtime/runtime-extensions'
9
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
+
10
20
  const DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX = 'lota:social:history'
11
21
 
12
22
  const SocialChatMessageRoleSchema = z.enum(['user', 'assistant'])
@@ -57,6 +67,8 @@ const SocialChatHistoryMessageSchema = z.object({
57
67
  metadata: SocialChatHistoryMetadataSchema.optional(),
58
68
  cursor: z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }),
59
69
  })
70
+ const SocialChatHistoryCursorSchema = z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) })
71
+ const decodeJsonStringEffect = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)
60
72
 
61
73
  export type SocialChatHistoryPart = z.infer<typeof SocialChatHistoryPartSchema>
62
74
  export type SocialChatHistoryMetadata = z.infer<typeof SocialChatHistoryMetadataSchema>
@@ -101,7 +113,7 @@ export function makeSocialChatHistoryService(
101
113
  const parseStoredMessageEffect = (value: string | null): Effect.Effect<SocialChatHistoryMessage | null> => {
102
114
  if (!value) return Effect.succeed(null)
103
115
 
104
- return Effect.try({ try: (): unknown => JSON.parse(value), catch: (cause) => cause }).pipe(
116
+ return decodeJsonStringEffect(value).pipe(
105
117
  Effect.map((parsed) => {
106
118
  const validated = SocialChatHistoryMessageSchema.safeParse(parsed)
107
119
  return validated.success ? validated.data : null
@@ -116,9 +128,9 @@ export function makeSocialChatHistoryService(
116
128
  const parseCursorEffect = (value: string | null): Effect.Effect<LotaRuntimeBackgroundCursor | null> => {
117
129
  if (!value) return Effect.succeed(null)
118
130
 
119
- return Effect.try({ try: (): unknown => JSON.parse(value), catch: (cause) => cause }).pipe(
131
+ return decodeJsonStringEffect(value).pipe(
120
132
  Effect.map((parsed) => {
121
- const validated = z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }).safeParse(parsed)
133
+ const validated = SocialChatHistoryCursorSchema.safeParse(parsed)
122
134
  return validated.success ? validated.data : null
123
135
  }),
124
136
  Effect.catch(() => Effect.succeed(null)),
@@ -145,19 +157,20 @@ export function makeSocialChatHistoryService(
145
157
  multi.zadd(workspaceIndexKey(message.workspaceId), score, storageKey)
146
158
  }
147
159
 
148
- yield* Effect.tryPromise(() => multi.exec())
160
+ yield* tryRedisPromise(() => multi.exec(), 'Failed to upsert social chat messages.')
149
161
  return normalizedMessages
150
162
  })
151
163
 
152
164
  const listThreadMessagesEffect = (params: { workspaceId: string; threadId: string }) =>
153
165
  Effect.gen(function* () {
154
166
  const conn = redis.getConnection()
155
- const storageKeys = yield* Effect.tryPromise(() =>
156
- conn.zrange(threadIndexKey(params.workspaceId, params.threadId), 0, -1),
167
+ const storageKeys = yield* tryRedisPromise(
168
+ () => conn.zrange(threadIndexKey(params.workspaceId, params.threadId), 0, -1),
169
+ 'Failed to list thread message keys.',
157
170
  )
158
171
  if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
159
172
 
160
- const storedValues = yield* Effect.tryPromise(() => conn.mget(storageKeys))
173
+ const storedValues = yield* tryRedisPromise(() => conn.mget(storageKeys), 'Failed to read thread messages.')
161
174
  const parsedMessages = yield* Effect.forEach(storedValues, parseStoredMessageEffect)
162
175
  return parsedMessages
163
176
  .filter((message): message is SocialChatHistoryMessage => message !== null)
@@ -175,14 +188,16 @@ export function makeSocialChatHistoryService(
175
188
  const scoreStart =
176
189
  params.cursor?.createdAt.getTime() ??
177
190
  (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),
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.',
182
197
  )
183
198
  if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
184
199
 
185
- const storedValues = yield* Effect.tryPromise(() => conn.mget(storageKeys))
200
+ const storedValues = yield* tryRedisPromise(() => conn.mget(storageKeys), 'Failed to read workspace messages.')
186
201
  const parsedMessages = yield* Effect.forEach(storedValues, parseStoredMessageEffect)
187
202
  return parsedMessages
188
203
  .filter((message): message is SocialChatHistoryMessage => message !== null)
@@ -210,18 +225,20 @@ export function makeSocialChatHistoryService(
210
225
  }).pipe(Effect.map((messages) => messages.length > 0))
211
226
 
212
227
  const getBackgroundCursorEffect = (kind: LotaRuntimeBackgroundCursorKind, workspaceId: string) =>
213
- Effect.tryPromise(() => redis.getConnection().get(cursorKey(kind, workspaceId))).pipe(
214
- Effect.flatMap((value) => parseCursorEffect(value)),
215
- )
228
+ tryRedisPromise(
229
+ () => redis.getConnection().get(cursorKey(kind, workspaceId)),
230
+ 'Failed to read background cursor.',
231
+ ).pipe(Effect.flatMap((value) => parseCursorEffect(value)))
216
232
 
217
233
  const setBackgroundCursorEffect = (
218
234
  kind: LotaRuntimeBackgroundCursorKind,
219
235
  workspaceId: string,
220
236
  cursor: LotaRuntimeBackgroundCursor,
221
237
  ) =>
222
- Effect.tryPromise(() => redis.getConnection().set(cursorKey(kind, workspaceId), serializeCursor(cursor))).pipe(
223
- Effect.asVoid,
224
- )
238
+ tryRedisPromise(
239
+ () => redis.getConnection().set(cursorKey(kind, workspaceId), serializeCursor(cursor)),
240
+ 'Failed to write background cursor.',
241
+ ).pipe(Effect.asVoid)
225
242
 
226
243
  return {
227
244
  upsertMessages: upsertMessagesEffect,
@@ -236,7 +253,7 @@ export function makeSocialChatHistoryService(
236
253
  export class SocialChatHistoryServiceTag extends Context.Service<
237
254
  SocialChatHistoryServiceTag,
238
255
  ReturnType<typeof makeSocialChatHistoryService>
239
- >()('SocialChatHistoryService') {}
256
+ >()('@lota-sdk/core/SocialChatHistoryService') {}
240
257
 
241
258
  export const SocialChatHistoryServiceLive = Layer.effect(
242
259
  SocialChatHistoryServiceTag,
@@ -1,8 +1,13 @@
1
1
  import type { OwnershipDispatchContext, PlanNodeResult, PlanNodeSpec, SystemPlanNodeOwner } from '@lota-sdk/shared'
2
2
  import { Context, Effect, Layer } from 'effect'
3
3
 
4
- import { BadRequestError } from '../effect/errors'
4
+ import { BadRequestError, ServiceError } from '../effect/errors'
5
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
5
6
  import { RuntimeConfigServiceTag } from '../effect/services'
7
+
8
+ const trySystemExecutorPromise = makeEffectTryPromiseWithMessage(
9
+ (message, cause) => new ServiceError({ message, cause }),
10
+ )
6
11
  import type { SystemNodeExecutor, PluginNodeExecutionParams } from '../runtime/plugin-types'
7
12
  import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
8
13
  import type { PlanValidationIssueInput } from './plan/plan-validator.service'
@@ -86,27 +91,27 @@ export function makeSystemExecutorService(config: ResolvedLotaRuntimeConfig) {
86
91
  return []
87
92
  },
88
93
 
89
- executeNode(params: {
94
+ executeNode: Effect.fn('SystemExecutor.executeNode')(function* (params: {
90
95
  nodeSpec: PlanNodeSpec
91
96
  resolvedInput: Record<string, unknown>
92
97
  context: OwnershipDispatchContext
93
98
  }) {
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
- }
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
+ }
101
105
 
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
- }
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
+ }
108
112
 
109
- return yield* Effect.tryPromise(() =>
113
+ return yield* trySystemExecutorPromise(
114
+ () =>
110
115
  executor.executeNode(
111
116
  buildSystemExecutionParams({
112
117
  owner,
@@ -115,16 +120,16 @@ export function makeSystemExecutorService(config: ResolvedLotaRuntimeConfig) {
115
120
  context: params.context,
116
121
  }),
117
122
  ),
118
- )
119
- })
120
- },
123
+ `System executor "${owner.ref}.${owner.operation}" failed.`,
124
+ ).pipe(Effect.withSpan('SystemExecutor.invoke'))
125
+ }),
121
126
  }
122
127
  }
123
128
 
124
129
  export class SystemExecutorServiceTag extends Context.Service<
125
130
  SystemExecutorServiceTag,
126
131
  ReturnType<typeof makeSystemExecutorService>
127
- >()('SystemExecutorService') {}
132
+ >()('@lota-sdk/core/SystemExecutorService') {}
128
133
 
129
134
  export const SystemExecutorServiceLive = Layer.effect(
130
135
  SystemExecutorServiceTag,
@@ -1,13 +1,14 @@
1
1
  import { THREAD } from '@lota-sdk/shared'
2
2
  import { Effect } from 'effect'
3
3
 
4
- import { getAgentDisplayNames, getAgentRoster, getCoreThreadProfile } from '../../config/agent-defaults'
4
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
5
5
  import { serverLogger } from '../../config/logger'
6
- import { getThreadBootstrapConfig } from '../../config/thread-defaults'
6
+ import type { ResolvedThreadBootstrapConfig } from '../../config/thread-defaults'
7
7
  import type { RecordIdRef } from '../../db/record-id'
8
8
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
9
9
  import { TABLES } from '../../db/tables'
10
10
  import { BadRequestError, DatabaseError } from '../../effect/errors'
11
+ import type { LockAcquisitionError, LockLostError, NotFoundError, RedisError, ServiceError } from '../../effect/errors'
11
12
  import type { RedisConnectionManager } from '../../redis/connection'
12
13
  import { withLeaseLock } from '../../redis/redis-lease-lock'
13
14
  import type { makeThreadMessageService } from './thread-message.service'
@@ -19,8 +20,8 @@ const THREAD_BOOTSTRAP_LOCK_REFRESH_INTERVAL_MS = 5_000
19
20
  const THREAD_BOOTSTRAP_LOCK_MAX_WAIT_MS = 5_000
20
21
  const THREAD_BOOTSTRAP_LOCK_RETRY_DELAY_MS = 100
21
22
 
22
- function getAgentDisplayName(agentId: string): string {
23
- return getAgentDisplayNames()[agentId] ?? agentId
23
+ function getAgentDisplayName(agentConfig: ResolvedAgentConfig, agentId: string): string {
24
+ return agentConfig.displayNames[agentId] ?? agentId
24
25
  }
25
26
 
26
27
  function buildBootstrapThreadsLockKey(userId: RecordIdRef, orgId: RecordIdRef): string {
@@ -31,9 +32,26 @@ function haveSameMembers(left: string[], right: string[]): boolean {
31
32
  return left.length === right.length && left.every((value, index) => value === right[index])
32
33
  }
33
34
 
34
- type NormalizedThreadFactory = (thread: ThreadRecord) => Effect.Effect<NormalizedThread, unknown>
35
+ type ThreadNormalizationError = BadRequestError | ServiceError
36
+ type ThreadBootstrapError =
37
+ | BadRequestError
38
+ | DatabaseError
39
+ | LockAcquisitionError
40
+ | LockLostError
41
+ | NotFoundError
42
+ | RedisError
43
+ | ServiceError
44
+
45
+ type NormalizedThreadFactory = (thread: ThreadRecord) => Effect.Effect<NormalizedThread, ThreadNormalizationError>
46
+ type DuplicateCreateErrorLike = { cause?: unknown; message?: unknown }
47
+
48
+ function isDuplicateCreateErrorLike(value: unknown): value is DuplicateCreateErrorLike {
49
+ return (typeof value === 'object' || typeof value === 'function') && value !== null
50
+ }
35
51
 
36
52
  export function createThreadBootstrapHelpers(deps: {
53
+ agentConfig: ResolvedAgentConfig
54
+ threadBootstrapConfig: ResolvedThreadBootstrapConfig
37
55
  threadStore: ThreadRecordStore
38
56
  threadMessageService: Pick<ReturnType<typeof makeThreadMessageService>, 'ensureBootstrapWelcomeMessageEffect'>
39
57
  redis: RedisConnectionManager
@@ -85,26 +103,26 @@ export function createThreadBootstrapHelpers(deps: {
85
103
  const seen = new Set<unknown>()
86
104
  let current = error
87
105
 
88
- while ((typeof current === 'object' || typeof current === 'function') && current !== null && !seen.has(current)) {
106
+ while (isDuplicateCreateErrorLike(current) && !seen.has(current)) {
89
107
  seen.add(current)
90
- const message = Reflect.get(current, 'message')
91
- if (typeof message === 'string' && message.includes('already contains')) {
108
+ if (typeof current.message === 'string' && current.message.includes('already contains')) {
92
109
  return true
93
110
  }
94
- current = Reflect.get(current, 'cause')
111
+ current = current.cause
95
112
  }
96
113
 
97
114
  return false
98
115
  }
99
116
 
100
- const normalizeThreadEffect = (thread: ThreadRecord) => deps.normalizeThread(thread)
117
+ const normalizeThreadEffect = (thread: ThreadRecord): Effect.Effect<NormalizedThread, ThreadNormalizationError> =>
118
+ deps.normalizeThread(thread)
101
119
 
102
120
  const getOrCreateDefaultEffect = (
103
121
  orgId: RecordIdRef,
104
122
  userId: RecordIdRef,
105
123
  agentId: string,
106
124
  config?: { title?: string; nameGenerated?: boolean },
107
- ) => {
125
+ ): Effect.Effect<{ created: boolean; record: ThreadRecord }, DatabaseError | NotFoundError> => {
108
126
  const lookup = { type: 'default' as const, organizationId: orgId, userId, agentId }
109
127
 
110
128
  return Effect.gen(function* () {
@@ -121,7 +139,7 @@ export function createThreadBootstrapHelpers(deps: {
121
139
  userId,
122
140
  agentId,
123
141
  members: [agentId],
124
- title: config?.title ?? getAgentDisplayName(agentId),
142
+ title: config?.title ?? getAgentDisplayName(deps.agentConfig, agentId),
125
143
  status: 'active',
126
144
  nameGenerated: config?.nameGenerated ?? false,
127
145
  isCompacting: false,
@@ -150,7 +168,7 @@ export function createThreadBootstrapHelpers(deps: {
150
168
  userId: RecordIdRef,
151
169
  threadType: string,
152
170
  config: { members: string[]; title: string; nameGenerated?: boolean },
153
- ) => {
171
+ ): Effect.Effect<{ created: boolean; record: ThreadRecord }, DatabaseError | NotFoundError> => {
154
172
  const lookup = { type: 'thread' as const, organizationId: orgId, userId, threadType }
155
173
 
156
174
  return Effect.gen(function* () {
@@ -199,7 +217,7 @@ export function createThreadBootstrapHelpers(deps: {
199
217
  threadType?: string
200
218
  members?: string[]
201
219
  title?: string
202
- }) =>
220
+ }): Effect.Effect<NormalizedThread, ThreadBootstrapError> =>
203
221
  Effect.gen(function* () {
204
222
  switch (input.type) {
205
223
  case 'default':
@@ -234,7 +252,7 @@ export function createThreadBootstrapHelpers(deps: {
234
252
  const threadType = input.threadType
235
253
  if (!threadType) return yield* new BadRequestError({ message: 'Thread threads require threadType' })
236
254
  const { record } = yield* getOrCreateThreadEffect(input.organizationId, input.userId, threadType, {
237
- members: input.members ?? [...getAgentRoster()],
255
+ members: input.members ?? [...deps.agentConfig.roster],
238
256
  title,
239
257
  nameGenerated,
240
258
  })
@@ -247,7 +265,7 @@ export function createThreadBootstrapHelpers(deps: {
247
265
  type: input.type,
248
266
  agentId: input.agentId,
249
267
  threadType: input.threadType,
250
- members: input.members ?? [...getAgentRoster()],
268
+ members: input.members ?? [...deps.agentConfig.roster],
251
269
  title,
252
270
  status: 'active',
253
271
  nameGenerated,
@@ -262,8 +280,8 @@ export function createThreadBootstrapHelpers(deps: {
262
280
  userId: RecordIdRef,
263
281
  orgId: RecordIdRef,
264
282
  options?: { onboardStatus?: string; userName?: string | null },
265
- ) => {
266
- const bootstrapConfig = getThreadBootstrapConfig()
283
+ ): Effect.Effect<void, ThreadBootstrapError> => {
284
+ const bootstrapConfig = deps.threadBootstrapConfig
267
285
 
268
286
  return withLeaseLock(
269
287
  {
@@ -326,7 +344,7 @@ export function createThreadBootstrapHelpers(deps: {
326
344
  if (onboardingCompleted) {
327
345
  for (const wsType of bootstrapConfig.threadTypesAfterOnboarding) {
328
346
  if (threadThreadsByType.has(wsType)) continue
329
- const profile = getCoreThreadProfile(wsType)
347
+ const profile = deps.agentConfig.getCoreThreadProfile(wsType)
330
348
  yield* getOrCreateThreadEffect(orgId, userId, wsType, {
331
349
  members: [...profile.members],
332
350
  title: profile.config.title,
@@ -5,6 +5,7 @@ import { BoundQuery } from 'surrealdb'
5
5
  import type { RecordIdRef } from '../../db/record-id'
6
6
  import type { SurrealDBService } from '../../db/service'
7
7
  import { TABLES } from '../../db/tables'
8
+ import type { BadRequestError, ServiceError } from '../../effect/errors'
8
9
  import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
9
10
  import { ThreadSchema } from './thread.types'
10
11
  import type { NormalizedThread, ThreadRecord } from './thread.types'
@@ -38,7 +39,7 @@ export function createThreadListingHelpers(deps: {
38
39
  normalizeThreads(
39
40
  threads: ThreadRecord[],
40
41
  options?: { checkLease?: boolean },
41
- ): Effect.Effect<NormalizedThread[], unknown>
42
+ ): Effect.Effect<NormalizedThread[], BadRequestError | ServiceError>
42
43
  }) {
43
44
  class ThreadListingError extends Schema.TaggedErrorClass<ThreadListingError>()('ThreadListingError', {
44
45
  message: Schema.String,
@@ -1,3 +1,4 @@
1
+ import type { Context } from 'effect'
1
2
  import { Schema, Effect } from 'effect'
2
3
 
3
4
  import { serverLogger } from '../../config/logger'
@@ -9,16 +10,19 @@ import {
9
10
  appendToMemoryBlock,
10
11
  compactMemoryBlockEntries,
11
12
  formatPersistedMemoryBlockForPrompt,
13
+ MemoryBlockCompactError,
12
14
  parseMemoryBlock,
13
15
  serializeMemoryBlock,
14
16
  } from '../../runtime/memory/memory-block'
17
+ import type { BackgroundWorkService } from '../background-work.service'
18
+ import type { makeContextCompactionService } from '../context-compaction.service'
15
19
  import { MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES, MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES } from './thread-constants'
16
20
  import type { ThreadRecordStore } from './thread-record-store'
17
21
  import type { ThreadRecord } from './thread.types'
18
22
 
19
- type ContextCompactionServiceLike = {
20
- compactMemoryBlock(params: { previousSummary: string; newEntriesText: string }): Effect.Effect<string, unknown>
21
- }
23
+ type ContextCompactionServiceLike = Pick<ReturnType<typeof makeContextCompactionService>, 'compactMemoryBlock'>
24
+
25
+ type BackgroundWorker = Context.Service.Shape<typeof BackgroundWorkService>
22
26
 
23
27
  class ThreadMemoryBlockError extends Schema.TaggedErrorClass<ThreadMemoryBlockError>()('ThreadMemoryBlockError', {
24
28
  message: Schema.String,
@@ -39,6 +43,7 @@ export function formatMemoryBlockForPrompt(thread: Pick<ThreadRecord, 'memoryBlo
39
43
  export function createThreadMemoryBlockHelpers(deps: {
40
44
  threadStore: ThreadRecordStore
41
45
  contextCompactionService: ContextCompactionServiceLike
46
+ background: BackgroundWorker
42
47
  }) {
43
48
  function appendMemoryBlock(threadId: RecordIdRef, entry: string): Effect.Effect<string, ThreadMemoryBlockError> {
44
49
  return Effect.gen(function* () {
@@ -63,7 +68,7 @@ export function createThreadMemoryBlockHelpers(deps: {
63
68
  )
64
69
 
65
70
  if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
66
- yield* Effect.forkDetach(
71
+ yield* deps.background.run(
67
72
  compactMemoryBlock(threadRef).pipe(
68
73
  Effect.catch((error: unknown) =>
69
74
  Effect.sync(() => {
@@ -71,6 +76,7 @@ export function createThreadMemoryBlockHelpers(deps: {
71
76
  }),
72
77
  ),
73
78
  ),
79
+ 'thread-memory-block.compactMemoryBlock',
74
80
  )
75
81
  }
76
82
 
@@ -93,7 +99,14 @@ export function createThreadMemoryBlockHelpers(deps: {
93
99
  entries: parseMemoryBlock(thread.memoryBlock),
94
100
  triggerEntries: MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
95
101
  chunkEntries: MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
96
- compact: (params) => deps.contextCompactionService.compactMemoryBlock(params),
102
+ compact: (params) =>
103
+ deps.contextCompactionService
104
+ .compactMemoryBlock(params)
105
+ .pipe(
106
+ Effect.mapError(
107
+ (cause) => new MemoryBlockCompactError({ message: 'compact callback failed', cause }),
108
+ ),
109
+ ),
97
110
  }),
98
111
  `Failed to compact memory block for thread ${threadIdString}`,
99
112
  )
@@ -3,8 +3,9 @@ import type { ChatMessage } from '@lota-sdk/shared'
3
3
  import { Context, Effect, Layer } from 'effect'
4
4
  import { RecordId, surql } from 'surrealdb'
5
5
  import { z } from 'zod'
6
+ import type { ZodTypeAny } from 'zod'
6
7
 
7
- import { getAgentDisplayNames } from '../../config/agent-defaults'
8
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
8
9
  import { CursorRowSchema, listMessageHistoryPageEffect } from '../../db/cursor-pagination'
9
10
  import type { CursorPaginationConfig } from '../../db/cursor-pagination'
10
11
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
@@ -15,12 +16,23 @@ import { ThreadMessageRowSchema } from '../../db/thread-message-row'
15
16
  import type { ThreadMessageRow } from '../../db/thread-message-row'
16
17
  import { ServiceError } from '../../effect/errors'
17
18
  import { effectTryServicePromise } from '../../effect/helpers'
18
- import { DatabaseServiceTag } from '../../effect/services'
19
+ import { AgentConfigServiceTag, DatabaseServiceTag } from '../../effect/services'
19
20
  import { sha256Hex } from '../../utils/crypto'
20
21
  import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
21
22
 
22
23
  const ThreadMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
23
24
 
25
+ function parseRowOrFail<TSchema extends ZodTypeAny>(
26
+ schema: TSchema,
27
+ value: unknown,
28
+ operation: string,
29
+ ): Effect.Effect<z.infer<TSchema>, ServiceError> {
30
+ return Effect.try({
31
+ try: () => schema.parse(value) as z.infer<TSchema>,
32
+ catch: (cause) => new ServiceError({ message: `Failed to parse row for ${operation}.`, cause }),
33
+ })
34
+ }
35
+
24
36
  function toMessageId(value: string | RecordIdRef): string {
25
37
  return recordIdToString(value, TABLES.THREAD_MESSAGE)
26
38
  }
@@ -94,7 +106,7 @@ const threadPaginationConfig: CursorPaginationConfig = {
94
106
  `,
95
107
  }
96
108
 
97
- export function makeThreadMessageService(db: SurrealDBService) {
109
+ export function makeThreadMessageService(db: SurrealDBService, agentConfig: ResolvedAgentConfig) {
98
110
  function upsertMessages(params: { threadId: RecordIdRef; messages: ChatMessage[] }) {
99
111
  const threadId = toThreadRef(params.threadId)
100
112
  return Effect.forEach(params.messages, (message) =>
@@ -160,7 +172,10 @@ export function makeThreadMessageService(db: SurrealDBService) {
160
172
  `),
161
173
  'Failed to list thread messages.',
162
174
  ).pipe(
163
- Effect.map((rows) => rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))),
175
+ Effect.flatMap((rows) =>
176
+ Effect.forEach(rows, (row) => parseRowOrFail(ThreadMessageRowSchema, row, 'listMessages')),
177
+ ),
178
+ Effect.map((rows) => rows.map((row) => toChatMessage(row))),
164
179
  )
165
180
  },
166
181
  listMessagesEffect(threadId: RecordIdRef) {
@@ -210,7 +225,10 @@ export function makeThreadMessageService(db: SurrealDBService) {
210
225
  `),
211
226
  'Failed to list thread messages after cursor.',
212
227
  )
213
- return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
228
+ const parsedRows = yield* Effect.forEach(rows, (row) =>
229
+ parseRowOrFail(ThreadMessageRowSchema, row, 'listMessagesAfterCursor'),
230
+ )
231
+ return parsedRows.map((row) => toChatMessage(row))
214
232
  })
215
233
  },
216
234
  listMessagesAfterCursorEffect(threadId: RecordIdRef, afterMessageId?: string) {
@@ -229,12 +247,10 @@ export function makeThreadMessageService(db: SurrealDBService) {
229
247
  `),
230
248
  'Failed to list recent thread messages.',
231
249
  ).pipe(
232
- Effect.map((rows) =>
233
- rows
234
- .map((row) => ThreadMessageRowSchema.parse(row))
235
- .reverse()
236
- .map((row) => toChatMessage(row)),
250
+ Effect.flatMap((rows) =>
251
+ Effect.forEach(rows, (row) => parseRowOrFail(ThreadMessageRowSchema, row, 'listRecentMessages')),
237
252
  ),
253
+ Effect.map((rows) => rows.reverse().map((row) => toChatMessage(row))),
238
254
  )
239
255
  },
240
256
  listRecentMessagesEffect(threadId: RecordIdRef, limit: number) {
@@ -333,7 +349,7 @@ export function makeThreadMessageService(db: SurrealDBService) {
333
349
  parts: [{ type: 'text', text: messageText }],
334
350
  metadata: {
335
351
  agentId: params.agentId,
336
- agentName: getAgentDisplayNames()[params.agentId] ?? params.agentId,
352
+ agentName: agentConfig.displayNames[params.agentId] ?? params.agentId,
337
353
  createdAt: nowEpochMillis(),
338
354
  },
339
355
  },
@@ -352,12 +368,13 @@ export function makeThreadMessageService(db: SurrealDBService) {
352
368
  export class ThreadMessageServiceTag extends Context.Service<
353
369
  ThreadMessageServiceTag,
354
370
  ReturnType<typeof makeThreadMessageService>
355
- >()('ThreadMessageService') {}
371
+ >()('@lota-sdk/core/ThreadMessageService') {}
356
372
 
357
373
  export const ThreadMessageServiceLive = Layer.effect(
358
374
  ThreadMessageServiceTag,
359
375
  Effect.gen(function* () {
360
376
  const db = yield* DatabaseServiceTag
361
- return makeThreadMessageService(db)
377
+ const agentConfig = yield* AgentConfigServiceTag
378
+ return makeThreadMessageService(db, agentConfig)
362
379
  }),
363
380
  )
@@ -62,7 +62,7 @@ export function makeThreadTitleService(
62
62
  export class ThreadTitleServiceTag extends Context.Service<
63
63
  ThreadTitleServiceTag,
64
64
  ReturnType<typeof makeThreadTitleService>
65
- >()('ThreadTitleService') {}
65
+ >()('@lota-sdk/core/ThreadTitleService') {}
66
66
 
67
67
  export const ThreadTitleServiceLive = Layer.effect(
68
68
  ThreadTitleServiceTag,