@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
@@ -1,17 +1,29 @@
1
1
  import type { Job } from 'bullmq'
2
- import { Effect } from 'effect'
2
+ import { Effect, Schema } from 'effect'
3
3
  import type { Context } from 'effect'
4
4
  import type IORedis from 'ioredis'
5
5
 
6
6
  import { serverLogger } from '../config/logger'
7
- import { ConfigurationError } from '../effect/errors'
8
7
  import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
9
- import { PlanAgentHeartbeatServiceTag } from '../services/plan/plan-agent-heartbeat.service'
10
- import type { WorkerHandle } from '../workers/worker-utils'
8
+ import { QueueJobServiceTag } from '../services/queue-job.service'
9
+ import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
11
10
  import { DEFAULT_JOB_RETENTION, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
12
- import { createQueueFactory } from './queue-factory'
11
+ import { createQueueFactoryWithDeps } from './queue-factory'
13
12
  import { runStandaloneQueueWorker } from './standalone-worker'
14
13
 
14
+ // Minimal service shape used by the worker processor. Declared structurally to
15
+ // avoid importing the service tag — which would form a dependency cycle since
16
+ // PlanAgentHeartbeatServiceLive depends on LotaQueuesServiceTag.
17
+ interface PlanAgentHeartbeatWorkerServiceShape {
18
+ wakeNode(params: PlanAgentHeartbeatWakeJob): Effect.Effect<unknown, unknown, unknown>
19
+ sweep(params: { organizationId?: string }): Effect.Effect<void, unknown, unknown>
20
+ }
21
+
22
+ class PlanAgentHeartbeatQueueError extends Schema.TaggedErrorClass<PlanAgentHeartbeatQueueError>()(
23
+ '@lota-sdk/core/PlanAgentHeartbeatQueueError',
24
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
25
+ ) {}
26
+
15
27
  export interface PlanAgentHeartbeatWakeJob {
16
28
  type: 'wake-node'
17
29
  organizationId: string
@@ -33,58 +45,11 @@ export const PLAN_AGENT_HEARTBEAT_QUEUE = 'plan-agent-heartbeat'
33
45
  const PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS = 30_000
34
46
  const PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID = 'plan-agent-heartbeat-sweep'
35
47
 
36
- interface PlanAgentHeartbeatQueueDeps {
48
+ export interface PlanAgentHeartbeatWorkerDeps {
37
49
  databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
38
- planAgentHeartbeatService: Context.Service.Shape<typeof PlanAgentHeartbeatServiceTag>
50
+ planAgentHeartbeatService: PlanAgentHeartbeatWorkerServiceShape
39
51
  }
40
52
 
41
- let _deps: PlanAgentHeartbeatQueueDeps | null = null
42
- function getDeps(): PlanAgentHeartbeatQueueDeps {
43
- if (!_deps)
44
- throw new ConfigurationError({
45
- message: 'Plan agent heartbeat queue is not configured. Initialize the runtime before starting the worker.',
46
- key: 'queue-deps',
47
- })
48
- return _deps
49
- }
50
-
51
- function enqueueDelayedPlanAgentHeartbeatSweep(delayMs = PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS): Promise<void> {
52
- return planAgentHeartbeatQueue.enqueue(
53
- { type: 'sweep' },
54
- { delay: delayMs, jobId: PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID },
55
- )
56
- }
57
-
58
- function processPlanAgentHeartbeatJob(job: Job<PlanAgentHeartbeatJob>): Promise<void> {
59
- const { planAgentHeartbeatService } = getDeps()
60
-
61
- return Effect.runPromise(
62
- Effect.gen(function* () {
63
- if (job.data.type === 'wake-node') {
64
- const wakeJob = job.data
65
- yield* planAgentHeartbeatService.wakeNode(wakeJob)
66
- return
67
- }
68
-
69
- yield* planAgentHeartbeatService.sweep({ organizationId: job.data.organizationId })
70
- if (!job.data.organizationId) {
71
- yield* Effect.tryPromise(() => enqueueDelayedPlanAgentHeartbeatSweep())
72
- }
73
- }),
74
- )
75
- }
76
-
77
- const planAgentHeartbeatQueue = createQueueFactory<PlanAgentHeartbeatJob>({
78
- name: PLAN_AGENT_HEARTBEAT_QUEUE,
79
- displayName: 'Plan agent heartbeat',
80
- jobName: 'plan-agent-heartbeat-job',
81
- concurrency: 2,
82
- lockDuration: LONG_JOB_LOCK_DURATION_MS,
83
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 5_000 } },
84
- prepare: () => getDeps().databaseService.connect(),
85
- processor: processPlanAgentHeartbeatJob,
86
- })
87
-
88
53
  function buildWakeJobId(params: {
89
54
  organizationId: string
90
55
  threadId: string
@@ -97,42 +62,105 @@ function buildWakeJobId(params: {
97
62
  return `plan-agent-wake__${encode(params.runId)}__${encode(params.nodeId)}__${encode(params.agentId)}`
98
63
  }
99
64
 
100
- export function enqueuePlanAgentHeartbeatWake(params: {
101
- organizationId: string
102
- threadId: string
103
- runId: string
104
- nodeId: string
105
- agentId: string
106
- reason: string
107
- }): Promise<void> {
108
- return planAgentHeartbeatQueue.enqueue({ type: 'wake-node', ...params }, { jobId: buildWakeJobId(params) })
65
+ export interface PlanAgentHeartbeatQueueRuntime {
66
+ enqueuePlanAgentHeartbeatWake(params: {
67
+ organizationId: string
68
+ threadId: string
69
+ runId: string
70
+ nodeId: string
71
+ agentId: string
72
+ reason: string
73
+ }): Promise<void>
74
+ startWorker(options: { registerSignals?: boolean; deps: PlanAgentHeartbeatWorkerDeps }): WorkerHandle
109
75
  }
110
76
 
111
- export function startPlanAgentHeartbeatWorker(options: {
112
- registerSignals?: boolean
77
+ interface MakePlanAgentHeartbeatQueueRuntimeParams {
113
78
  connectionProvider: () => IORedis
114
- deps: PlanAgentHeartbeatQueueDeps
115
- }): WorkerHandle {
116
- _deps = options.deps
117
- const handle = planAgentHeartbeatQueue.startWorker({
118
- registerSignals: options.registerSignals,
119
- connectionProvider: options.connectionProvider,
120
- })
79
+ queueJobService: QueueJobService
80
+ }
121
81
 
122
- enqueueDelayedPlanAgentHeartbeatSweep().catch((error: unknown) => {
123
- serverLogger.error`Plan agent heartbeat scheduler setup failed: ${error}`
82
+ export function makePlanAgentHeartbeatQueueRuntime(
83
+ params: MakePlanAgentHeartbeatQueueRuntimeParams,
84
+ ): PlanAgentHeartbeatQueueRuntime {
85
+ const { connectionProvider, queueJobService } = params
86
+
87
+ const enqueueDelayedSweep = (delayMs = PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS): Promise<void> =>
88
+ queue.enqueue({ type: 'sweep' }, { delay: delayMs, jobId: PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID })
89
+
90
+ const processPlanAgentHeartbeatJob = (
91
+ deps: PlanAgentHeartbeatWorkerDeps,
92
+ job: Job<PlanAgentHeartbeatJob>,
93
+ ): Promise<void> => {
94
+ const { planAgentHeartbeatService: service } = deps
95
+ return Effect.runPromise(
96
+ Effect.gen(function* () {
97
+ if (job.data.type === 'wake-node') {
98
+ const wakeJob = job.data
99
+ yield* service.wakeNode(wakeJob)
100
+ return
101
+ }
102
+
103
+ yield* service.sweep({ organizationId: job.data.organizationId })
104
+ if (!job.data.organizationId) {
105
+ yield* Effect.tryPromise({
106
+ try: () => enqueueDelayedSweep(),
107
+ catch: (cause) =>
108
+ new PlanAgentHeartbeatQueueError({
109
+ message: 'Failed to enqueue delayed plan-agent heartbeat sweep.',
110
+ cause,
111
+ }),
112
+ })
113
+ }
114
+ }) as Effect.Effect<void, never, never>,
115
+ )
116
+ }
117
+
118
+ const queue = createQueueFactoryWithDeps<PlanAgentHeartbeatJob, PlanAgentHeartbeatWorkerDeps>({
119
+ name: PLAN_AGENT_HEARTBEAT_QUEUE,
120
+ displayName: 'Plan agent heartbeat',
121
+ jobName: 'plan-agent-heartbeat-job',
122
+ concurrency: 2,
123
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
124
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 5_000 } },
125
+ connectionProvider,
126
+ queueJobService,
127
+ prepare: ({ databaseService }) => Effect.runPromise(databaseService.connect()),
128
+ processor: processPlanAgentHeartbeatJob,
124
129
  })
125
130
 
126
- return handle
131
+ return {
132
+ enqueuePlanAgentHeartbeatWake: (wakeParams) =>
133
+ queue.enqueue({ type: 'wake-node', ...wakeParams }, { jobId: buildWakeJobId(wakeParams) }),
134
+ startWorker: (options) => {
135
+ const handle = queue.startWorker({
136
+ deps: options.deps,
137
+ registerSignals: options.registerSignals,
138
+ connectionProvider,
139
+ })
140
+
141
+ enqueueDelayedSweep().catch((error: unknown) => {
142
+ serverLogger.error`Plan agent heartbeat scheduler setup failed: ${error}`
143
+ })
144
+
145
+ return handle
146
+ },
147
+ }
127
148
  }
128
149
 
129
150
  runStandaloneQueueWorker((runtime) => {
130
- const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
131
- startPlanAgentHeartbeatWorker({
132
- connectionProvider: () => resolve(RedisServiceTag).getConnectionForBullMQ(),
133
- deps: {
134
- databaseService: resolve(DatabaseServiceTag),
135
- planAgentHeartbeatService: resolve(PlanAgentHeartbeatServiceTag),
136
- },
137
- })
151
+ void (async () => {
152
+ const { PlanAgentHeartbeatServiceTag } = await import('../services/plan/plan-agent-heartbeat.service')
153
+ const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
154
+ const redis = resolve(RedisServiceTag)
155
+ const planAgentHeartbeatQueue = makePlanAgentHeartbeatQueueRuntime({
156
+ connectionProvider: () => redis.getConnectionForBullMQ(),
157
+ queueJobService: resolve(QueueJobServiceTag),
158
+ })
159
+ planAgentHeartbeatQueue.startWorker({
160
+ deps: {
161
+ databaseService: resolve(DatabaseServiceTag),
162
+ planAgentHeartbeatService: resolve(PlanAgentHeartbeatServiceTag),
163
+ },
164
+ })
165
+ })()
138
166
  })
@@ -4,15 +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 { ConfigurationError } from '../effect/errors'
8
7
  import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
9
8
  import { PlanCycleServiceTag } from '../services/plan/plan-cycle.service'
10
9
  import { PlanDeadlineServiceTag } from '../services/plan/plan-deadline.service'
11
10
  import { PlanExecutorServiceTag } from '../services/plan/plan-executor.service'
12
11
  import { PlanSchedulerServiceTag } from '../services/plan/plan-scheduler.service'
12
+ import { QueueJobServiceTag } from '../services/queue-job.service'
13
13
  import { nowEpochMillis } from '../utils/date-time'
14
- import type { WorkerHandle } from '../workers/worker-utils'
15
- import { createQueueFactory } from './queue-factory'
14
+ import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
15
+ import { createQueueFactoryWithDeps } from './queue-factory'
16
16
  import { runStandaloneQueueWorker } from './standalone-worker'
17
17
 
18
18
  export interface PlanSchedulerFireJob {
@@ -29,7 +29,7 @@ export type PlanSchedulerJob = PlanSchedulerFireJob | PlanSchedulerDeadlineJob
29
29
 
30
30
  export const PLAN_SCHEDULER_QUEUE = 'plan-scheduler'
31
31
 
32
- interface PlanSchedulerQueueDeps {
32
+ export interface PlanSchedulerWorkerDeps {
33
33
  databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
34
34
  planSchedulerService: Context.Service.Shape<typeof PlanSchedulerServiceTag>
35
35
  planDeadlineService: Context.Service.Shape<typeof PlanDeadlineServiceTag>
@@ -37,16 +37,6 @@ interface PlanSchedulerQueueDeps {
37
37
  planCycleService: Context.Service.Shape<typeof PlanCycleServiceTag>
38
38
  }
39
39
 
40
- let _deps: PlanSchedulerQueueDeps | null = null
41
- function getDeps(): PlanSchedulerQueueDeps {
42
- if (!_deps)
43
- throw new ConfigurationError({
44
- message: 'Plan scheduler queue is not configured. Initialize the runtime before starting the worker.',
45
- key: 'queue-deps',
46
- })
47
- return _deps
48
- }
49
-
50
40
  class PlanSchedulerQueueError extends Schema.TaggedErrorClass<PlanSchedulerQueueError>()('PlanSchedulerQueueError', {
51
41
  stage: Schema.Literals(['remove-schedule-fire-job', 'recover-active-schedules', 'recover-deadline-checks']),
52
42
  message: Schema.String,
@@ -57,111 +47,125 @@ function toPlanSchedulerQueueError(stage: PlanSchedulerQueueError['stage'], caus
57
47
  return new PlanSchedulerQueueError({ stage, message: cause instanceof Error ? cause.message : String(cause), cause })
58
48
  }
59
49
 
60
- function processPlanSchedulerJob(job: Job<PlanSchedulerJob>): Promise<void> {
61
- const { planSchedulerService, planDeadlineService, planExecutorService, planCycleService } = getDeps()
50
+ function processPlanSchedulerJob(deps: PlanSchedulerWorkerDeps, job: Job<PlanSchedulerJob>): Promise<void> {
51
+ 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>))
62
56
 
63
57
  switch (job.data.type) {
64
58
  case 'fire-schedule':
65
- return Effect.runPromise(
66
- Effect.asVoid(
67
- planSchedulerService.fireScheduleById(job.data.scheduleId, {
68
- promoteDelayedNode: (params) => planExecutorService.promoteDelayedNode(params),
69
- advanceCycle: (cycleId) => planCycleService.advanceCycle(cycleId),
70
- recoverDeadlineChecks: () => Effect.runPromise(Effect.asVoid(planDeadlineService.recoverDeadlineChecks())),
71
- }),
72
- ),
59
+ return runWithResolvedContext(
60
+ planSchedulerService.fireScheduleById(job.data.scheduleId, {
61
+ promoteDelayedNode: (params) => runWithResolvedContext(planExecutorService.promoteDelayedNode(params)),
62
+ advanceCycle: (cycleId) => runWithResolvedContext(planCycleService.advanceCycle(cycleId)),
63
+ recoverDeadlineChecks: () => runWithResolvedContext(planDeadlineService.recoverDeadlineChecks()),
64
+ }),
73
65
  )
74
66
  case 'check-deadlines':
75
- return Effect.runPromise(Effect.asVoid(planDeadlineService.checkDeadlines()))
67
+ return runWithResolvedContext(planDeadlineService.checkDeadlines())
76
68
  }
77
69
  }
78
70
 
79
- const planScheduler = createQueueFactory<PlanSchedulerJob>({
80
- name: PLAN_SCHEDULER_QUEUE,
81
- displayName: 'Plan scheduler',
82
- jobName: 'plan-scheduler-job',
83
- concurrency: 1,
84
- defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 5000 } },
85
- prepare: () => getDeps().databaseService.connect(),
86
- processor: processPlanSchedulerJob,
87
- })
88
-
89
- /** Enqueue a delayed job that fires a specific schedule at its nextFireAt time. */
90
- export function enqueueScheduleFire(scheduleId: string, delayMs: number): Promise<void> {
91
- return planScheduler.enqueue(
92
- { type: 'fire-schedule', scheduleId },
93
- { delay: Math.max(0, delayMs), jobId: `schedule:${scheduleId}`, removeOnComplete: true, removeOnFail: 50 },
94
- )
95
- }
96
-
97
- /** Remove a pending fire job for a schedule (on cancel/pause/complete). */
98
- export function removeScheduleFireJob(scheduleId: string): Promise<void> {
99
- return Effect.runPromise(
100
- Effect.tryPromise({
101
- try: () => planScheduler.getQueue().remove(`schedule:${scheduleId}`),
102
- catch: (cause) => toPlanSchedulerQueueError('remove-schedule-fire-job', cause),
103
- }).pipe(Effect.asVoid),
104
- )
105
- }
106
-
107
71
  const DEADLINE_CHECK_JOB_PREFIX = 'deadline-check'
108
72
 
109
- export function enqueueDeadlineCheck(scheduledFor: Date): Promise<void> {
110
- const delay = Math.max(0, scheduledFor.getTime() - nowEpochMillis())
111
- return planScheduler.enqueue(
112
- { type: 'check-deadlines', scheduledFor: scheduledFor.toISOString() },
113
- {
114
- delay,
115
- jobId: `${DEADLINE_CHECK_JOB_PREFIX}:${scheduledFor.getTime()}`,
116
- removeOnComplete: true,
117
- removeOnFail: 50,
118
- },
119
- )
73
+ export interface PlanSchedulerQueueRuntime {
74
+ enqueueScheduleFire(scheduleId: string, delayMs: number): Promise<void>
75
+ removeScheduleFireJob(scheduleId: string): Promise<void>
76
+ enqueueDeadlineCheck(scheduledFor: Date): Promise<void>
77
+ startWorker(options: { registerSignals?: boolean; deps: PlanSchedulerWorkerDeps }): WorkerHandle
120
78
  }
121
79
 
122
- export function startPlanSchedulerWorker(options: {
123
- registerSignals?: boolean
80
+ interface MakePlanSchedulerQueueRuntimeParams {
124
81
  connectionProvider: () => IORedis
125
- deps: PlanSchedulerQueueDeps
126
- }): WorkerHandle {
127
- _deps = options.deps
128
- const handle = planScheduler.startWorker({
129
- registerSignals: options.registerSignals,
130
- connectionProvider: options.connectionProvider,
82
+ queueJobService: QueueJobService
83
+ }
84
+
85
+ export function makePlanSchedulerQueueRuntime(params: MakePlanSchedulerQueueRuntimeParams): PlanSchedulerQueueRuntime {
86
+ const { connectionProvider, queueJobService } = params
87
+
88
+ const queue = createQueueFactoryWithDeps<PlanSchedulerJob, PlanSchedulerWorkerDeps>({
89
+ name: PLAN_SCHEDULER_QUEUE,
90
+ displayName: 'Plan scheduler',
91
+ jobName: 'plan-scheduler-job',
92
+ concurrency: 1,
93
+ defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 5000 } },
94
+ connectionProvider,
95
+ queueJobService,
96
+ prepare: ({ databaseService }) => Effect.runPromise(databaseService.connect()),
97
+ processor: processPlanSchedulerJob,
131
98
  })
132
99
 
133
- // Recover active schedules on startup
134
- void Effect.runFork(
135
- options.deps.planSchedulerService.recoverActiveSchedules().pipe(
136
- Effect.mapError((cause) => toPlanSchedulerQueueError('recover-active-schedules', cause)),
137
- Effect.catchTag('PlanSchedulerQueueError', (error) =>
138
- Effect.sync(() => {
139
- serverLogger.error`Plan scheduler startup recovery failed: ${error.message}`
140
- }),
100
+ const enqueueScheduleFire: PlanSchedulerQueueRuntime['enqueueScheduleFire'] = (scheduleId, delayMs) =>
101
+ queue.enqueue(
102
+ { type: 'fire-schedule', scheduleId },
103
+ { delay: Math.max(0, delayMs), jobId: `schedule:${scheduleId}`, removeOnComplete: true, removeOnFail: 50 },
104
+ )
105
+
106
+ const removeScheduleFireJob: PlanSchedulerQueueRuntime['removeScheduleFireJob'] = (scheduleId) =>
107
+ Effect.runPromise(
108
+ Effect.tryPromise({
109
+ try: () => queue.getQueue().remove(`schedule:${scheduleId}`),
110
+ catch: (cause) => toPlanSchedulerQueueError('remove-schedule-fire-job', cause),
111
+ }).pipe(Effect.asVoid),
112
+ )
113
+
114
+ const enqueueDeadlineCheck: PlanSchedulerQueueRuntime['enqueueDeadlineCheck'] = (scheduledFor) => {
115
+ const delay = Math.max(0, scheduledFor.getTime() - nowEpochMillis())
116
+ return queue.enqueue(
117
+ { type: 'check-deadlines', scheduledFor: scheduledFor.toISOString() },
118
+ {
119
+ delay,
120
+ jobId: `${DEADLINE_CHECK_JOB_PREFIX}:${scheduledFor.getTime()}`,
121
+ removeOnComplete: true,
122
+ removeOnFail: 50,
123
+ },
124
+ )
125
+ }
126
+
127
+ const startWorker: PlanSchedulerQueueRuntime['startWorker'] = (options) => {
128
+ const { deps } = options
129
+ const handle = queue.startWorker({ deps, registerSignals: options.registerSignals, connectionProvider })
130
+
131
+ // Recover active schedules on startup
132
+ void Effect.runFork(
133
+ deps.planSchedulerService.recoverActiveSchedules().pipe(
134
+ Effect.mapError((cause) => toPlanSchedulerQueueError('recover-active-schedules', cause)),
135
+ Effect.catchTag('PlanSchedulerQueueError', (error) =>
136
+ Effect.sync(() => {
137
+ serverLogger.error`Plan scheduler startup recovery failed: ${error.message}`
138
+ }),
139
+ ),
141
140
  ),
142
- ),
143
- )
144
-
145
- // Seed deadline checks from current runtime state. Subsequent checks are
146
- // re-seeded by the queue job after each sweep and by schedule fire events.
147
- void Effect.runFork(
148
- options.deps.planDeadlineService.recoverDeadlineChecks().pipe(
149
- Effect.mapError((cause) => toPlanSchedulerQueueError('recover-deadline-checks', cause)),
150
- Effect.catchTag('PlanSchedulerQueueError', (error) =>
151
- Effect.sync(() => {
152
- serverLogger.error`Plan deadline recovery failed: ${error.message}`
153
- }),
141
+ )
142
+
143
+ // Seed deadline checks from current runtime state.
144
+ void Effect.runFork(
145
+ deps.planDeadlineService.recoverDeadlineChecks().pipe(
146
+ Effect.mapError((cause) => toPlanSchedulerQueueError('recover-deadline-checks', cause)),
147
+ Effect.catchTag('PlanSchedulerQueueError', (error) =>
148
+ Effect.sync(() => {
149
+ serverLogger.error`Plan deadline recovery failed: ${error.message}`
150
+ }),
151
+ ),
154
152
  ),
155
- ),
156
- )
153
+ )
157
154
 
158
- return handle
155
+ return handle
156
+ }
157
+
158
+ return { enqueueScheduleFire, removeScheduleFireJob, enqueueDeadlineCheck, startWorker }
159
159
  }
160
160
 
161
161
  runStandaloneQueueWorker((runtime) => {
162
162
  const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
163
- startPlanSchedulerWorker({
164
- connectionProvider: () => resolve(RedisServiceTag).getConnectionForBullMQ(),
163
+ const redis = resolve(RedisServiceTag)
164
+ const planSchedulerQueue = makePlanSchedulerQueueRuntime({
165
+ connectionProvider: () => redis.getConnectionForBullMQ(),
166
+ queueJobService: resolve(QueueJobServiceTag),
167
+ })
168
+ planSchedulerQueue.startWorker({
165
169
  deps: {
166
170
  databaseService: resolve(DatabaseServiceTag),
167
171
  planSchedulerService: resolve(PlanSchedulerServiceTag),
@@ -3,11 +3,12 @@ import { Effect } from 'effect'
3
3
  import type { Context } from 'effect'
4
4
  import type IORedis from 'ioredis'
5
5
 
6
- import type { MaybeAwaitableService } from '../effect/awaitable-effect'
7
- import { ConfigurationError } from '../effect/errors'
8
6
  import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
7
+ import type { RedisConnectionManager } from '../redis/connection'
9
8
  import { MemoryServiceTag } from '../services/memory/memory.service'
10
- import { createQueueFactory } from './queue-factory'
9
+ import { QueueJobServiceTag } from '../services/queue-job.service'
10
+ import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
11
+ import { createQueueFactoryWithDeps } from './queue-factory'
11
12
  import { runStandaloneQueueWorker } from './standalone-worker'
12
13
 
13
14
  interface PostChatMemoryMessage {
@@ -16,7 +17,7 @@ interface PostChatMemoryMessage {
16
17
  agentName?: string
17
18
  }
18
19
 
19
- interface PostChatMemoryExtractionJob {
20
+ export interface PostChatMemoryExtractionJob {
20
21
  orgId: string
21
22
  threadId: string
22
23
  sourceId: string
@@ -30,24 +31,17 @@ interface PostChatMemoryExtractionJob {
30
31
  attachmentContext?: string
31
32
  }
32
33
 
33
- interface PostChatMemoryQueueDeps {
34
+ export interface PostChatMemoryWorkerDeps {
34
35
  databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
35
- memoryService: MaybeAwaitableService<Context.Service.Shape<typeof MemoryServiceTag>>
36
+ memoryService: Context.Service.Shape<typeof MemoryServiceTag>
37
+ redisManager: RedisConnectionManager
36
38
  }
37
39
 
38
- let _deps: PostChatMemoryQueueDeps | null = null
39
- function getDeps(): PostChatMemoryQueueDeps {
40
- if (!_deps)
41
- throw new ConfigurationError({
42
- message: 'Post-chat memory queue is not configured. Initialize the runtime before starting the worker.',
43
- key: 'queue-deps',
44
- })
45
- return _deps
46
- }
47
-
48
- function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>): Promise<void> {
49
- const { memoryService } = getDeps()
50
-
40
+ function processPostChatMemoryJob(
41
+ deps: PostChatMemoryWorkerDeps,
42
+ job: Job<PostChatMemoryExtractionJob>,
43
+ ): Promise<void> {
44
+ const { memoryService, redisManager } = deps
51
45
  const data = job.data
52
46
  const userMessage = data.userMessage.trim()
53
47
  const agentMessages = data.agentMessages
@@ -89,43 +83,59 @@ function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>): Promis
89
83
  attachmentContext: data.attachmentContext,
90
84
  agentNames: uniqueAgentNames,
91
85
  }),
92
- ),
86
+ ).pipe(Effect.provideService(RedisServiceTag, redisManager)),
93
87
  )
94
88
  }
95
89
 
96
- const postChatMemory = createQueueFactory<PostChatMemoryExtractionJob>({
97
- name: 'post-chat-memory',
98
- displayName: 'Post-chat memory',
99
- jobName: 'extract-memory',
100
- concurrency: 10,
101
- lockDuration: 900_000,
102
- maxStalledCount: 10,
103
- stalledInterval: 120_000,
104
- defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
105
- prepare: () => getDeps().databaseService.connect(),
106
- processor: processPostChatMemoryJob,
107
- })
108
-
109
- export function enqueuePostChatMemory(job: PostChatMemoryExtractionJob, options?: { dedupeKey?: string }) {
110
- return postChatMemory.enqueue(job, options?.dedupeKey ? { jobId: options.dedupeKey } : undefined)
90
+ export interface PostChatMemoryQueueRuntime {
91
+ enqueuePostChatMemory(job: PostChatMemoryExtractionJob, options?: { dedupeKey?: string }): Promise<void>
92
+ startWorker(options: { registerSignals?: boolean; deps: PostChatMemoryWorkerDeps }): WorkerHandle
111
93
  }
112
94
 
113
- export function startPostChatMemoryWorker(options: {
114
- registerSignals?: boolean
95
+ interface MakePostChatMemoryQueueRuntimeParams {
115
96
  connectionProvider: () => IORedis
116
- deps: PostChatMemoryQueueDeps
117
- }) {
118
- _deps = options.deps
119
- return postChatMemory.startWorker({
120
- registerSignals: options.registerSignals,
121
- connectionProvider: options.connectionProvider,
97
+ queueJobService: QueueJobService
98
+ }
99
+
100
+ export function makePostChatMemoryQueueRuntime(
101
+ params: MakePostChatMemoryQueueRuntimeParams,
102
+ ): PostChatMemoryQueueRuntime {
103
+ const { connectionProvider, queueJobService } = params
104
+ const queue = createQueueFactoryWithDeps<PostChatMemoryExtractionJob, PostChatMemoryWorkerDeps>({
105
+ name: 'post-chat-memory',
106
+ displayName: 'Post-chat memory',
107
+ jobName: 'extract-memory',
108
+ concurrency: 10,
109
+ lockDuration: 900_000,
110
+ maxStalledCount: 10,
111
+ stalledInterval: 120_000,
112
+ defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
113
+ connectionProvider,
114
+ queueJobService,
115
+ prepare: ({ databaseService }) => Effect.runPromise(databaseService.connect()),
116
+ processor: processPostChatMemoryJob,
122
117
  })
118
+
119
+ return {
120
+ enqueuePostChatMemory: (job, options) =>
121
+ queue.enqueue(job, options?.dedupeKey ? { jobId: options.dedupeKey } : undefined),
122
+ startWorker: (options) =>
123
+ queue.startWorker({ deps: options.deps, registerSignals: options.registerSignals, connectionProvider }),
124
+ }
123
125
  }
124
126
 
125
127
  runStandaloneQueueWorker((runtime) => {
126
128
  const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
127
- startPostChatMemoryWorker({
128
- connectionProvider: () => resolve(RedisServiceTag).getConnectionForBullMQ(),
129
- deps: { databaseService: resolve(DatabaseServiceTag), memoryService: resolve(MemoryServiceTag) },
129
+ const redis = resolve(RedisServiceTag)
130
+ const postChatMemoryQueue = makePostChatMemoryQueueRuntime({
131
+ connectionProvider: () => redis.getConnectionForBullMQ(),
132
+ queueJobService: resolve(QueueJobServiceTag),
133
+ })
134
+ postChatMemoryQueue.startWorker({
135
+ deps: {
136
+ databaseService: resolve(DatabaseServiceTag),
137
+ memoryService: resolve(MemoryServiceTag),
138
+ redisManager: redis,
139
+ },
130
140
  })
131
141
  })