@lota-sdk/core 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) 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 +38 -10
  4. package/src/config/agent-defaults.ts +22 -9
  5. package/src/config/agent-types.ts +1 -1
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/index.ts +0 -1
  8. package/src/config/logger.ts +20 -7
  9. package/src/config/thread-defaults.ts +12 -4
  10. package/src/create-runtime.ts +69 -656
  11. package/src/db/memory-query-builder.ts +2 -1
  12. package/src/db/memory-store.ts +29 -20
  13. package/src/db/memory.ts +188 -195
  14. package/src/db/service-normalization.ts +97 -64
  15. package/src/db/service.ts +706 -538
  16. package/src/db/startup.ts +30 -19
  17. package/src/effect/awaitable-effect.ts +46 -37
  18. package/src/effect/helpers.ts +30 -5
  19. package/src/effect/index.ts +7 -5
  20. package/src/effect/layers.ts +82 -72
  21. package/src/effect/runtime.ts +18 -3
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -66
  24. package/src/index.ts +13 -11
  25. package/src/queues/autonomous-job.queue.ts +59 -71
  26. package/src/queues/context-compaction.queue.ts +6 -18
  27. package/src/queues/delayed-node-promotion.queue.ts +9 -17
  28. package/src/queues/organization-learning.queue.ts +17 -4
  29. package/src/queues/plan-agent-heartbeat.queue.ts +23 -20
  30. package/src/queues/plan-scheduler.queue.ts +6 -18
  31. package/src/queues/post-chat-memory.queue.ts +6 -18
  32. package/src/queues/queue-factory.ts +128 -50
  33. package/src/queues/title-generation.queue.ts +6 -17
  34. package/src/redis/connection.ts +181 -164
  35. package/src/redis/runtime-connection.ts +13 -3
  36. package/src/redis/stream-context.ts +17 -9
  37. package/src/runtime/agent-runtime-policy.ts +1 -1
  38. package/src/runtime/agent-stream-helpers.ts +15 -11
  39. package/src/runtime/chat-run-orchestration.ts +1 -1
  40. package/src/runtime/context-compaction/context-compaction-runtime.ts +1 -1
  41. package/src/runtime/context-compaction/context-compaction.ts +126 -82
  42. package/src/runtime/domain-layer.ts +192 -0
  43. package/src/runtime/graph-designer.ts +15 -7
  44. package/src/runtime/helper-model.ts +8 -4
  45. package/src/runtime/index.ts +0 -1
  46. package/src/runtime/memory/memory-block.ts +19 -9
  47. package/src/runtime/memory/memory-pipeline.ts +53 -66
  48. package/src/runtime/memory/memory-scope.ts +33 -29
  49. package/src/runtime/plugin-resolution.ts +33 -54
  50. package/src/runtime/post-turn-side-effects.ts +6 -26
  51. package/src/runtime/retrieval-adapters.ts +4 -4
  52. package/src/runtime/runtime-accessors.ts +92 -0
  53. package/src/runtime/runtime-config.ts +3 -3
  54. package/src/runtime/runtime-extensions.ts +20 -9
  55. package/src/runtime/runtime-lifecycle.ts +124 -0
  56. package/src/runtime/runtime-services.ts +386 -0
  57. package/src/runtime/runtime-token.ts +47 -0
  58. package/src/runtime/social-chat/social-chat-agent-runner.ts +7 -5
  59. package/src/runtime/social-chat/social-chat-history.ts +21 -12
  60. package/src/runtime/social-chat/social-chat.ts +401 -365
  61. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +58 -52
  62. package/src/runtime/thread-turn-context.ts +21 -27
  63. package/src/services/agent-activity.service.ts +1 -1
  64. package/src/services/agent-executor.service.ts +179 -187
  65. package/src/services/artifact.service.ts +10 -5
  66. package/src/services/attachment.service.ts +35 -1
  67. package/src/services/autonomous-job.service.ts +58 -56
  68. package/src/services/background-work.service.ts +54 -0
  69. package/src/services/chat-run-registry.service.ts +3 -1
  70. package/src/services/context-compaction.service.ts +1 -1
  71. package/src/services/document-chunk.service.ts +8 -17
  72. package/src/services/execution-plan/execution-plan-graph.ts +74 -52
  73. package/src/services/execution-plan/execution-plan.service.ts +1 -1
  74. package/src/services/feedback-loop.service.ts +1 -1
  75. package/src/services/global-orchestrator.service.ts +33 -10
  76. package/src/services/graph-full-routing.ts +44 -33
  77. package/src/services/index.ts +1 -0
  78. package/src/services/institutional-memory.service.ts +8 -17
  79. package/src/services/learned-skill.service.ts +38 -35
  80. package/src/services/memory/memory-errors.ts +27 -0
  81. package/src/services/memory/memory-org-memory.ts +14 -3
  82. package/src/services/memory/memory-preseeded.ts +10 -4
  83. package/src/services/memory/memory-utils.ts +2 -1
  84. package/src/services/memory/memory.service.ts +26 -44
  85. package/src/services/memory/rerank.service.ts +3 -11
  86. package/src/services/monitoring-window.service.ts +1 -1
  87. package/src/services/mutating-approval.service.ts +1 -1
  88. package/src/services/node-workspace.service.ts +2 -2
  89. package/src/services/notification.service.ts +16 -4
  90. package/src/services/organization-member.service.ts +1 -1
  91. package/src/services/organization.service.ts +34 -51
  92. package/src/services/ownership-dispatcher.service.ts +132 -90
  93. package/src/services/plan/plan-agent-heartbeat.service.ts +1 -1
  94. package/src/services/plan/plan-agent-query.service.ts +1 -1
  95. package/src/services/plan/plan-approval.service.ts +52 -48
  96. package/src/services/plan/plan-artifact.service.ts +2 -2
  97. package/src/services/plan/plan-builder.service.ts +2 -2
  98. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  99. package/src/services/plan/plan-compiler.service.ts +1 -1
  100. package/src/services/plan/plan-completion-side-effects.ts +18 -24
  101. package/src/services/plan/plan-coordination.service.ts +1 -1
  102. package/src/services/plan/plan-cycle.service.ts +171 -164
  103. package/src/services/plan/plan-deadline.service.ts +290 -304
  104. package/src/services/plan/plan-event-delivery.service.ts +44 -39
  105. package/src/services/plan/plan-executor-graph.ts +114 -67
  106. package/src/services/plan/plan-executor-helpers.ts +60 -75
  107. package/src/services/plan/plan-executor.service.ts +550 -467
  108. package/src/services/plan/plan-run.service.ts +12 -19
  109. package/src/services/plan/plan-scheduler.service.ts +27 -33
  110. package/src/services/plan/plan-template.service.ts +1 -1
  111. package/src/services/plan/plan-transaction-events.ts +8 -5
  112. package/src/services/plan/plan-validator.service.ts +1 -1
  113. package/src/services/plan/plan-workspace.service.ts +17 -11
  114. package/src/services/plugin-executor.service.ts +26 -21
  115. package/src/services/quality-metrics.service.ts +1 -1
  116. package/src/services/queue-job.service.ts +8 -17
  117. package/src/services/recent-activity-title.service.ts +17 -9
  118. package/src/services/recent-activity.service.ts +1 -1
  119. package/src/services/skill-resolver.service.ts +1 -1
  120. package/src/services/social-chat-history.service.ts +37 -20
  121. package/src/services/system-executor.service.ts +25 -20
  122. package/src/services/thread/thread-bootstrap.ts +26 -10
  123. package/src/services/thread/thread-listing.ts +2 -1
  124. package/src/services/thread/thread-memory-block.ts +18 -5
  125. package/src/services/thread/thread-message.service.ts +24 -8
  126. package/src/services/thread/thread-title.service.ts +1 -1
  127. package/src/services/thread/thread-turn-execution.ts +1 -1
  128. package/src/services/thread/thread-turn-preparation.service.ts +18 -16
  129. package/src/services/thread/thread-turn-streaming.ts +12 -11
  130. package/src/services/thread/thread-turn.ts +43 -10
  131. package/src/services/thread/thread.service.ts +11 -2
  132. package/src/services/user.service.ts +1 -1
  133. package/src/services/write-intent-validator.service.ts +1 -1
  134. package/src/storage/attachment-storage.service.ts +7 -4
  135. package/src/storage/generated-document-storage.service.ts +1 -1
  136. package/src/system-agents/context-compaction.agent.ts +1 -1
  137. package/src/system-agents/helper-agent-options.ts +1 -1
  138. package/src/system-agents/memory-reranker.agent.ts +1 -1
  139. package/src/system-agents/memory.agent.ts +1 -1
  140. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  141. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  142. package/src/system-agents/skill-extractor.agent.ts +1 -1
  143. package/src/system-agents/skill-manager.agent.ts +1 -1
  144. package/src/system-agents/title-generator.agent.ts +1 -1
  145. package/src/tools/execution-plan.tool.ts +28 -17
  146. package/src/tools/fetch-webpage.tool.ts +20 -13
  147. package/src/tools/firecrawl-client.ts +13 -3
  148. package/src/tools/plan-approval.tool.ts +9 -1
  149. package/src/tools/search-web.tool.ts +16 -9
  150. package/src/tools/team-think.tool.ts +2 -2
  151. package/src/utils/async.ts +15 -6
  152. package/src/utils/errors.ts +27 -15
  153. package/src/workers/bootstrap.ts +25 -48
  154. package/src/workers/organization-learning.worker.ts +1 -1
  155. package/src/workers/regular-chat-memory-digest.runner.ts +25 -15
  156. package/src/workers/worker-utils.ts +20 -2
  157. package/src/config/search.ts +0 -3
  158. package/src/runtime/agent-types.ts +0 -1
@@ -33,7 +33,7 @@ import type { RecordIdInput } from '../../db/record-id'
33
33
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
34
34
  import type { SurrealDBService } from '../../db/service'
35
35
  import { TABLES } from '../../db/tables'
36
- import { ServiceError } from '../../effect/errors'
36
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
37
37
  import { DatabaseServiceTag } from '../../effect/services'
38
38
  import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../../utils/date-time'
39
39
 
@@ -159,7 +159,10 @@ function serializeEvent(event: PlanEventRecord): SerializablePlanEvent {
159
159
  }
160
160
  }
161
161
 
162
- type PlanRunServiceError = ServiceError
162
+ class PlanRunServiceError extends Schema.TaggedErrorClass<PlanRunServiceError>()('PlanRunServiceError', {
163
+ message: Schema.String,
164
+ cause: Schema.optional(Schema.Defect),
165
+ }) {}
163
166
 
164
167
  class PlanRunNotFoundError extends Schema.TaggedErrorClass<PlanRunNotFoundError>()('PlanRunNotFoundError', {
165
168
  message: Schema.String,
@@ -172,25 +175,15 @@ class PlanRunSerializationError extends Schema.TaggedErrorClass<PlanRunSerializa
172
175
  { message: Schema.String },
173
176
  ) {}
174
177
 
178
+ const effectTryPlanRunPromise = makeEffectTryPromiseWithMessage(
179
+ (message, cause) => new PlanRunServiceError({ message, cause }),
180
+ )
181
+
175
182
  function tryPlanRunPromise<A>(
176
183
  message: string,
177
- thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
184
+ evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
178
185
  ): Effect.Effect<A, PlanRunServiceError> {
179
- return Effect.suspend(() => {
180
- try {
181
- const value = thunk()
182
- if (Effect.isEffect(value)) {
183
- return value.pipe(Effect.mapError((cause) => new ServiceError({ message, cause })))
184
- }
185
-
186
- return Effect.tryPromise({
187
- try: () => Promise.resolve(value),
188
- catch: (cause) => new ServiceError({ message, cause }),
189
- })
190
- } catch (cause) {
191
- return Effect.fail(new ServiceError({ message, cause }))
192
- }
193
- })
186
+ return effectTryPlanRunPromise(evaluate, message)
194
187
  }
195
188
 
196
189
  export function makePlanRunService(db: SurrealDBService) {
@@ -632,7 +625,7 @@ export function makePlanRunService(db: SurrealDBService) {
632
625
  }
633
626
 
634
627
  export class PlanRunServiceTag extends Context.Service<PlanRunServiceTag, ReturnType<typeof makePlanRunService>>()(
635
- 'PlanRunService',
628
+ '@lota-sdk/core/PlanRunService',
636
629
  ) {}
637
630
 
638
631
  export const PlanRunServiceLive = Layer.effect(
@@ -8,7 +8,7 @@ import { ensureRecordId, recordIdToString } from '../../db/record-id'
8
8
  import type { SurrealDBService } from '../../db/service'
9
9
  import { TABLES } from '../../db/tables'
10
10
  import { NotFoundError } from '../../effect/errors'
11
- import { effectTryPromise as effectTryPromiseShared } from '../../effect/helpers'
11
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
12
12
  import { DatabaseServiceTag } from '../../effect/services'
13
13
  import { nowDate, nowEpochMillis, toDatabaseDateTime, unsafeDateFrom } from '../../utils/date-time'
14
14
 
@@ -23,12 +23,7 @@ class PlanSchedulerError extends Schema.TaggedErrorClass<PlanSchedulerError>()('
23
23
  cause: Schema.optional(Schema.Defect),
24
24
  }) {}
25
25
 
26
- function effectTryPromise<A>(
27
- evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
28
- message: string,
29
- ): Effect.Effect<A, PlanSchedulerError> {
30
- return effectTryPromiseShared(evaluate, (cause) => new PlanSchedulerError({ message, cause }))
31
- }
26
+ const effectTryPromise = makeEffectTryPromiseWithMessage((message, cause) => new PlanSchedulerError({ message, cause }))
32
27
 
33
28
  function failPlanScheduler(message: string, cause?: unknown): Effect.Effect<never, PlanSchedulerError> {
34
29
  return Effect.fail(new PlanSchedulerError({ message, ...(cause === undefined ? {} : { cause }) }))
@@ -143,13 +138,9 @@ export function makePlanSchedulerService(db: SurrealDBService) {
143
138
  const isRecurring = schedule.scheduleSpec.type === 'cron' || schedule.scheduleSpec.type === 'monitoring'
144
139
  const maxReached = schedule.scheduleSpec.maxFires !== undefined && newFireCount >= schedule.scheduleSpec.maxFires
145
140
 
146
- let nextFireAt: Date | null = null
147
- let newStatus: 'active' | 'completed' = 'completed'
148
-
149
- if (isRecurring && !maxReached) {
150
- nextFireAt = yield* computeNextFireAtEffect(schedule.scheduleSpec, now)
151
- newStatus = 'active'
152
- }
141
+ const isActive = isRecurring && !maxReached
142
+ const nextFireAt: Date | null = isActive ? yield* computeNextFireAtEffect(schedule.scheduleSpec, now) : null
143
+ const newStatus: 'active' | 'completed' = isActive ? 'active' : 'completed'
153
144
 
154
145
  yield* effectTryPromise(
155
146
  () =>
@@ -167,7 +158,6 @@ export function makePlanSchedulerService(db: SurrealDBService) {
167
158
  'Failed to update fired schedule.',
168
159
  )
169
160
 
170
- let shouldRecoverDeadlineChecks = false
171
161
  if (newStatus === 'active') {
172
162
  if (!nextFireAt) {
173
163
  return yield* new PlanSchedulerError({ message: 'Recurring schedules must resolve a next fire time.' })
@@ -185,6 +175,7 @@ export function makePlanSchedulerService(db: SurrealDBService) {
185
175
 
186
176
  const runId = schedule.runId
187
177
  const nodeId = schedule.nodeId
178
+ const promotedNode = Boolean(runId && nodeId)
188
179
  if (runId && nodeId) {
189
180
  yield* effectTryPromise(
190
181
  () =>
@@ -195,26 +186,29 @@ export function makePlanSchedulerService(db: SurrealDBService) {
195
186
  }),
196
187
  'Failed to promote delayed plan node.',
197
188
  )
198
- shouldRecoverDeadlineChecks = true
199
189
  }
200
190
 
201
- if (schedule.planSpecId && !schedule.runId) {
202
- const cycle = yield* effectTryPromise(
203
- () =>
204
- db.findOne(
205
- TABLES.PLAN_CYCLE,
206
- { scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
207
- PlanCycleRecordSchema,
208
- ),
209
- 'Failed to load plan cycle for schedule.',
210
- )
211
- if (cycle) {
212
- yield* effectTryPromise(() => runtimeDeps.advanceCycle(cycle.id), 'Failed to advance plan cycle.')
213
- shouldRecoverDeadlineChecks = true
214
- }
215
- }
191
+ const advancedCycle =
192
+ schedule.planSpecId && !schedule.runId
193
+ ? yield* effectTryPromise(
194
+ () =>
195
+ db.findOne(
196
+ TABLES.PLAN_CYCLE,
197
+ { scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
198
+ PlanCycleRecordSchema,
199
+ ),
200
+ 'Failed to load plan cycle for schedule.',
201
+ ).pipe(
202
+ Effect.tap((cycle) =>
203
+ cycle
204
+ ? effectTryPromise(() => runtimeDeps.advanceCycle(cycle.id), 'Failed to advance plan cycle.')
205
+ : Effect.void,
206
+ ),
207
+ Effect.map((cycle) => cycle !== null),
208
+ )
209
+ : false
216
210
 
217
- if (shouldRecoverDeadlineChecks) {
211
+ if (promotedNode || advancedCycle) {
218
212
  yield* effectTryPromise(() => runtimeDeps.recoverDeadlineChecks(), 'Failed to recover deadline checks.')
219
213
  }
220
214
  })
@@ -374,7 +368,7 @@ export function makePlanSchedulerService(db: SurrealDBService) {
374
368
  export class PlanSchedulerServiceTag extends Context.Service<
375
369
  PlanSchedulerServiceTag,
376
370
  ReturnType<typeof makePlanSchedulerService>
377
- >()('PlanSchedulerService') {}
371
+ >()('@lota-sdk/core/PlanSchedulerService') {}
378
372
 
379
373
  export const PlanSchedulerServiceLive = Layer.effect(
380
374
  PlanSchedulerServiceTag,
@@ -212,7 +212,7 @@ export function makePlanTemplateService(deps: PlanTemplateDeps) {
212
212
  export class PlanTemplateServiceTag extends Context.Service<
213
213
  PlanTemplateServiceTag,
214
214
  ReturnType<typeof makePlanTemplateService>
215
- >()('PlanTemplateService') {}
215
+ >()('@lota-sdk/core/PlanTemplateService') {}
216
216
 
217
217
  export const PlanTemplateServiceLive = Layer.effect(
218
218
  PlanTemplateServiceTag,
@@ -23,11 +23,14 @@ export function withTransactionAndEventsEffect<T, E, R>(params: {
23
23
  (error) => new PlanTransactionEventsError({ message: 'Failed to run plan transaction.', cause: error }),
24
24
  ),
25
25
  )
26
- yield* Effect.tryPromise({
27
- try: () => params.planEventDeliveryService.dispatchEvents(emittedEvents),
28
- catch: (error) =>
29
- new PlanTransactionEventsError({ message: 'Failed to dispatch plan transaction events.', cause: error }),
30
- })
26
+ yield* params.planEventDeliveryService
27
+ .dispatchEventsEffect(emittedEvents)
28
+ .pipe(
29
+ Effect.mapError(
30
+ (error) =>
31
+ new PlanTransactionEventsError({ message: 'Failed to dispatch plan transaction events.', cause: error }),
32
+ ),
33
+ )
31
34
  return result
32
35
  })
33
36
  }
@@ -897,7 +897,7 @@ export function makePlanValidatorService(planCoordinationService: ReturnType<typ
897
897
  export class PlanValidatorServiceTag extends Context.Service<
898
898
  PlanValidatorServiceTag,
899
899
  ReturnType<typeof makePlanValidatorService>
900
- >()('PlanValidatorService') {}
900
+ >()('@lota-sdk/core/PlanValidatorService') {}
901
901
 
902
902
  export const PlanValidatorServiceLive = Layer.effect(
903
903
  PlanValidatorServiceTag,
@@ -1,10 +1,14 @@
1
1
  import { Context, Effect, Layer, Schema } from 'effect'
2
2
 
3
+ import { ServiceError } from '../../effect/errors'
4
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
3
5
  import { RedisServiceTag } from '../../effect/services'
4
6
  import { toValidationError } from '../../effect/zod'
5
7
  import type { RedisConnectionManager } from '../../redis/connection'
6
8
  import { nowEpochMillis } from '../../utils/date-time'
7
9
 
10
+ const tryWorkspacePromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
11
+
8
12
  const PlanWorkspaceEntrySchema = Schema.Struct({
9
13
  value: Schema.Unknown,
10
14
  version: Schema.Number,
@@ -29,10 +33,9 @@ function parsePlanWorkspaceEntryEffect(
29
33
  fieldKey: string,
30
34
  raw: string,
31
35
  ): Effect.Effect<PlanWorkspaceEntry, ReturnType<typeof toValidationError>> {
32
- return Effect.try({
33
- try: () => Schema.decodeUnknownSync(PlanWorkspaceEntryJsonSchema)(raw),
34
- catch: (error) => toValidationError(error, `Invalid plan workspace entry JSON for "${fieldKey}"`),
35
- })
36
+ return Schema.decodeUnknownEffect(PlanWorkspaceEntryJsonSchema)(raw).pipe(
37
+ Effect.mapError((error) => toValidationError(error, `Invalid plan workspace entry JSON for "${fieldKey}"`)),
38
+ )
36
39
  }
37
40
 
38
41
  export function makePlanWorkspaceService(redis: RedisConnectionManager) {
@@ -56,7 +59,10 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
56
59
  timestamp: nowEpochMillis(),
57
60
  }
58
61
  const serialized = yield* Schema.encodeEffect(PlanWorkspaceEntryJsonSchema)(entry)
59
- yield* Effect.tryPromise(() => conn.hset(hashKey, fieldKey, serialized))
62
+ yield* tryWorkspacePromise(
63
+ () => conn.hset(hashKey, fieldKey, serialized),
64
+ 'Failed to write plan workspace entry.',
65
+ )
60
66
  })
61
67
 
62
68
  const snapshotReadEffect = (params: { runId: string; readerNodeId: string; snapshotSequence?: number }) =>
@@ -64,7 +70,7 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
64
70
  const conn = redis.getConnection()
65
71
  const hashKey = `${PLAN_WORKSPACE_KEY_PREFIX}${params.runId}`
66
72
  const nodePrefix = `${params.readerNodeId}:`
67
- const all = yield* Effect.tryPromise(() => conn.hgetall(hashKey))
73
+ const all = yield* tryWorkspacePromise(() => conn.hgetall(hashKey), 'Failed to read plan workspace snapshot.')
68
74
  const result: Record<string, PlanWorkspaceEntry> = {}
69
75
  for (const [fieldKey, raw] of Object.entries(all)) {
70
76
  if (!fieldKey.startsWith(nodePrefix)) continue
@@ -83,11 +89,11 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
83
89
  const hashKey = `${PLAN_WORKSPACE_KEY_PREFIX}${params.runId}`
84
90
  const key = params.key
85
91
  if (key !== undefined) {
86
- const raw = yield* Effect.tryPromise(() => conn.hget(hashKey, key))
92
+ const raw = yield* tryWorkspacePromise(() => conn.hget(hashKey, key), 'Failed to read plan workspace entry.')
87
93
  if (!raw) return {}
88
94
  return { [key]: yield* parsePlanWorkspaceEntryEffect(key, raw) }
89
95
  }
90
- const all = yield* Effect.tryPromise(() => conn.hgetall(hashKey))
96
+ const all = yield* tryWorkspacePromise(() => conn.hgetall(hashKey), 'Failed to read plan workspace entries.')
91
97
  const result: Record<string, PlanWorkspaceEntry> = {}
92
98
  for (const [k, v] of Object.entries(all)) {
93
99
  result[k] = yield* parsePlanWorkspaceEntryEffect(k, v)
@@ -97,10 +103,10 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
97
103
 
98
104
  const cleanupEffect = (runId: string) =>
99
105
  Effect.asVoid(
100
- Effect.tryPromise(() => {
106
+ tryWorkspacePromise(() => {
101
107
  const conn = redis.getConnection()
102
108
  return conn.del(`${PLAN_WORKSPACE_KEY_PREFIX}${runId}`)
103
- }),
109
+ }, 'Failed to clean up plan workspace.'),
104
110
  )
105
111
 
106
112
  return {
@@ -114,7 +120,7 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
114
120
  export class PlanWorkspaceServiceTag extends Context.Service<
115
121
  PlanWorkspaceServiceTag,
116
122
  ReturnType<typeof makePlanWorkspaceService>
117
- >()('PlanWorkspaceService') {}
123
+ >()('@lota-sdk/core/PlanWorkspaceService') {}
118
124
 
119
125
  export const PlanWorkspaceServiceLive = Layer.effect(
120
126
  PlanWorkspaceServiceTag,
@@ -1,8 +1,13 @@
1
1
  import type { OwnershipDispatchContext, PlanNodeSpec, PluginPlanNodeOwner } 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 tryPluginExecutorPromise = makeEffectTryPromiseWithMessage(
9
+ (message, cause) => new ServiceError({ message, cause }),
10
+ )
6
11
  import type { LotaPlugin, PluginNodeExecutionParams } from '../runtime/plugin-types'
7
12
  import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
8
13
  import type { PlanValidationIssueInput } from './plan/plan-validator.service'
@@ -82,28 +87,28 @@ export function makePluginExecutorService(config: ResolvedLotaRuntimeConfig) {
82
87
  return []
83
88
  },
84
89
 
85
- executeNode(params: {
90
+ executeNode: Effect.fn('PluginExecutor.executeNode')(function* (params: {
86
91
  nodeSpec: PlanNodeSpec
87
92
  resolvedInput: Record<string, unknown>
88
93
  context: OwnershipDispatchContext
89
94
  }) {
90
- return Effect.gen(function* () {
91
- const owner = params.nodeSpec.owner
92
- if (!isPluginOwner(owner)) {
93
- return yield* new BadRequestError({
94
- message: `PluginExecutor cannot execute owner type "${owner.executorType}".`,
95
- })
96
- }
95
+ const owner = params.nodeSpec.owner
96
+ if (!isPluginOwner(owner)) {
97
+ return yield* new BadRequestError({
98
+ message: `PluginExecutor cannot execute owner type "${owner.executorType}".`,
99
+ })
100
+ }
97
101
 
98
- const plugin = getPluginRuntime()[owner.ref]
99
- const nodeExecutor = plugin?.nodeExecutor
100
- if (!plugin || !nodeExecutor || !nodeExecutor.supportedOperations.includes(owner.operation)) {
101
- return yield* new BadRequestError({
102
- message: `Plugin executor ${owner.ref}.${owner.operation} is not registered.`,
103
- })
104
- }
102
+ const plugin = getPluginRuntime()[owner.ref]
103
+ const nodeExecutor = plugin?.nodeExecutor
104
+ if (!plugin || !nodeExecutor || !nodeExecutor.supportedOperations.includes(owner.operation)) {
105
+ return yield* new BadRequestError({
106
+ message: `Plugin executor ${owner.ref}.${owner.operation} is not registered.`,
107
+ })
108
+ }
105
109
 
106
- return yield* Effect.tryPromise(() =>
110
+ return yield* tryPluginExecutorPromise(
111
+ () =>
107
112
  nodeExecutor.executeNode(
108
113
  buildPluginExecutionParams({
109
114
  owner,
@@ -112,16 +117,16 @@ export function makePluginExecutorService(config: ResolvedLotaRuntimeConfig) {
112
117
  context: params.context,
113
118
  }),
114
119
  ),
115
- )
116
- })
117
- },
120
+ `Plugin executor "${owner.ref}.${owner.operation}" failed.`,
121
+ ).pipe(Effect.withSpan('PluginExecutor.invoke'))
122
+ }),
118
123
  }
119
124
  }
120
125
 
121
126
  export class PluginExecutorServiceTag extends Context.Service<
122
127
  PluginExecutorServiceTag,
123
128
  ReturnType<typeof makePluginExecutorService>
124
- >()('PluginExecutorService') {}
129
+ >()('@lota-sdk/core/PluginExecutorService') {}
125
130
 
126
131
  export const PluginExecutorServiceLive = Layer.effect(
127
132
  PluginExecutorServiceTag,
@@ -139,7 +139,7 @@ export function makeQualityMetricsService(db: SurrealDBService) {
139
139
  export class QualityMetricsServiceTag extends Context.Service<
140
140
  QualityMetricsServiceTag,
141
141
  ReturnType<typeof makeQualityMetricsService>
142
- >()('QualityMetricsService') {}
142
+ >()('@lota-sdk/core/QualityMetricsService') {}
143
143
 
144
144
  export const QualityMetricsServiceLive = Layer.effect(
145
145
  QualityMetricsServiceTag,
@@ -14,6 +14,7 @@ import type { SurrealDBService } from '../db/service'
14
14
  import { TABLES } from '../db/tables'
15
15
  import { isRetriableTransactionConflict } from '../db/transaction-conflict'
16
16
  import { BadRequestError, DatabaseError } from '../effect/errors'
17
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
17
18
  import { DatabaseServiceTag } from '../effect/services'
18
19
  import { createDeterministicRecordId } from '../utils/crypto'
19
20
  import { nowDate, unsafeDateFrom } from '../utils/date-time'
@@ -184,25 +185,15 @@ function getQueuedStatus(job: TrackedBullJobLike): QueueJobStatus {
184
185
  return typeof delay === 'number' && delay > 0 ? 'delayed' : 'waiting'
185
186
  }
186
187
 
188
+ const effectTryQueueJobPersistence = makeEffectTryPromiseWithMessage(
189
+ (message, cause) => new DatabaseError({ message, cause }),
190
+ )
191
+
187
192
  function tryQueueJobPersistence<A>(
188
193
  message: string,
189
- thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
194
+ evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
190
195
  ): Effect.Effect<A, DatabaseError> {
191
- return Effect.suspend(() => {
192
- try {
193
- const value = thunk()
194
- if (Effect.isEffect(value)) {
195
- return value.pipe(Effect.mapError((cause) => new DatabaseError({ message, cause })))
196
- }
197
-
198
- return Effect.tryPromise({
199
- try: () => Promise.resolve(value),
200
- catch: (cause: unknown) => new DatabaseError({ message, cause }),
201
- })
202
- } catch (cause) {
203
- return Effect.fail(new DatabaseError({ message, cause }))
204
- }
205
- })
196
+ return effectTryQueueJobPersistence(evaluate, message)
206
197
  }
207
198
 
208
199
  function withPersistenceRetry<T>(work: Effect.Effect<T, DatabaseError>): Effect.Effect<T, DatabaseError> {
@@ -456,7 +447,7 @@ export function makeQueueJobService(db: SurrealDBService) {
456
447
  }
457
448
 
458
449
  export class QueueJobServiceTag extends Context.Service<QueueJobServiceTag, ReturnType<typeof makeQueueJobService>>()(
459
- 'QueueJobService',
450
+ '@lota-sdk/core/QueueJobService',
460
451
  ) {}
461
452
 
462
453
  export const QueueJobServiceLive = Layer.effect(
@@ -1,5 +1,7 @@
1
1
  import { Context, Effect, Layer } from 'effect'
2
2
 
3
+ import { ServiceError } from '../effect/errors'
4
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
3
5
  import type { HelperModelRuntime } from '../runtime/helper-model'
4
6
  import { HelperModelTag } from '../runtime/helper-model'
5
7
  import { normalizeTitle } from '../runtime/title-helpers'
@@ -12,6 +14,10 @@ import { RecentActivityServiceTag } from './recent-activity.service'
12
14
 
13
15
  const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
14
16
 
17
+ const tryRecentActivityTitlePromise = makeEffectTryPromiseWithMessage(
18
+ (message, cause) => new ServiceError({ message, cause }),
19
+ )
20
+
15
21
  function buildRefinementPromptInput(
16
22
  candidate: {
17
23
  id: string
@@ -55,14 +61,16 @@ export function makeRecentActivityTitleService(
55
61
  }
56
62
 
57
63
  const refinedTitle = normalizeTitle(
58
- yield* Effect.tryPromise(() =>
59
- helperModelRuntime.generateHelperText({
60
- tag: 'recent-activity-title-refinement',
61
- createAgent: createRecentActivityTitleRefinerAgent,
62
- defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
63
- timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
64
- messages: [{ role: 'user', content: promptInput }],
65
- }),
64
+ yield* tryRecentActivityTitlePromise(
65
+ () =>
66
+ helperModelRuntime.generateHelperText({
67
+ tag: 'recent-activity-title-refinement',
68
+ createAgent: createRecentActivityTitleRefinerAgent,
69
+ defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
70
+ timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
71
+ messages: [{ role: 'user', content: promptInput }],
72
+ }),
73
+ 'Failed to generate recent activity title refinement.',
66
74
  ),
67
75
  )
68
76
  if (
@@ -84,7 +92,7 @@ export function makeRecentActivityTitleService(
84
92
  export class RecentActivityTitleServiceTag extends Context.Service<
85
93
  RecentActivityTitleServiceTag,
86
94
  ReturnType<typeof makeRecentActivityTitleService>
87
- >()('RecentActivityTitleService') {}
95
+ >()('@lota-sdk/core/RecentActivityTitleService') {}
88
96
 
89
97
  export const RecentActivityTitleServiceLive = Layer.effect(
90
98
  RecentActivityTitleServiceTag,
@@ -387,7 +387,7 @@ export function makeRecentActivityService(db: SurrealDBService) {
387
387
  export class RecentActivityServiceTag extends Context.Service<
388
388
  RecentActivityServiceTag,
389
389
  ReturnType<typeof makeRecentActivityService>
390
- >()('RecentActivityService') {}
390
+ >()('@lota-sdk/core/RecentActivityService') {}
391
391
 
392
392
  export const RecentActivityServiceLive = Layer.effect(
393
393
  RecentActivityServiceTag,
@@ -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,