@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 @@ 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,8 +8,10 @@ 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
+ import type { PlanSchedulerQueueRuntime } from '../../queues/plan-scheduler.queue'
14
+ import { LotaQueuesServiceTag } from '../../queues/queues.service'
13
15
  import { nowDate, nowEpochMillis, toDatabaseDateTime, unsafeDateFrom } from '../../utils/date-time'
14
16
 
15
17
  interface PlanSchedulerRuntimeDeps {
@@ -23,72 +25,77 @@ class PlanSchedulerError extends Schema.TaggedErrorClass<PlanSchedulerError>()('
23
25
  cause: Schema.optional(Schema.Defect),
24
26
  }) {}
25
27
 
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
- }
28
+ const effectTryPromise = makeEffectTryPromiseWithMessage((message, cause) => new PlanSchedulerError({ message, cause }))
32
29
 
33
- function failPlanScheduler(message: string, cause?: unknown): Effect.Effect<never, PlanSchedulerError> {
34
- return Effect.fail(new PlanSchedulerError({ message, ...(cause === undefined ? {} : { cause }) }))
30
+ function requireScheduleField<A>(value: A | undefined, message: string): A {
31
+ if (value === undefined) {
32
+ throw new PlanSchedulerError({ message })
33
+ }
34
+ return value
35
35
  }
36
36
 
37
- function requireScheduleField<A>(value: A | undefined, message: string): Effect.Effect<A, PlanSchedulerError> {
38
- return value === undefined ? failPlanScheduler(message) : Effect.succeed(value)
39
- }
37
+ function computeNextCronDate(cronExpression: string, baseTime: Date): Date {
38
+ const parsedCron = Cron.parse(cronExpression)
39
+ if (!Result.isSuccess(parsedCron)) {
40
+ throw new PlanSchedulerError({ message: `Invalid cron expression: "${cronExpression}".` })
41
+ }
40
42
 
41
- const computeNextCronDateEffect: (cronExpression: string, baseTime: Date) => Effect.Effect<Date, PlanSchedulerError> =
42
- Effect.fn('PlanScheduler.computeNextCronDate')(function* (cronExpression: string, baseTime: Date) {
43
- const parsedCron = Cron.parse(cronExpression)
44
- const cron = Result.isSuccess(parsedCron)
45
- ? parsedCron.success
46
- : yield* failPlanScheduler(`Invalid cron expression: "${cronExpression}".`)
47
-
48
- return yield* Effect.try({
49
- try: () => Cron.next(cron, baseTime),
50
- catch: (cause) =>
51
- new PlanSchedulerError({
52
- message: `Failed to compute the next fire time for cron expression "${cronExpression}".`,
53
- cause,
54
- }),
43
+ try {
44
+ return Cron.next(parsedCron.success, baseTime)
45
+ } catch (cause) {
46
+ throw new PlanSchedulerError({
47
+ message: `Failed to compute the next fire time for cron expression "${cronExpression}".`,
48
+ cause,
55
49
  })
56
- })
57
-
58
- const computeNextFireAtEffect: (spec: PlanScheduleSpec, baseTime?: Date) => Effect.Effect<Date, PlanSchedulerError> =
59
- Effect.fn('PlanScheduler.computeNextFireAt')(function* (spec: PlanScheduleSpec, baseTime: Date = nowDate()) {
60
- switch (spec.type) {
61
- case 'immediate':
62
- return baseTime
50
+ }
51
+ }
63
52
 
64
- case 'absolute': {
65
- const at = yield* requireScheduleField(spec.at, 'Absolute schedules require an "at" timestamp.')
66
- return unsafeDateFrom(at)
67
- }
53
+ // `computeNextFireAt` must stay purely synchronous — no Clock, no service lookups, no async.
54
+ // `computeNextFireAtEffect` wraps it with `Effect.try` so the same logic is composable from
55
+ // Effect callers without an `Effect.runSync` round trip.
56
+ function computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = nowDate()): Date {
57
+ switch (spec.type) {
58
+ case 'immediate':
59
+ return baseTime
68
60
 
69
- case 'relative': {
70
- const delayMs = yield* requireScheduleField(spec.delayMs, 'Relative schedules require "delayMs".')
71
- return unsafeDateFrom(baseTime.getTime() + delayMs)
72
- }
61
+ case 'absolute':
62
+ return unsafeDateFrom(requireScheduleField(spec.at, 'Absolute schedules require an "at" timestamp.'))
73
63
 
74
- case 'cron': {
75
- const cronExpression = yield* requireScheduleField(spec.cron, 'Cron schedules require a "cron" expression.')
76
- return yield* computeNextCronDateEffect(cronExpression, baseTime)
77
- }
64
+ case 'relative': {
65
+ const delayMs = requireScheduleField(spec.delayMs, 'Relative schedules require "delayMs".')
66
+ return unsafeDateFrom(baseTime.getTime() + delayMs)
67
+ }
78
68
 
79
- case 'monitoring': {
80
- const intervalMs = yield* requireScheduleField(spec.intervalMs, 'Monitoring schedules require "intervalMs".')
81
- return unsafeDateFrom(baseTime.getTime() + intervalMs)
82
- }
69
+ case 'cron': {
70
+ const cronExpression = requireScheduleField(spec.cron, 'Cron schedules require a "cron" expression.')
71
+ return computeNextCronDate(cronExpression, baseTime)
72
+ }
83
73
 
84
- default:
85
- return yield* failPlanScheduler('Unsupported schedule type in schedule specification.')
74
+ case 'monitoring': {
75
+ const intervalMs = requireScheduleField(spec.intervalMs, 'Monitoring schedules require "intervalMs".')
76
+ return unsafeDateFrom(baseTime.getTime() + intervalMs)
86
77
  }
78
+
79
+ default:
80
+ throw new PlanSchedulerError({ message: 'Unsupported schedule type in schedule specification.' })
81
+ }
82
+ }
83
+
84
+ const isPlanSchedulerError = Schema.is(PlanSchedulerError)
85
+
86
+ function computeNextFireAtEffect(spec: PlanScheduleSpec, baseTime?: Date): Effect.Effect<Date, PlanSchedulerError> {
87
+ return Effect.try({
88
+ try: () => computeNextFireAt(spec, baseTime),
89
+ catch: (cause) =>
90
+ isPlanSchedulerError(cause)
91
+ ? cause
92
+ : new PlanSchedulerError({ message: 'Failed to compute the next fire time.', cause }),
87
93
  })
94
+ }
88
95
 
89
- export function makePlanSchedulerService(db: SurrealDBService) {
90
- const loadPlanSchedulerQueue = () =>
91
- effectTryPromise(() => import('../../queues/plan-scheduler.queue'), 'Failed to load plan scheduler queue module.')
96
+ export function makePlanSchedulerService(db: SurrealDBService, schedulerQueue: PlanSchedulerQueueRuntime) {
97
+ const loadPlanSchedulerQueue = (): Effect.Effect<PlanSchedulerQueueRuntime, PlanSchedulerError> =>
98
+ Effect.succeed(schedulerQueue)
92
99
 
93
100
  const createScheduleEffect = (params: {
94
101
  organizationId: RecordIdInput
@@ -143,13 +150,9 @@ export function makePlanSchedulerService(db: SurrealDBService) {
143
150
  const isRecurring = schedule.scheduleSpec.type === 'cron' || schedule.scheduleSpec.type === 'monitoring'
144
151
  const maxReached = schedule.scheduleSpec.maxFires !== undefined && newFireCount >= schedule.scheduleSpec.maxFires
145
152
 
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
- }
153
+ const isActive = isRecurring && !maxReached
154
+ const nextFireAt: Date | null = isActive ? yield* computeNextFireAtEffect(schedule.scheduleSpec, now) : null
155
+ const newStatus: 'active' | 'completed' = isActive ? 'active' : 'completed'
153
156
 
154
157
  yield* effectTryPromise(
155
158
  () =>
@@ -167,7 +170,6 @@ export function makePlanSchedulerService(db: SurrealDBService) {
167
170
  'Failed to update fired schedule.',
168
171
  )
169
172
 
170
- let shouldRecoverDeadlineChecks = false
171
173
  if (newStatus === 'active') {
172
174
  if (!nextFireAt) {
173
175
  return yield* new PlanSchedulerError({ message: 'Recurring schedules must resolve a next fire time.' })
@@ -185,6 +187,7 @@ export function makePlanSchedulerService(db: SurrealDBService) {
185
187
 
186
188
  const runId = schedule.runId
187
189
  const nodeId = schedule.nodeId
190
+ const promotedNode = Boolean(runId && nodeId)
188
191
  if (runId && nodeId) {
189
192
  yield* effectTryPromise(
190
193
  () =>
@@ -195,26 +198,29 @@ export function makePlanSchedulerService(db: SurrealDBService) {
195
198
  }),
196
199
  'Failed to promote delayed plan node.',
197
200
  )
198
- shouldRecoverDeadlineChecks = true
199
201
  }
200
202
 
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
- }
203
+ const advancedCycle =
204
+ schedule.planSpecId && !schedule.runId
205
+ ? yield* effectTryPromise(
206
+ () =>
207
+ db.findOne(
208
+ TABLES.PLAN_CYCLE,
209
+ { scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
210
+ PlanCycleRecordSchema,
211
+ ),
212
+ 'Failed to load plan cycle for schedule.',
213
+ ).pipe(
214
+ Effect.tap((cycle) =>
215
+ cycle
216
+ ? effectTryPromise(() => runtimeDeps.advanceCycle(cycle.id), 'Failed to advance plan cycle.')
217
+ : Effect.void,
218
+ ),
219
+ Effect.map((cycle) => cycle !== null),
220
+ )
221
+ : false
216
222
 
217
- if (shouldRecoverDeadlineChecks) {
223
+ if (promotedNode || advancedCycle) {
218
224
  yield* effectTryPromise(() => runtimeDeps.recoverDeadlineChecks(), 'Failed to recover deadline checks.')
219
225
  }
220
226
  })
@@ -357,7 +363,7 @@ export function makePlanSchedulerService(db: SurrealDBService) {
357
363
  return {
358
364
  createSchedule: createScheduleEffect,
359
365
  computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = nowDate()): Date {
360
- return Effect.runSync(computeNextFireAtEffect(spec, baseTime))
366
+ return computeNextFireAt(spec, baseTime)
361
367
  },
362
368
  /** Called by the BullMQ worker when a fire-schedule job executes. */
363
369
  fireScheduleById: fireScheduleByIdEffect,
@@ -374,12 +380,13 @@ export function makePlanSchedulerService(db: SurrealDBService) {
374
380
  export class PlanSchedulerServiceTag extends Context.Service<
375
381
  PlanSchedulerServiceTag,
376
382
  ReturnType<typeof makePlanSchedulerService>
377
- >()('PlanSchedulerService') {}
383
+ >()('@lota-sdk/core/PlanSchedulerService') {}
378
384
 
379
385
  export const PlanSchedulerServiceLive = Layer.effect(
380
386
  PlanSchedulerServiceTag,
381
387
  Effect.gen(function* () {
382
388
  const db = yield* DatabaseServiceTag
383
- return makePlanSchedulerService(db)
389
+ const queues = yield* LotaQueuesServiceTag
390
+ return makePlanSchedulerService(db, queues.planScheduler)
384
391
  }),
385
392
  )
@@ -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
+ .dispatchEvents(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,9 @@
1
1
  import { Context, Effect, Layer } from 'effect'
2
2
 
3
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
4
+ import { ServiceError } from '../effect/errors'
5
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
6
+ import { AgentConfigServiceTag } from '../effect/services'
3
7
  import type { HelperModelRuntime } from '../runtime/helper-model'
4
8
  import { HelperModelTag } from '../runtime/helper-model'
5
9
  import { normalizeTitle } from '../runtime/title-helpers'
@@ -12,6 +16,10 @@ import { RecentActivityServiceTag } from './recent-activity.service'
12
16
 
13
17
  const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
14
18
 
19
+ const tryRecentActivityTitlePromise = makeEffectTryPromiseWithMessage(
20
+ (message, cause) => new ServiceError({ message, cause }),
21
+ )
22
+
15
23
  function buildRefinementPromptInput(
16
24
  candidate: {
17
25
  id: string
@@ -39,6 +47,7 @@ function buildRefinementPromptInput(
39
47
  }
40
48
 
41
49
  export function makeRecentActivityTitleService(
50
+ agentConfig: ResolvedAgentConfig,
42
51
  recentActivityService: ReturnType<typeof makeRecentActivityService>,
43
52
  helperModelRuntime: HelperModelRuntime,
44
53
  ) {
@@ -55,14 +64,16 @@ export function makeRecentActivityTitleService(
55
64
  }
56
65
 
57
66
  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
- }),
67
+ yield* tryRecentActivityTitlePromise(
68
+ () =>
69
+ helperModelRuntime.generateHelperText({
70
+ tag: 'recent-activity-title-refinement',
71
+ createAgent: (options) => createRecentActivityTitleRefinerAgent(agentConfig, options),
72
+ defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
73
+ timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
74
+ messages: [{ role: 'user', content: promptInput }],
75
+ }),
76
+ 'Failed to generate recent activity title refinement.',
66
77
  ),
67
78
  )
68
79
  if (
@@ -84,13 +95,14 @@ export function makeRecentActivityTitleService(
84
95
  export class RecentActivityTitleServiceTag extends Context.Service<
85
96
  RecentActivityTitleServiceTag,
86
97
  ReturnType<typeof makeRecentActivityTitleService>
87
- >()('RecentActivityTitleService') {}
98
+ >()('@lota-sdk/core/RecentActivityTitleService') {}
88
99
 
89
100
  export const RecentActivityTitleServiceLive = Layer.effect(
90
101
  RecentActivityTitleServiceTag,
91
102
  Effect.gen(function* () {
103
+ const agentConfig = yield* AgentConfigServiceTag
92
104
  const recentActivityService = yield* RecentActivityServiceTag
93
105
  const helperModelRuntime = yield* HelperModelTag
94
- return makeRecentActivityTitleService(recentActivityService, helperModelRuntime)
106
+ return makeRecentActivityTitleService(agentConfig, recentActivityService, helperModelRuntime)
95
107
  }),
96
108
  )
@@ -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,