@lota-sdk/core 0.4.13 → 0.4.14

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 (138) hide show
  1. package/package.json +4 -4
  2. package/src/ai/embedding-cache.ts +17 -11
  3. package/src/ai-gateway/ai-gateway.ts +164 -94
  4. package/src/ai-gateway/index.ts +4 -1
  5. package/src/config/agent-defaults.ts +2 -2
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/create-runtime.ts +259 -200
  8. package/src/db/cursor-pagination.ts +2 -9
  9. package/src/db/memory-store.ts +194 -175
  10. package/src/db/memory.ts +125 -71
  11. package/src/db/schema-fingerprint.ts +5 -4
  12. package/src/db/service-normalization.ts +4 -3
  13. package/src/db/service.ts +3 -2
  14. package/src/db/startup.ts +15 -16
  15. package/src/effect/errors.ts +161 -21
  16. package/src/effect/index.ts +0 -1
  17. package/src/embeddings/provider.ts +15 -7
  18. package/src/queues/autonomous-job.queue.ts +10 -22
  19. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  20. package/src/queues/document-processor.queue.ts +13 -4
  21. package/src/queues/memory-consolidation.queue.ts +26 -14
  22. package/src/queues/plan-agent-heartbeat.queue.ts +10 -9
  23. package/src/queues/plan-scheduler.queue.ts +37 -15
  24. package/src/queues/queue-factory.ts +59 -35
  25. package/src/queues/standalone-worker.ts +3 -2
  26. package/src/redis/connection.ts +10 -3
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +5 -5
  29. package/src/redis/stream-context.ts +1 -1
  30. package/src/runtime/chat-message.ts +64 -1
  31. package/src/runtime/chat-run-orchestration.ts +33 -20
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  33. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  34. package/src/runtime/domain-layer.ts +13 -7
  35. package/src/runtime/execution-plan.ts +7 -3
  36. package/src/runtime/memory/memory-block.ts +3 -9
  37. package/src/runtime/memory/memory-scope.ts +3 -1
  38. package/src/runtime/plugin-resolution.ts +2 -1
  39. package/src/runtime/post-turn-side-effects.ts +6 -5
  40. package/src/runtime/retrieval-adapters.ts +8 -20
  41. package/src/runtime/runtime-config.ts +3 -9
  42. package/src/runtime/runtime-extensions.ts +2 -4
  43. package/src/runtime/runtime-lifecycle.ts +56 -16
  44. package/src/runtime/runtime-services.ts +180 -102
  45. package/src/runtime/runtime-worker-registry.ts +3 -1
  46. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  47. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  48. package/src/runtime/social-chat/social-chat.ts +356 -223
  49. package/src/runtime/specialist-runner.ts +3 -1
  50. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  51. package/src/runtime/thread-turn-context.ts +142 -102
  52. package/src/runtime/turn-lifecycle.ts +15 -46
  53. package/src/services/agent-activity.service.ts +1 -1
  54. package/src/services/agent-executor.service.ts +107 -77
  55. package/src/services/autonomous-job.service.ts +354 -293
  56. package/src/services/background-work.service.ts +3 -3
  57. package/src/services/context-compaction.service.ts +7 -2
  58. package/src/services/document-chunk.service.ts +50 -32
  59. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  60. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  61. package/src/services/feedback-loop.service.ts +5 -4
  62. package/src/services/graph-full-routing.ts +37 -36
  63. package/src/services/institutional-memory.service.ts +28 -30
  64. package/src/services/learned-skill.service.ts +107 -72
  65. package/src/services/memory/memory-errors.ts +4 -23
  66. package/src/services/memory/memory-org-memory.ts +10 -5
  67. package/src/services/memory/memory-rerank.ts +18 -6
  68. package/src/services/memory/memory.service.ts +170 -111
  69. package/src/services/memory/rerank.service.ts +29 -20
  70. package/src/services/organization-member.service.ts +1 -1
  71. package/src/services/organization.service.ts +69 -75
  72. package/src/services/ownership-dispatcher.service.ts +40 -39
  73. package/src/services/plan/plan-agent-heartbeat.service.ts +26 -23
  74. package/src/services/plan/plan-agent-query.service.ts +39 -31
  75. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  76. package/src/services/plan/plan-coordination.service.ts +2 -1
  77. package/src/services/plan/plan-cycle.service.ts +6 -5
  78. package/src/services/plan/plan-deadline.service.ts +57 -54
  79. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  80. package/src/services/plan/plan-executor-graph.ts +18 -15
  81. package/src/services/plan/plan-executor.service.ts +235 -262
  82. package/src/services/plan/plan-run.service.ts +169 -93
  83. package/src/services/plan/plan-scheduler.service.ts +192 -202
  84. package/src/services/plan/plan-template.service.ts +1 -1
  85. package/src/services/plan/plan-transaction-events.ts +1 -1
  86. package/src/services/plan/plan-workspace.service.ts +23 -14
  87. package/src/services/plugin-executor.service.ts +5 -9
  88. package/src/services/queue-job.service.ts +117 -59
  89. package/src/services/recent-activity-title.service.ts +13 -12
  90. package/src/services/recent-activity.service.ts +6 -1
  91. package/src/services/social-chat-history.service.ts +29 -25
  92. package/src/services/system-executor.service.ts +5 -9
  93. package/src/services/thread/thread-active-run.ts +2 -2
  94. package/src/services/thread/thread-listing.ts +61 -57
  95. package/src/services/thread/thread-memory-block.ts +73 -48
  96. package/src/services/thread/thread-message.service.ts +76 -65
  97. package/src/services/thread/thread-record-store.ts +8 -8
  98. package/src/services/thread/thread-title.service.ts +10 -4
  99. package/src/services/thread/thread-turn-execution.ts +43 -45
  100. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  101. package/src/services/thread/thread-turn-streaming.ts +82 -85
  102. package/src/services/thread/thread-turn.ts +8 -8
  103. package/src/services/thread/thread.service.ts +135 -100
  104. package/src/services/user.service.ts +45 -48
  105. package/src/storage/attachment-parser.ts +6 -2
  106. package/src/storage/attachment-storage.service.ts +5 -6
  107. package/src/storage/generated-document-storage.service.ts +1 -1
  108. package/src/system-agents/context-compaction.agent.ts +10 -9
  109. package/src/system-agents/delegated-agent-factory.ts +30 -6
  110. package/src/system-agents/memory-reranker.agent.ts +10 -9
  111. package/src/system-agents/memory.agent.ts +10 -9
  112. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  113. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  114. package/src/system-agents/skill-extractor.agent.ts +13 -12
  115. package/src/system-agents/skill-manager.agent.ts +13 -12
  116. package/src/system-agents/thread-router.agent.ts +10 -5
  117. package/src/system-agents/title-generator.agent.ts +13 -12
  118. package/src/tools/fetch-webpage.tool.ts +13 -13
  119. package/src/tools/memory-block.tool.ts +3 -1
  120. package/src/tools/plan-approval.tool.ts +4 -2
  121. package/src/tools/read-file-parts.tool.ts +10 -4
  122. package/src/tools/remember-memory.tool.ts +3 -1
  123. package/src/tools/research-topic.tool.ts +9 -5
  124. package/src/tools/search-web.tool.ts +16 -16
  125. package/src/tools/search.tool.ts +20 -5
  126. package/src/tools/team-think.tool.ts +61 -38
  127. package/src/utils/async.ts +5 -5
  128. package/src/utils/errors.ts +19 -18
  129. package/src/utils/sse-keepalive.ts +28 -25
  130. package/src/workers/bootstrap.ts +75 -11
  131. package/src/workers/memory-consolidation.worker.ts +82 -91
  132. package/src/workers/organization-learning.worker.ts +14 -4
  133. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  134. package/src/workers/skill-extraction.runner.ts +97 -61
  135. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  136. package/src/workers/utils/thread-message-query.ts +24 -24
  137. package/src/workers/worker-utils.ts +23 -4
  138. package/src/effect/helpers.ts +0 -123
@@ -4,10 +4,15 @@ import type { Context } from 'effect'
4
4
  import type IORedis from 'ioredis'
5
5
 
6
6
  import { serverLogger } from '../config/logger'
7
+ import { ERROR_TAGS } from '../effect/errors'
7
8
  import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
9
+ import type { makePlanCycleService } from '../services/plan/plan-cycle.service'
8
10
  import { PlanCycleServiceTag } from '../services/plan/plan-cycle.service'
11
+ import type { makePlanDeadlineService } from '../services/plan/plan-deadline.service'
9
12
  import { PlanDeadlineServiceTag } from '../services/plan/plan-deadline.service'
13
+ import type { makePlanExecutorService } from '../services/plan/plan-executor.service'
10
14
  import { PlanExecutorServiceTag } from '../services/plan/plan-executor.service'
15
+ import type { makePlanSchedulerService } from '../services/plan/plan-scheduler.service'
11
16
  import { PlanSchedulerServiceTag } from '../services/plan/plan-scheduler.service'
12
17
  import { QueueJobServiceTag } from '../services/queue-job.service'
13
18
  import { nowEpochMillis } from '../utils/date-time'
@@ -15,6 +20,20 @@ import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
15
20
  import { createQueueFactoryWithDeps } from './queue-factory'
16
21
  import { runStandaloneQueueWorker } from './standalone-worker'
17
22
 
23
+ type PlanSchedulerWorkerServiceMethods = Pick<
24
+ ReturnType<typeof makePlanSchedulerService>,
25
+ 'fireScheduleById' | 'recoverActiveSchedules'
26
+ >
27
+
28
+ type PlanDeadlineWorkerServiceMethods = Pick<
29
+ ReturnType<typeof makePlanDeadlineService>,
30
+ 'checkDeadlines' | 'recoverDeadlineChecks'
31
+ >
32
+
33
+ type PlanExecutorWorkerServiceMethods = Pick<ReturnType<typeof makePlanExecutorService>, 'promoteDelayedNode'>
34
+
35
+ type PlanCycleWorkerServiceMethods = Pick<ReturnType<typeof makePlanCycleService>, 'advanceCycle'>
36
+
18
37
  export interface PlanSchedulerFireJob {
19
38
  type: 'fire-schedule'
20
39
  scheduleId: string
@@ -31,17 +50,21 @@ export const PLAN_SCHEDULER_QUEUE = 'plan-scheduler'
31
50
 
32
51
  export interface PlanSchedulerWorkerDeps {
33
52
  databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
34
- planSchedulerService: Context.Service.Shape<typeof PlanSchedulerServiceTag>
35
- planDeadlineService: Context.Service.Shape<typeof PlanDeadlineServiceTag>
36
- planExecutorService: Context.Service.Shape<typeof PlanExecutorServiceTag>
37
- planCycleService: Context.Service.Shape<typeof PlanCycleServiceTag>
53
+ runPromise: <A, E, R>(effect: Effect.Effect<A, E, R>) => Promise<A>
54
+ planSchedulerService: PlanSchedulerWorkerServiceMethods
55
+ planDeadlineService: PlanDeadlineWorkerServiceMethods
56
+ planExecutorService: PlanExecutorWorkerServiceMethods
57
+ planCycleService: PlanCycleWorkerServiceMethods
38
58
  }
39
59
 
40
- class PlanSchedulerQueueError extends Schema.TaggedErrorClass<PlanSchedulerQueueError>()('PlanSchedulerQueueError', {
41
- stage: Schema.Literals(['remove-schedule-fire-job', 'recover-active-schedules', 'recover-deadline-checks']),
42
- message: Schema.String,
43
- cause: Schema.Defect,
44
- }) {}
60
+ class PlanSchedulerQueueError extends Schema.TaggedErrorClass<PlanSchedulerQueueError>()(
61
+ ERROR_TAGS.PlanSchedulerQueueError,
62
+ {
63
+ stage: Schema.Literals(['remove-schedule-fire-job', 'recover-active-schedules', 'recover-deadline-checks']),
64
+ message: Schema.String,
65
+ cause: Schema.Defect,
66
+ },
67
+ ) {}
45
68
 
46
69
  function toPlanSchedulerQueueError(stage: PlanSchedulerQueueError['stage'], cause: unknown): PlanSchedulerQueueError {
47
70
  return new PlanSchedulerQueueError({ stage, message: cause instanceof Error ? cause.message : String(cause), cause })
@@ -49,10 +72,8 @@ function toPlanSchedulerQueueError(stage: PlanSchedulerQueueError['stage'], caus
49
72
 
50
73
  function processPlanSchedulerJob(deps: PlanSchedulerWorkerDeps, job: Job<PlanSchedulerJob>): Promise<void> {
51
74
  const { planSchedulerService, planDeadlineService, planExecutorService, planCycleService } = deps
52
- const runWithResolvedContext = <A, E>(effect: Effect.Effect<A, E, unknown>): Promise<void> =>
53
- // Service Effects carry their provided context through the managed runtime
54
- // that resolved these service tags, so residual R collapses at runtime.
55
- Effect.runPromise(Effect.asVoid(effect as Effect.Effect<A, E, never>))
75
+ const runWithResolvedContext = <A, E, R>(effect: Effect.Effect<A, E, R>): Promise<void> =>
76
+ deps.runPromise(Effect.asVoid(effect))
56
77
 
57
78
  switch (job.data.type) {
58
79
  case 'fire-schedule':
@@ -132,7 +153,7 @@ export function makePlanSchedulerQueueRuntime(params: MakePlanSchedulerQueueRunt
132
153
  void Effect.runFork(
133
154
  deps.planSchedulerService.recoverActiveSchedules().pipe(
134
155
  Effect.mapError((cause) => toPlanSchedulerQueueError('recover-active-schedules', cause)),
135
- Effect.catchTag('PlanSchedulerQueueError', (error) =>
156
+ Effect.catchTag(ERROR_TAGS.PlanSchedulerQueueError, (error) =>
136
157
  Effect.sync(() => {
137
158
  serverLogger.error`Plan scheduler startup recovery failed: ${error.message}`
138
159
  }),
@@ -144,7 +165,7 @@ export function makePlanSchedulerQueueRuntime(params: MakePlanSchedulerQueueRunt
144
165
  void Effect.runFork(
145
166
  deps.planDeadlineService.recoverDeadlineChecks().pipe(
146
167
  Effect.mapError((cause) => toPlanSchedulerQueueError('recover-deadline-checks', cause)),
147
- Effect.catchTag('PlanSchedulerQueueError', (error) =>
168
+ Effect.catchTag(ERROR_TAGS.PlanSchedulerQueueError, (error) =>
148
169
  Effect.sync(() => {
149
170
  serverLogger.error`Plan deadline recovery failed: ${error.message}`
150
171
  }),
@@ -168,6 +189,7 @@ runStandaloneQueueWorker((runtime) => {
168
189
  planSchedulerQueue.startWorker({
169
190
  deps: {
170
191
  databaseService: resolve(DatabaseServiceTag),
192
+ runPromise: (effect) => runtime.runPromise(effect),
171
193
  planSchedulerService: resolve(PlanSchedulerServiceTag),
172
194
  planDeadlineService: resolve(PlanDeadlineServiceTag),
173
195
  planExecutorService: resolve(PlanExecutorServiceTag),
@@ -5,6 +5,7 @@ import type IORedis from 'ioredis'
5
5
 
6
6
  import type { LotaLogger } from '../config/logger'
7
7
  import { serverLogger } from '../config/logger'
8
+ import { ERROR_TAGS } from '../effect/errors'
8
9
  import type { TrackedBullJobLike } from '../services/queue-job.service'
9
10
  import {
10
11
  attachWorkerEvents,
@@ -15,7 +16,7 @@ import {
15
16
  } from '../workers/worker-utils'
16
17
  import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
17
18
 
18
- class QueueFactoryError extends Schema.TaggedErrorClass<QueueFactoryError>()('@lota-sdk/core/QueueFactoryError', {
19
+ class QueueFactoryError extends Schema.TaggedErrorClass<QueueFactoryError>()(ERROR_TAGS.QueueFactoryError, {
19
20
  message: Schema.String,
20
21
  cause: Schema.optional(Schema.Defect),
21
22
  }) {}
@@ -148,7 +149,7 @@ function createQueueFactoryRuntime<TJob>(config: QueueFactoryConfigBase): {
148
149
  return Reflect.get(target, property, receiver) as unknown
149
150
  }
150
151
 
151
- const value = (target as unknown as Record<string, unknown>)[property]
152
+ const value = Reflect.get(target, property, receiver) as unknown
152
153
  if (typeof value !== 'function' || !queueMethodsThatWaitForClose.has(property as QueueMethod)) {
153
154
  return value
154
155
  }
@@ -229,46 +230,69 @@ function createQueueFactoryRuntime<TJob>(config: QueueFactoryConfigBase): {
229
230
  ...(config.maxStalledCount !== undefined ? { maxStalledCount: config.maxStalledCount } : {}),
230
231
  }
231
232
 
232
- const worker = workerConfig.processorPath
233
- ? new Worker(config.name, workerConfig.processorPath, workerOptions)
234
- : new Worker(
235
- config.name,
236
- createTracedWorkerProcessor(
233
+ let worker: Worker
234
+ try {
235
+ worker = workerConfig.processorPath
236
+ ? new Worker(config.name, workerConfig.processorPath, workerOptions)
237
+ : new Worker(
237
238
  config.name,
238
- (job) =>
239
- Effect.runPromise(
240
- Effect.gen(function* () {
241
- const inlineWorkerConfig = workerConfig as QueueWorkerConfigInline<TJob>
242
- const typedJob = job as Job<TJob>
243
- const prepare = inlineWorkerConfig.prepare
244
- if (prepare) {
245
- yield* Effect.tryPromise({
246
- try: () => prepare(typedJob),
239
+ createTracedWorkerProcessor(
240
+ config.name,
241
+ (job) =>
242
+ Effect.runPromise(
243
+ Effect.gen(function* () {
244
+ const inlineWorkerConfig = workerConfig as QueueWorkerConfigInline<TJob>
245
+ const typedJob = job as Job<TJob>
246
+ const prepare = inlineWorkerConfig.prepare
247
+ if (prepare) {
248
+ yield* Effect.tryPromise({
249
+ try: () => prepare(typedJob),
250
+ catch: (cause) =>
251
+ new QueueFactoryError({
252
+ message: `Worker prepare failed for queue "${config.name}".`,
253
+ cause,
254
+ }),
255
+ })
256
+ }
257
+ return yield* Effect.tryPromise({
258
+ try: () => inlineWorkerConfig.processor(typedJob),
247
259
  catch: (cause) =>
248
- new QueueFactoryError({ message: `Worker prepare failed for queue "${config.name}".`, cause }),
260
+ new QueueFactoryError({
261
+ message: `Worker processor failed for queue "${config.name}".`,
262
+ cause,
263
+ }),
249
264
  })
250
- }
251
- return yield* Effect.tryPromise({
252
- try: () => inlineWorkerConfig.processor(typedJob),
253
- catch: (cause) =>
254
- new QueueFactoryError({ message: `Worker processor failed for queue "${config.name}".`, cause }),
255
- })
256
- }),
257
- ),
258
- config.queueJobService,
259
- ),
260
- workerOptions,
261
- )
265
+ }),
266
+ ),
267
+ config.queueJobService,
268
+ ),
269
+ workerOptions,
270
+ )
271
+ } catch (error) {
272
+ // Construction threw — nothing to clean up since the Worker is created
273
+ // atomically by BullMQ; just rethrow so the caller can fail loudly.
274
+ logger.error`Failed to construct BullMQ worker "${config.displayName}": ${error}`
275
+ throw error
276
+ }
262
277
 
263
- attachWorkerEvents(worker, config.displayName, logger)
278
+ // Acquire-style setup — if ANY step below throws, best-effort close the
279
+ // already-constructed worker before bubbling the error so no connection
280
+ // leaks.
281
+ try {
282
+ attachWorkerEvents(worker, config.displayName, logger)
283
+ const shutdown = createWorkerShutdown(worker, config.displayName, logger)
264
284
 
265
- const shutdown = createWorkerShutdown(worker, config.displayName, logger)
285
+ if (registerSignals) {
286
+ registerShutdownSignals({ name: config.displayName, shutdown, logger })
287
+ }
266
288
 
267
- if (registerSignals) {
268
- registerShutdownSignals({ name: config.displayName, shutdown, logger })
289
+ return { worker, shutdown }
290
+ } catch (error) {
291
+ void worker.close().catch((closeError: unknown) => {
292
+ logger.warn`Failed to close BullMQ worker "${config.displayName}" after setup error: ${closeError}`
293
+ })
294
+ throw error
269
295
  }
270
-
271
- return { worker, shutdown }
272
296
  }
273
297
 
274
298
  return { getQueue, enqueue, startWorker }
@@ -2,10 +2,11 @@ import type { ManagedRuntime } from 'effect'
2
2
  import { Schema, Effect } from 'effect'
3
3
 
4
4
  import { serverLogger } from '../config/logger'
5
+ import { ERROR_TAGS } from '../effect/errors'
5
6
  import { initializeSandboxedWorkerRuntime } from '../workers/bootstrap'
6
7
 
7
8
  class StandaloneQueueWorkerError extends Schema.TaggedErrorClass<StandaloneQueueWorkerError>()(
8
- 'StandaloneQueueWorkerError',
9
+ ERROR_TAGS.StandaloneQueueWorkerError,
9
10
  { message: Schema.String, cause: Schema.Defect },
10
11
  ) {}
11
12
 
@@ -28,7 +29,7 @@ export function runStandaloneQueueWorker(start: (runtime: ManagedRuntime.Managed
28
29
 
29
30
  yield* Effect.sync(() => start(runtime))
30
31
  }).pipe(
31
- Effect.catchTag('StandaloneQueueWorkerError', (error) =>
32
+ Effect.catchTag(ERROR_TAGS.StandaloneQueueWorkerError, (error) =>
32
33
  Effect.sync(() => {
33
34
  serverLogger.error`Standalone queue worker failed: ${error.message}`
34
35
  process.exit(1)
@@ -3,7 +3,6 @@ import IORedis from 'ioredis'
3
3
  import type { RedisOptions } from 'ioredis'
4
4
 
5
5
  import { RedisError } from '../effect/errors'
6
- import { effectTryServicePromise } from '../effect/helpers'
7
6
  import { getErrorMessage } from '../utils/errors'
8
7
 
9
8
  export interface RedisConnectionLogger {
@@ -147,7 +146,10 @@ function acquireRedisClient(
147
146
 
148
147
  if (client.status !== 'end') {
149
148
  const quitExit = yield* Effect.exit(
150
- effectTryServicePromise(() => client.quit(), 'Failed to close Redis connection manager'),
149
+ Effect.tryPromise({
150
+ try: () => client.quit(),
151
+ catch: (cause) => new RedisError({ message: 'Failed to close Redis connection manager', cause }),
152
+ }),
151
153
  )
152
154
  if (Exit.isFailure(quitExit)) {
153
155
  log(options.logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(quitExit.cause)}`)
@@ -190,7 +192,12 @@ function startHealthCheckFiber(
190
192
  isHealthCheckRunning = true
191
193
  try {
192
194
  if (client.status === 'ready') {
193
- const pingExit = yield* Effect.exit(effectTryServicePromise(() => client.ping(), 'Redis health check failed'))
195
+ const pingExit = yield* Effect.exit(
196
+ Effect.tryPromise({
197
+ try: () => client.ping(),
198
+ catch: (cause) => new RedisError({ message: 'Redis health check failed', cause }),
199
+ }),
200
+ )
194
201
  if (Exit.isFailure(pingExit)) {
195
202
  log(logger, 'warn', `Redis health check failed: ${getErrorMessage(pingExit.cause)}`)
196
203
  state.isHealthy = false
@@ -16,7 +16,7 @@ const ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS = 30_000
16
16
  const ORG_MEMORY_LOCK_MAX_WAIT_MS = 15 * 60 * 1000
17
17
 
18
18
  class OrgMemoryLockCallbackError extends Schema.TaggedErrorClass<OrgMemoryLockCallbackError>()(
19
- 'OrgMemoryLockCallbackError',
19
+ '@lota-sdk/core/OrgMemoryLockCallbackError',
20
20
  { message: Schema.String, cause: Schema.Defect },
21
21
  ) {}
22
22
 
@@ -1,7 +1,7 @@
1
1
  import { Clock, Deferred, Duration, Effect, Random, Schedule } from 'effect'
2
2
  import type IORedis from 'ioredis'
3
3
 
4
- import { LockAcquisitionError, LockLostError, RedisError } from '../effect/errors'
4
+ import { ERROR_TAGS, LockAcquisitionError, LockLostError, RedisError } from '../effect/errors'
5
5
  import { RedisServiceTag } from '../effect/services'
6
6
  import { getErrorMessage } from '../utils/errors'
7
7
 
@@ -119,7 +119,7 @@ function acquireLock(
119
119
  schedule: Schedule.fixed(Duration.millis(options.retryDelayMs)),
120
120
  }).pipe(
121
121
  Effect.asVoid,
122
- Effect.catchTag('LockAcquisitionError', () =>
122
+ Effect.catchTag(ERROR_TAGS.LockAcquisitionError, () =>
123
123
  Effect.fail(new LockAcquisitionError({ lockKey: options.lockKey, maxWaitMs: options.maxWaitMs })),
124
124
  ),
125
125
  )
@@ -170,7 +170,7 @@ function startRefreshFiber(
170
170
  Effect.andThen(
171
171
  Effect.sync(() => {
172
172
  const message =
173
- error._tag === 'LockLostError'
173
+ error._tag === ERROR_TAGS.LockLostError
174
174
  ? `${options.label} refresh was rejected for key ${options.lockKey}`
175
175
  : error.message
176
176
 
@@ -188,8 +188,8 @@ function startRefreshFiber(
188
188
  yield* Effect.sleep(Duration.millis(options.refreshIntervalMs))
189
189
 
190
190
  yield* refreshLock(redis, options, lockValue).pipe(
191
- Effect.catchTag('LockLostError', handleLockLoss),
192
- Effect.catchTag('RedisError', handleLockLoss),
191
+ Effect.catchTag(ERROR_TAGS.LockLostError, handleLockLoss),
192
+ Effect.catchTag(ERROR_TAGS.RedisError, handleLockLoss),
193
193
  )
194
194
  }
195
195
  })
@@ -22,7 +22,7 @@ function toPublisher(client: Redis): Publisher {
22
22
  type SharedSubscriberEvent = { readonly channel: string; readonly message: string }
23
23
 
24
24
  class SharedSubscriberCloseError extends Schema.TaggedErrorClass<SharedSubscriberCloseError>()(
25
- 'SharedSubscriberCloseError',
25
+ '@lota-sdk/core/SharedSubscriberCloseError',
26
26
  { message: Schema.String, cause: Schema.optional(Schema.Defect) },
27
27
  ) {}
28
28
 
@@ -1,4 +1,15 @@
1
- import type { MessagePartLike } from './chat-types'
1
+ import { chatLogger } from '../config/logger'
2
+ import type { ChatMessageLike, MessagePartLike } from './chat-types'
3
+
4
+ const AI_SDK_SUPPORTED_MESSAGE_PART_TYPES = new Set([
5
+ 'text',
6
+ 'reasoning',
7
+ 'source-url',
8
+ 'source-document',
9
+ 'file',
10
+ 'step-start',
11
+ 'dynamic-tool',
12
+ ])
2
13
 
3
14
  export function hasMessageContent(parts: readonly MessagePartLike[]): boolean {
4
15
  for (const part of parts) {
@@ -8,3 +19,55 @@ export function hasMessageContent(parts: readonly MessagePartLike[]): boolean {
8
19
 
9
20
  return false
10
21
  }
22
+
23
+ function isAiSdkSupportedMessagePart(part: MessagePartLike): boolean {
24
+ if (typeof part.type !== 'string') {
25
+ return false
26
+ }
27
+
28
+ return (
29
+ AI_SDK_SUPPORTED_MESSAGE_PART_TYPES.has(part.type) || part.type.startsWith('data-') || part.type.startsWith('tool-')
30
+ )
31
+ }
32
+
33
+ function isSubstantiveAssistantMessagePart(part: MessagePartLike): boolean {
34
+ return isAiSdkSupportedMessagePart(part) && part.type !== 'step-start'
35
+ }
36
+
37
+ function sanitizeAssistantMessageParts<TPart extends MessagePartLike>(parts: readonly TPart[]): TPart[] {
38
+ return parts.filter((part): part is TPart => isSubstantiveAssistantMessagePart(part))
39
+ }
40
+
41
+ export function sanitizePersistedMessages<TMessage extends ChatMessageLike>(messages: readonly TMessage[]): TMessage[] {
42
+ const sanitizedMessages: TMessage[] = []
43
+
44
+ for (const message of messages) {
45
+ const rawMessageId = (message as Record<string, unknown>).id
46
+ const messageId = typeof rawMessageId === 'string' && rawMessageId.trim() ? rawMessageId : 'unknown'
47
+
48
+ if (message.parts.length === 0) {
49
+ chatLogger.warn`Dropping persisted thread message with empty parts during history sanitization (role=${message.role}, id=${messageId})`
50
+ continue
51
+ }
52
+
53
+ if (message.role !== 'assistant') {
54
+ sanitizedMessages.push(message)
55
+ continue
56
+ }
57
+
58
+ const sanitizedParts = sanitizeAssistantMessageParts(message.parts)
59
+ if (sanitizedParts.length === 0) {
60
+ chatLogger.warn`Dropping persisted assistant thread message without substantive parts during history sanitization (id=${messageId})`
61
+ continue
62
+ }
63
+
64
+ if (sanitizedParts.length === message.parts.length) {
65
+ sanitizedMessages.push(message)
66
+ continue
67
+ }
68
+
69
+ sanitizedMessages.push({ ...message, parts: sanitizedParts } as TMessage)
70
+ }
71
+
72
+ return sanitizedMessages
73
+ }
@@ -1,17 +1,20 @@
1
1
  import { Cause, Context, Schema, Duration, Effect, Latch, Layer } from 'effect'
2
2
 
3
- import { effectTryPromise } from '../effect/helpers'
3
+ import { ERROR_TAGS } from '../effect/errors'
4
4
  import { nowEpochMillis } from '../utils/date-time'
5
5
 
6
6
  const COMPACTION_WAIT_REFRESH_MS = 1_000
7
7
  const COMPACTION_MAX_WAIT_MS = 120_000
8
8
 
9
- class WaitForCompactionError extends Schema.TaggedErrorClass<WaitForCompactionError>()('WaitForCompactionError', {
10
- entityId: Schema.String,
11
- entityLabel: Schema.String,
12
- message: Schema.String,
13
- cause: Schema.optional(Schema.Defect),
14
- }) {}
9
+ class WaitForCompactionError extends Schema.TaggedErrorClass<WaitForCompactionError>()(
10
+ ERROR_TAGS.WaitForCompactionError,
11
+ {
12
+ entityId: Schema.String,
13
+ entityLabel: Schema.String,
14
+ message: Schema.String,
15
+ cause: Schema.optional(Schema.Defect),
16
+ },
17
+ ) {}
15
18
 
16
19
  function toWaitForCompactionError(params: {
17
20
  entityId: string
@@ -28,12 +31,12 @@ function toWaitForCompactionError(params: {
28
31
 
29
32
  interface CompactionCoordination {
30
33
  readonly signal: (entityId: string, compacting: boolean) => Effect.Effect<void>
31
- readonly waitIfNeeded: <TEntity>(params: {
34
+ readonly waitIfNeeded: <TEntity, TError, TRequirements>(params: {
32
35
  entityId: string
33
36
  entityLabel: string
34
- loadEntity: () => PromiseLike<TEntity> | Effect.Effect<TEntity, unknown>
37
+ loadEntity: () => Effect.Effect<TEntity, TError, TRequirements>
35
38
  isCompacting: (entity: TEntity) => boolean
36
- }) => Effect.Effect<TEntity, WaitForCompactionError>
39
+ }) => Effect.Effect<TEntity, WaitForCompactionError, TRequirements>
37
40
  }
38
41
 
39
42
  export class CompactionCoordinationTag extends Context.Service<CompactionCoordinationTag, CompactionCoordination>()(
@@ -65,18 +68,25 @@ export const CompactionCoordinationLive = Layer.effect(
65
68
  }
66
69
  }),
67
70
 
68
- waitIfNeeded: Effect.fn('CompactionCoordination.waitIfNeeded')(function* <TEntity>(params: {
71
+ waitIfNeeded: Effect.fn('CompactionCoordination.waitIfNeeded')(function* <
72
+ TEntity,
73
+ TError,
74
+ TRequirements,
75
+ >(params: {
69
76
  entityId: string
70
77
  entityLabel: string
71
- loadEntity: () => PromiseLike<TEntity> | Effect.Effect<TEntity, unknown>
78
+ loadEntity: () => Effect.Effect<TEntity, TError, TRequirements>
72
79
  isCompacting: (entity: TEntity) => boolean
73
80
  }) {
74
81
  const deadline = nowEpochMillis() + COMPACTION_MAX_WAIT_MS
75
82
  const latch = getLatch(params.entityId)
76
- let entity = yield* effectTryPromise(
77
- () => params.loadEntity(),
78
- (cause) => toWaitForCompactionError({ entityId: params.entityId, entityLabel: params.entityLabel, cause }),
79
- )
83
+ let entity = yield* params
84
+ .loadEntity()
85
+ .pipe(
86
+ Effect.mapError((cause) =>
87
+ toWaitForCompactionError({ entityId: params.entityId, entityLabel: params.entityLabel, cause }),
88
+ ),
89
+ )
80
90
 
81
91
  while (params.isCompacting(entity)) {
82
92
  Latch.closeUnsafe(latch)
@@ -94,10 +104,13 @@ export const CompactionCoordinationLive = Layer.effect(
94
104
  Effect.timeout(Duration.millis(refreshWindowMs)),
95
105
  Effect.catchIf(Cause.isTimeoutError, () => Effect.void),
96
106
  )
97
- entity = yield* effectTryPromise(
98
- () => params.loadEntity(),
99
- (cause) => toWaitForCompactionError({ entityId: params.entityId, entityLabel: params.entityLabel, cause }),
100
- )
107
+ entity = yield* params
108
+ .loadEntity()
109
+ .pipe(
110
+ Effect.mapError((cause) =>
111
+ toWaitForCompactionError({ entityId: params.entityId, entityLabel: params.entityLabel, cause }),
112
+ ),
113
+ )
101
114
  }
102
115
 
103
116
  Latch.openUnsafe(latch)
@@ -1,6 +1,7 @@
1
1
  import { Schema, Effect } from 'effect'
2
2
 
3
- import { createContextCompactionAgent } from '../../system-agents/context-compaction.agent'
3
+ import type { AiGatewayModels } from '../../ai-gateway/ai-gateway'
4
+ import { makeContextCompactionAgentFactory } from '../../system-agents/context-compaction.agent'
4
5
  import type { GenerateHelperStructuredParams, GenerateHelperTextParams } from '../helper-model'
5
6
  import {
6
7
  buildContextCompactionPrompt,
@@ -29,12 +30,13 @@ interface HelperModelRuntime {
29
30
 
30
31
  interface CreateContextCompactionRuntimeDeps {
31
32
  helperModelRuntime: HelperModelRuntime
33
+ aiGatewayModels: AiGatewayModels
32
34
  now?: () => number
33
35
  randomId?: () => string
34
36
  }
35
37
 
36
38
  class ContextCompactionRuntimeError extends Schema.TaggedErrorClass<ContextCompactionRuntimeError>()(
37
- 'ContextCompactionRuntimeError',
39
+ '@lota-sdk/core/ContextCompactionRuntimeError',
38
40
  { message: Schema.String, cause: Schema.Defect },
39
41
  ) {}
40
42
 
@@ -48,11 +50,15 @@ function tryContextCompactionPromise<A>(
48
50
  })
49
51
  }
50
52
 
51
- function runContextCompacterEffect(helperModelRuntime: HelperModelRuntime, params: ContextCompactionRunnerParams) {
53
+ function runContextCompacterEffect(
54
+ helperModelRuntime: HelperModelRuntime,
55
+ aiGatewayModels: AiGatewayModels,
56
+ params: ContextCompactionRunnerParams,
57
+ ) {
52
58
  return tryContextCompactionPromise('Failed to compact runtime context.', () =>
53
59
  helperModelRuntime.generateHelperStructured({
54
60
  tag: 'context-compaction',
55
- createAgent: createContextCompactionAgent,
61
+ createAgent: makeContextCompactionAgentFactory(aiGatewayModels),
56
62
  messages: [
57
63
  {
58
64
  role: 'user',
@@ -69,10 +75,11 @@ function runContextCompacterEffect(helperModelRuntime: HelperModelRuntime, param
69
75
  }
70
76
 
71
77
  export function createWiredContextCompactionRuntime(deps: CreateContextCompactionRuntimeDeps) {
72
- const { helperModelRuntime } = deps
78
+ const { helperModelRuntime, aiGatewayModels } = deps
79
+ const createAgent = makeContextCompactionAgentFactory(aiGatewayModels)
73
80
 
74
81
  const runtime = createContextCompactionRuntime({
75
- runCompacter: (params) => runContextCompacterEffect(helperModelRuntime, params),
82
+ runCompacter: (params) => runContextCompacterEffect(helperModelRuntime, aiGatewayModels, params),
76
83
  now: deps.now,
77
84
  randomId: deps.randomId,
78
85
  thresholdRatio: CONTEXT_COMPACTION_THRESHOLD_RATIO,
@@ -92,7 +99,7 @@ export function createWiredContextCompactionRuntime(deps: CreateContextCompactio
92
99
  return tryContextCompactionPromise('Failed to compact memory block summary.', () =>
93
100
  helperModelRuntime.generateHelperText({
94
101
  tag: 'memory-block-compaction',
95
- createAgent: createContextCompactionAgent,
102
+ createAgent,
96
103
  messages: [{ role: 'user', content: buildMemoryBlockCompactionPrompt({ previousSummary, newEntriesText }) }],
97
104
  maxOutputTokens: CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS,
98
105
  }),