@lota-sdk/core 0.4.8 → 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 (272) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +96 -22
  3. package/src/ai-gateway/ai-gateway.ts +766 -223
  4. package/src/config/agent-defaults.ts +189 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/constants.ts +8 -2
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +299 -19
  10. package/src/config/thread-defaults.ts +40 -20
  11. package/src/create-runtime.ts +200 -449
  12. package/src/db/base.service.ts +52 -28
  13. package/src/db/cursor-pagination.ts +71 -30
  14. package/src/db/memory-query-builder.ts +2 -1
  15. package/src/db/memory-store.helpers.ts +4 -7
  16. package/src/db/memory-store.ts +868 -601
  17. package/src/db/memory.ts +396 -280
  18. package/src/db/record-id.ts +32 -10
  19. package/src/db/schema-fingerprint.ts +30 -12
  20. package/src/db/service-normalization.ts +288 -0
  21. package/src/db/service.ts +912 -779
  22. package/src/db/startup.ts +153 -68
  23. package/src/db/transaction-conflict.ts +15 -0
  24. package/src/effect/awaitable-effect.ts +96 -0
  25. package/src/effect/errors.ts +121 -0
  26. package/src/effect/helpers.ts +123 -0
  27. package/src/effect/index.ts +24 -0
  28. package/src/effect/layers.ts +238 -0
  29. package/src/effect/runtime-ref.ts +25 -0
  30. package/src/effect/runtime.ts +46 -0
  31. package/src/effect/services.ts +61 -0
  32. package/src/effect/zod.ts +43 -0
  33. package/src/embeddings/provider.ts +128 -83
  34. package/src/index.ts +48 -1
  35. package/src/openrouter/direct-provider.ts +11 -35
  36. package/src/queues/autonomous-job.queue.ts +117 -73
  37. package/src/queues/context-compaction.queue.ts +50 -17
  38. package/src/queues/delayed-node-promotion.queue.ts +46 -17
  39. package/src/queues/document-processor.queue.ts +52 -77
  40. package/src/queues/memory-consolidation.queue.ts +47 -32
  41. package/src/queues/organization-learning.queue.ts +26 -4
  42. package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
  43. package/src/queues/plan-scheduler.queue.ts +97 -33
  44. package/src/queues/post-chat-memory.queue.ts +56 -26
  45. package/src/queues/queue-factory.ts +227 -59
  46. package/src/queues/standalone-worker.ts +39 -0
  47. package/src/queues/title-generation.queue.ts +45 -11
  48. package/src/redis/connection.ts +182 -113
  49. package/src/redis/index.ts +6 -8
  50. package/src/redis/org-memory-lock.ts +60 -27
  51. package/src/redis/redis-lease-lock.ts +200 -121
  52. package/src/redis/runtime-connection.ts +20 -0
  53. package/src/redis/stream-context.ts +92 -46
  54. package/src/runtime/agent-identity-overrides.ts +2 -2
  55. package/src/runtime/agent-runtime-policy.ts +5 -2
  56. package/src/runtime/agent-stream-helpers.ts +24 -9
  57. package/src/runtime/chat-run-orchestration.ts +102 -19
  58. package/src/runtime/chat-run-registry.ts +36 -2
  59. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  60. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
  61. package/src/runtime/domain-layer.ts +192 -0
  62. package/src/runtime/execution-plan-visibility.ts +2 -2
  63. package/src/runtime/execution-plan.ts +42 -15
  64. package/src/runtime/graph-designer.ts +16 -4
  65. package/src/runtime/helper-model.ts +139 -48
  66. package/src/runtime/index.ts +7 -8
  67. package/src/runtime/indexed-repositories-policy.ts +3 -3
  68. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
  69. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  70. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
  71. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  72. package/src/runtime/memory/memory-scope.ts +53 -0
  73. package/src/runtime/plugin-resolution.ts +124 -25
  74. package/src/runtime/plugin-types.ts +9 -1
  75. package/src/runtime/post-turn-side-effects.ts +177 -130
  76. package/src/runtime/retrieval-adapters.ts +40 -6
  77. package/src/runtime/runtime-accessors.ts +92 -0
  78. package/src/runtime/runtime-config.ts +150 -61
  79. package/src/runtime/runtime-extensions.ts +23 -25
  80. package/src/runtime/runtime-lifecycle.ts +124 -0
  81. package/src/runtime/runtime-services.ts +386 -0
  82. package/src/runtime/runtime-token.ts +47 -0
  83. package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
  84. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
  85. package/src/runtime/social-chat/social-chat.ts +630 -0
  86. package/src/runtime/specialist-runner.ts +36 -10
  87. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
  88. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  89. package/src/runtime/thread-chat-helpers.ts +2 -2
  90. package/src/runtime/thread-plan-turn.ts +2 -1
  91. package/src/runtime/thread-turn-context.ts +183 -111
  92. package/src/runtime/turn-lifecycle.ts +93 -27
  93. package/src/services/agent-activity.service.ts +287 -203
  94. package/src/services/agent-executor.service.ts +253 -149
  95. package/src/services/artifact.service.ts +231 -149
  96. package/src/services/attachment.service.ts +171 -115
  97. package/src/services/autonomous-job.service.ts +890 -491
  98. package/src/services/background-work.service.ts +54 -0
  99. package/src/services/chat-run-registry.service.ts +13 -1
  100. package/src/services/context-compaction.service.ts +136 -86
  101. package/src/services/document-chunk.service.ts +151 -88
  102. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  103. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  104. package/src/services/execution-plan/execution-plan-graph.ts +278 -0
  105. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  106. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  107. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  108. package/src/services/feedback-loop.service.ts +132 -76
  109. package/src/services/global-orchestrator.service.ts +101 -168
  110. package/src/services/graph-full-routing.ts +193 -0
  111. package/src/services/index.ts +19 -21
  112. package/src/services/institutional-memory.service.ts +213 -125
  113. package/src/services/learned-skill.service.ts +368 -260
  114. package/src/services/memory/memory-conversation.ts +95 -0
  115. package/src/services/memory/memory-errors.ts +27 -0
  116. package/src/services/memory/memory-org-memory.ts +50 -0
  117. package/src/services/memory/memory-preseeded.ts +86 -0
  118. package/src/services/memory/memory-rerank.ts +297 -0
  119. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
  120. package/src/services/memory/memory.service.ts +674 -0
  121. package/src/services/memory/rerank.service.ts +201 -0
  122. package/src/services/monitoring-window.service.ts +92 -70
  123. package/src/services/mutating-approval.service.ts +62 -53
  124. package/src/services/node-workspace.service.ts +141 -98
  125. package/src/services/notification.service.ts +29 -16
  126. package/src/services/organization-member.service.ts +120 -66
  127. package/src/services/organization.service.ts +153 -77
  128. package/src/services/ownership-dispatcher.service.ts +456 -263
  129. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  130. package/src/services/plan/plan-agent-query.service.ts +322 -0
  131. package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
  132. package/src/services/plan/plan-artifact.service.ts +60 -0
  133. package/src/services/plan/plan-builder.service.ts +76 -0
  134. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  135. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  136. package/src/services/plan/plan-completion-side-effects.ts +169 -0
  137. package/src/services/plan/plan-coordination.service.ts +181 -0
  138. package/src/services/plan/plan-cycle.service.ts +405 -0
  139. package/src/services/plan/plan-deadline.service.ts +533 -0
  140. package/src/services/plan/plan-event-delivery.service.ts +266 -0
  141. package/src/services/plan/plan-executor-context.ts +35 -0
  142. package/src/services/plan/plan-executor-graph.ts +522 -0
  143. package/src/services/plan/plan-executor-helpers.ts +307 -0
  144. package/src/services/plan/plan-executor-persistence.ts +209 -0
  145. package/src/services/plan/plan-executor.service.ts +1737 -0
  146. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  147. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  148. package/src/services/plan/plan-run-serialization.ts +15 -0
  149. package/src/services/plan/plan-run.service.ts +637 -0
  150. package/src/services/plan/plan-scheduler.service.ts +379 -0
  151. package/src/services/plan/plan-template.service.ts +224 -0
  152. package/src/services/plan/plan-transaction-events.ts +36 -0
  153. package/src/services/plan/plan-validator.service.ts +907 -0
  154. package/src/services/plan/plan-workspace.service.ts +131 -0
  155. package/src/services/plugin-executor.service.ts +102 -68
  156. package/src/services/quality-metrics.service.ts +112 -94
  157. package/src/services/queue-job.service.ts +288 -231
  158. package/src/services/recent-activity-title.service.ts +73 -36
  159. package/src/services/recent-activity.service.ts +274 -259
  160. package/src/services/skill-resolver.service.ts +38 -12
  161. package/src/services/social-chat-history.service.ts +190 -122
  162. package/src/services/system-executor.service.ts +96 -61
  163. package/src/services/thread/thread-active-run.ts +203 -0
  164. package/src/services/thread/thread-bootstrap.ts +385 -0
  165. package/src/services/thread/thread-listing.ts +199 -0
  166. package/src/services/thread/thread-memory-block.ts +130 -0
  167. package/src/services/thread/thread-message.service.ts +379 -0
  168. package/src/services/thread/thread-record-store.ts +155 -0
  169. package/src/services/thread/thread-title.service.ts +74 -0
  170. package/src/services/thread/thread-turn-execution.ts +280 -0
  171. package/src/services/thread/thread-turn-message-context.ts +73 -0
  172. package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
  173. package/src/services/thread/thread-turn-streaming.ts +403 -0
  174. package/src/services/thread/thread-turn-tracing.ts +35 -0
  175. package/src/services/thread/thread-turn.ts +376 -0
  176. package/src/services/thread/thread.service.ts +344 -0
  177. package/src/services/user.service.ts +82 -32
  178. package/src/services/write-intent-validator.service.ts +63 -51
  179. package/src/storage/attachment-parser.ts +69 -27
  180. package/src/storage/attachment-storage.service.ts +334 -275
  181. package/src/storage/generated-document-storage.service.ts +66 -34
  182. package/src/system-agents/agent-result.ts +3 -1
  183. package/src/system-agents/context-compaction.agent.ts +3 -3
  184. package/src/system-agents/delegated-agent-factory.ts +159 -90
  185. package/src/system-agents/helper-agent-options.ts +1 -1
  186. package/src/system-agents/memory-reranker.agent.ts +3 -3
  187. package/src/system-agents/memory.agent.ts +3 -3
  188. package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
  189. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
  190. package/src/system-agents/skill-extractor.agent.ts +3 -3
  191. package/src/system-agents/skill-manager.agent.ts +3 -3
  192. package/src/system-agents/thread-router.agent.ts +157 -113
  193. package/src/system-agents/title-generator.agent.ts +3 -3
  194. package/src/tools/execution-plan.tool.ts +241 -171
  195. package/src/tools/fetch-webpage.tool.ts +29 -18
  196. package/src/tools/firecrawl-client.ts +26 -6
  197. package/src/tools/index.ts +1 -0
  198. package/src/tools/memory-block.tool.ts +14 -6
  199. package/src/tools/plan-approval.tool.ts +57 -47
  200. package/src/tools/read-file-parts.tool.ts +44 -33
  201. package/src/tools/remember-memory.tool.ts +65 -45
  202. package/src/tools/search-web.tool.ts +33 -22
  203. package/src/tools/search.tool.ts +41 -29
  204. package/src/tools/team-think.tool.ts +125 -84
  205. package/src/tools/user-questions.tool.ts +4 -3
  206. package/src/tools/web-tool-shared.ts +6 -0
  207. package/src/utils/async.ts +25 -22
  208. package/src/utils/crypto.ts +21 -0
  209. package/src/utils/date-time.ts +40 -1
  210. package/src/utils/errors.ts +111 -20
  211. package/src/utils/hono-error-handler.ts +24 -39
  212. package/src/utils/index.ts +2 -1
  213. package/src/utils/null-proto-record.ts +41 -0
  214. package/src/utils/sse-keepalive.ts +124 -21
  215. package/src/workers/bootstrap.ts +164 -52
  216. package/src/workers/memory-consolidation.worker.ts +325 -237
  217. package/src/workers/organization-learning.worker.ts +50 -16
  218. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  219. package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
  220. package/src/workers/skill-extraction.runner.ts +176 -93
  221. package/src/workers/utils/file-section-chunker.ts +8 -10
  222. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  223. package/src/workers/utils/repomix-file-sections.ts +2 -2
  224. package/src/workers/utils/thread-message-query.ts +97 -38
  225. package/src/workers/worker-utils.ts +74 -31
  226. package/src/config/debug-logger.ts +0 -47
  227. package/src/config/search.ts +0 -3
  228. package/src/redis/connection-accessor.ts +0 -26
  229. package/src/runtime/agent-types.ts +0 -1
  230. package/src/runtime/context-compaction-runtime.ts +0 -87
  231. package/src/runtime/memory-scope.ts +0 -43
  232. package/src/runtime/social-chat-agent-runner.ts +0 -118
  233. package/src/runtime/social-chat.ts +0 -516
  234. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  235. package/src/services/adaptive-playbook.service.ts +0 -152
  236. package/src/services/artifact-provenance.service.ts +0 -172
  237. package/src/services/chat-attachments.service.ts +0 -17
  238. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  239. package/src/services/execution-plan.service.ts +0 -1118
  240. package/src/services/memory.service.ts +0 -914
  241. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  242. package/src/services/plan-agent-query.service.ts +0 -267
  243. package/src/services/plan-artifact.service.ts +0 -50
  244. package/src/services/plan-builder.service.ts +0 -67
  245. package/src/services/plan-checkpoint.service.ts +0 -81
  246. package/src/services/plan-completion-side-effects.ts +0 -80
  247. package/src/services/plan-coordination.service.ts +0 -157
  248. package/src/services/plan-cycle.service.ts +0 -284
  249. package/src/services/plan-deadline.service.ts +0 -430
  250. package/src/services/plan-event-delivery.service.ts +0 -166
  251. package/src/services/plan-executor.service.ts +0 -1950
  252. package/src/services/plan-run.service.ts +0 -515
  253. package/src/services/plan-scheduler.service.ts +0 -240
  254. package/src/services/plan-template.service.ts +0 -177
  255. package/src/services/plan-validator.service.ts +0 -818
  256. package/src/services/plan-workspace.service.ts +0 -83
  257. package/src/services/rerank.service.ts +0 -156
  258. package/src/services/thread-message.service.ts +0 -275
  259. package/src/services/thread-plan-registry.service.ts +0 -22
  260. package/src/services/thread-title.service.ts +0 -39
  261. package/src/services/thread-turn-preparation.service.ts +0 -1147
  262. package/src/services/thread-turn.ts +0 -172
  263. package/src/services/thread.service.ts +0 -869
  264. package/src/utils/env.ts +0 -8
  265. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  266. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  267. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  268. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  269. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  270. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  271. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  272. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,234 @@
1
+ import { Context, Schema, Effect, Layer } from 'effect'
2
+
3
+ import { serverLogger } from '../../config/logger'
4
+ import { ensureRecordId } from '../../db/record-id'
5
+ import { TABLES } from '../../db/tables'
6
+ import { effectTryPromise } from '../../effect/helpers'
7
+ import { RedisServiceTag } from '../../effect/services'
8
+ import type { RedisConnectionManager } from '../../redis/connection'
9
+ import { withLeaseLock } from '../../redis/redis-lease-lock'
10
+ import { resolvePlanNodeExecutionVisibility } from '../../runtime/execution-plan-visibility'
11
+ import type { makeThreadService } from '../thread/thread.service'
12
+ import { ThreadServiceTag } from '../thread/thread.service'
13
+ import type { makePlanAgentQueryService } from './plan-agent-query.service'
14
+ import { PlanAgentQueryServiceTag } from './plan-agent-query.service'
15
+ import type { makePlanExecutorService } from './plan-executor.service'
16
+ import { PlanExecutorServiceTag } from './plan-executor.service'
17
+ import type { makePlanRunService } from './plan-run.service'
18
+ import { PlanRunServiceTag } from './plan-run.service'
19
+
20
+ const PLAN_AGENT_HEARTBEAT_LOCK_TTL_MS = 300_000
21
+ const PLAN_AGENT_HEARTBEAT_LOCK_REFRESH_MS = 20_000
22
+
23
+ function buildHeartbeatLockKey(runId: string, nodeId: string): string {
24
+ return `plan-agent-heartbeat:${runId}:${nodeId}`
25
+ }
26
+
27
+ function buildWakeDedupeKey(params: {
28
+ organizationId: string
29
+ threadId: string
30
+ runId: string
31
+ nodeId: string
32
+ agentId: string
33
+ }) {
34
+ return `${params.organizationId}:${params.threadId}:${params.runId}:${params.nodeId}:${params.agentId}`
35
+ }
36
+
37
+ class PlanAgentHeartbeatError extends Schema.TaggedErrorClass<PlanAgentHeartbeatError>()('PlanAgentHeartbeatError', {
38
+ operation: Schema.String,
39
+ cause: Schema.Defect,
40
+ }) {}
41
+
42
+ function tryHeartbeatPromise<A>(
43
+ operation: string,
44
+ thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
45
+ ): Effect.Effect<A, PlanAgentHeartbeatError> {
46
+ return effectTryPromise(thunk, (cause) => new PlanAgentHeartbeatError({ operation, cause }))
47
+ }
48
+
49
+ function heartbeatServiceEffect<A, E>(
50
+ operation: string,
51
+ effect: Effect.Effect<A, E>,
52
+ ): Effect.Effect<A, PlanAgentHeartbeatError> {
53
+ return effect.pipe(Effect.mapError((cause) => new PlanAgentHeartbeatError({ operation, cause })))
54
+ }
55
+
56
+ interface PlanAgentHeartbeatDeps {
57
+ redis: RedisConnectionManager
58
+ planAgentQueryService: ReturnType<typeof makePlanAgentQueryService>
59
+ planExecutorService: ReturnType<typeof makePlanExecutorService>
60
+ planRunService: ReturnType<typeof makePlanRunService>
61
+ threadService: ReturnType<typeof makeThreadService>
62
+ }
63
+
64
+ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
65
+ const { planExecutorService, planRunService, redis, planAgentQueryService, threadService } = deps
66
+
67
+ const wakeNodeEffect = (params: {
68
+ organizationId: string
69
+ threadId: string
70
+ runId: string
71
+ nodeId: string
72
+ agentId: string
73
+ reason: string
74
+ }): Effect.Effect<boolean, PlanAgentHeartbeatError> =>
75
+ Effect.gen(function* () {
76
+ const threadRef = ensureRecordId(params.threadId, TABLES.THREAD)
77
+ yield* heartbeatServiceEffect(
78
+ 'clear-stale-active-run',
79
+ threadService.clearStaleActiveRunIfMissingFromRegistry(threadRef),
80
+ )
81
+ if (yield* heartbeatServiceEffect('get-active-run-id', threadService.getActiveRunId(threadRef))) {
82
+ return false
83
+ }
84
+
85
+ return yield* withLeaseLock(
86
+ {
87
+ redis: redis.getConnection(),
88
+ lockKey: buildHeartbeatLockKey(params.runId, params.nodeId),
89
+ lockTtlMs: PLAN_AGENT_HEARTBEAT_LOCK_TTL_MS,
90
+ refreshIntervalMs: PLAN_AGENT_HEARTBEAT_LOCK_REFRESH_MS,
91
+ label: 'plan agent heartbeat',
92
+ maxWaitMs: 5_000,
93
+ logger: serverLogger,
94
+ },
95
+ () =>
96
+ Effect.gen(function* () {
97
+ yield* heartbeatServiceEffect(
98
+ 'clear-stale-active-run',
99
+ threadService.clearStaleActiveRunIfMissingFromRegistry(threadRef),
100
+ )
101
+ if (yield* heartbeatServiceEffect('has-active-run-lease', threadService.hasActiveRunLease(threadRef))) {
102
+ return false
103
+ }
104
+ if (yield* heartbeatServiceEffect('get-active-run-id', threadService.getActiveRunId(threadRef))) {
105
+ return false
106
+ }
107
+
108
+ const run = yield* heartbeatServiceEffect('get-run-by-id', planRunService.getRunById(params.runId))
109
+ const spec = yield* heartbeatServiceEffect(
110
+ 'get-plan-spec-by-id',
111
+ planRunService.getPlanSpecById(run.planSpecId),
112
+ )
113
+ const [nodeSpec, nodeRun] = yield* Effect.all([
114
+ heartbeatServiceEffect(
115
+ 'get-node-spec-by-node-id',
116
+ planRunService.getNodeSpecByNodeId(spec.id, params.nodeId),
117
+ ),
118
+ heartbeatServiceEffect(
119
+ 'get-node-run-by-node-id',
120
+ planRunService.getNodeRunByNodeId(run.id, params.nodeId),
121
+ ),
122
+ ])
123
+
124
+ if (nodeSpec.owner.executorType !== 'agent' || nodeSpec.owner.ref !== params.agentId) {
125
+ return false
126
+ }
127
+
128
+ const visibility = resolvePlanNodeExecutionVisibility(spec, nodeSpec)
129
+ if (visibility !== 'visible') {
130
+ return false
131
+ }
132
+
133
+ if (nodeRun.status !== 'running' && nodeRun.status !== 'ready') {
134
+ return false
135
+ }
136
+
137
+ if (nodeRun.status === 'ready' && run.currentNodeId === params.nodeId) {
138
+ yield* tryHeartbeatPromise('transition-node-to-running', () =>
139
+ planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: params.nodeId }),
140
+ )
141
+ }
142
+
143
+ const { triggerPlanNodeTurn } = yield* tryHeartbeatPromise(
144
+ 'import-thread-turn',
145
+ () => import('../thread/thread-turn'),
146
+ )
147
+
148
+ yield* tryHeartbeatPromise('trigger-plan-node-turn', () =>
149
+ triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId }),
150
+ )
151
+ return true
152
+ }),
153
+ ).pipe(Effect.mapError((cause) => new PlanAgentHeartbeatError({ operation: 'wake-node-lock', cause })))
154
+ })
155
+
156
+ const sweepEffect = (params?: { organizationId?: string }): Effect.Effect<void, PlanAgentHeartbeatError> =>
157
+ Effect.gen(function* () {
158
+ const { enqueuePlanAgentHeartbeatWake } = yield* tryHeartbeatPromise(
159
+ 'import-heartbeat-queue',
160
+ () => import('../../queues/plan-agent-heartbeat.queue'),
161
+ )
162
+ const [actionable, recentlyUnblocked, approachingDeadlines] = yield* Effect.all([
163
+ heartbeatServiceEffect(
164
+ 'get-actionable-nodes-for-agent',
165
+ planAgentQueryService.getActionableNodesForAgent({ organizationId: params?.organizationId }),
166
+ ),
167
+ heartbeatServiceEffect(
168
+ 'get-recently-unblocked-nodes',
169
+ planAgentQueryService.getRecentlyUnblockedNodes({ organizationId: params?.organizationId }),
170
+ ),
171
+ heartbeatServiceEffect(
172
+ 'get-approaching-deadlines',
173
+ planAgentQueryService.getApproachingDeadlines({ organizationId: params?.organizationId, withinMinutes: 60 }),
174
+ ),
175
+ ])
176
+
177
+ const wakeTargets = new Map<
178
+ string,
179
+ { organizationId: string; threadId: string; runId: string; nodeId: string; agentId: string; reason: string }
180
+ >()
181
+
182
+ for (const node of actionable) {
183
+ wakeTargets.set(buildWakeDedupeKey(node), { ...node, reason: 'heartbeat-actionable' })
184
+ }
185
+
186
+ for (const node of recentlyUnblocked) {
187
+ wakeTargets.set(buildWakeDedupeKey(node), { ...node, reason: node.sourceEventType })
188
+ }
189
+
190
+ for (const node of approachingDeadlines) {
191
+ if (!node.agentId || node.visibility !== 'visible') {
192
+ continue
193
+ }
194
+ const wakeTarget = {
195
+ organizationId: node.organizationId,
196
+ threadId: node.threadId,
197
+ runId: node.runId,
198
+ nodeId: node.nodeId,
199
+ agentId: node.agentId,
200
+ reason: `deadline-${node.status}`,
201
+ }
202
+ wakeTargets.set(buildWakeDedupeKey(wakeTarget), wakeTarget)
203
+ }
204
+
205
+ for (const target of wakeTargets.values()) {
206
+ yield* tryHeartbeatPromise('enqueue-heartbeat-wake', () => enqueuePlanAgentHeartbeatWake(target))
207
+ }
208
+ })
209
+
210
+ return { wakeNode: wakeNodeEffect, sweep: sweepEffect }
211
+ }
212
+
213
+ export class PlanAgentHeartbeatServiceTag extends Context.Service<
214
+ PlanAgentHeartbeatServiceTag,
215
+ ReturnType<typeof makePlanAgentHeartbeatService>
216
+ >()('@lota-sdk/core/PlanAgentHeartbeatService') {}
217
+
218
+ export const PlanAgentHeartbeatServiceLive = Layer.effect(
219
+ PlanAgentHeartbeatServiceTag,
220
+ Effect.gen(function* () {
221
+ const redis = yield* RedisServiceTag
222
+ const planAgentQueryService = yield* PlanAgentQueryServiceTag
223
+ const planRunService = yield* PlanRunServiceTag
224
+ const planExecutor = yield* PlanExecutorServiceTag
225
+ const threadSvc = yield* ThreadServiceTag
226
+ return makePlanAgentHeartbeatService({
227
+ redis,
228
+ planAgentQueryService,
229
+ planExecutorService: planExecutor,
230
+ planRunService,
231
+ threadService: threadSvc,
232
+ })
233
+ }),
234
+ )
@@ -0,0 +1,322 @@
1
+ import type { PlanExecutionVisibility, PlanNodeSpecRecord, PlanRunRecord, PlanSpecRecord } from '@lota-sdk/shared'
2
+ import { PlanRunSchema } from '@lota-sdk/shared'
3
+ import { Context, Schema, Effect, Layer } from 'effect'
4
+ import { BoundQuery } from 'surrealdb'
5
+
6
+ import type { RecordIdInput } from '../../db/record-id'
7
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
8
+ import type { SurrealDBService } from '../../db/service'
9
+ import { TABLES } from '../../db/tables'
10
+ import { effectTryPromise } from '../../effect/helpers'
11
+ import { DatabaseServiceTag } from '../../effect/services'
12
+ import { resolvePlanNodeExecutionVisibility } from '../../runtime/execution-plan-visibility'
13
+ import { nowDate, unsafeDateFrom } from '../../utils/date-time'
14
+ import { evaluateDeadline } from './plan-deadline.service'
15
+ import type { makePlanRunService } from './plan-run.service'
16
+ import { PlanRunServiceTag } from './plan-run.service'
17
+
18
+ const ACTIVE_PLAN_RUN_STATUSES = ['running', 'awaiting-human', 'blocked'] as const
19
+ const ACTIONABLE_NODE_STATUSES = new Set(['ready', 'running'])
20
+ const DEADLINE_TRACKED_NODE_STATUSES = new Set(['ready', 'running', 'awaiting-human'])
21
+
22
+ export interface ActionablePlanAgentNode {
23
+ organizationId: string
24
+ threadId: string
25
+ runId: string
26
+ nodeId: string
27
+ agentId: string
28
+ status: 'ready' | 'running'
29
+ visibility: PlanExecutionVisibility
30
+ }
31
+
32
+ export interface ApproachingDeadlineNode {
33
+ organizationId: string
34
+ threadId: string
35
+ runId: string
36
+ nodeId: string
37
+ agentId?: string
38
+ visibility?: PlanExecutionVisibility
39
+ dueAt?: string
40
+ status: 'warning' | 'escalated' | 'missed'
41
+ nextTriggerAt?: string | null
42
+ }
43
+
44
+ export interface RecentlyUnblockedNode {
45
+ organizationId: string
46
+ threadId: string
47
+ runId: string
48
+ nodeId: string
49
+ agentId: string
50
+ visibility: PlanExecutionVisibility
51
+ unblockedAt: string
52
+ sourceEventType: 'node-unblocked' | 'approval-resolved'
53
+ }
54
+
55
+ function isVisibleAgentNode(params: {
56
+ nodeSpec: PlanNodeSpecRecord
57
+ spec: PlanSpecRecord
58
+ }): { agentId: string; visibility: PlanExecutionVisibility } | null {
59
+ if (params.nodeSpec.owner.executorType !== 'agent') {
60
+ return null
61
+ }
62
+
63
+ const visibility = resolvePlanNodeExecutionVisibility(params.spec, params.nodeSpec)
64
+ const isVisible = visibility === 'visible'
65
+ if (!isVisible) {
66
+ return null
67
+ }
68
+
69
+ return { agentId: params.nodeSpec.owner.ref, visibility }
70
+ }
71
+
72
+ class PlanAgentQueryError extends Schema.TaggedErrorClass<PlanAgentQueryError>()('PlanAgentQueryError', {
73
+ operation: Schema.String,
74
+ cause: Schema.Defect,
75
+ }) {}
76
+
77
+ function toPlanAgentQueryError(operation: string, cause: unknown): PlanAgentQueryError {
78
+ return new PlanAgentQueryError({ operation, cause })
79
+ }
80
+
81
+ function queryEffect<A>(
82
+ operation: string,
83
+ thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
84
+ ): Effect.Effect<A, PlanAgentQueryError> {
85
+ return effectTryPromise(thunk, (cause) => toPlanAgentQueryError(operation, cause))
86
+ }
87
+
88
+ function queryServiceEffect<A, E>(
89
+ operation: string,
90
+ effect: Effect.Effect<A, E>,
91
+ ): Effect.Effect<A, PlanAgentQueryError> {
92
+ return effect.pipe(Effect.mapError((cause) => toPlanAgentQueryError(operation, cause)))
93
+ }
94
+
95
+ interface PlanAgentQueryDeps {
96
+ db: SurrealDBService
97
+ planRunService: ReturnType<typeof makePlanRunService>
98
+ }
99
+
100
+ export function makePlanAgentQueryService(deps: PlanAgentQueryDeps) {
101
+ const { db, planRunService } = deps
102
+
103
+ function listActiveRunsEffect(organizationId?: RecordIdInput): Effect.Effect<PlanRunRecord[], PlanAgentQueryError> {
104
+ const bindings = {
105
+ statuses: [...ACTIVE_PLAN_RUN_STATUSES],
106
+ ...(organizationId ? { organizationId: ensureRecordId(organizationId, TABLES.ORGANIZATION) } : {}),
107
+ }
108
+
109
+ const whereOrganization = organizationId ? ' AND organizationId = $organizationId' : ''
110
+ return queryEffect('list-active-runs', () =>
111
+ db.queryMany(
112
+ new BoundQuery(
113
+ `SELECT * FROM ${TABLES.PLAN_RUN} WHERE status INSIDE $statuses${whereOrganization} ORDER BY updatedAt DESC`,
114
+ bindings,
115
+ ),
116
+ PlanRunSchema,
117
+ ),
118
+ )
119
+ }
120
+
121
+ const getActionableNodesForAgentEffect = (params: {
122
+ agentId?: string
123
+ organizationId?: RecordIdInput
124
+ }): Effect.Effect<ActionablePlanAgentNode[], PlanAgentQueryError> =>
125
+ Effect.gen(function* () {
126
+ const runs = yield* listActiveRunsEffect(params.organizationId)
127
+ const actionable: ActionablePlanAgentNode[] = []
128
+
129
+ for (const run of runs) {
130
+ const spec = yield* queryServiceEffect('get-plan-spec-by-id', planRunService.getPlanSpecById(run.planSpecId))
131
+
132
+ if (spec.executionMode === 'graph-full') {
133
+ const [nodeSpecs, nodeRuns] = yield* Effect.all([
134
+ queryServiceEffect('list-node-specs', planRunService.listNodeSpecs(spec.id)),
135
+ queryServiceEffect('list-node-runs', planRunService.listNodeRuns(run.id)),
136
+ ])
137
+ for (const nodeRun of nodeRuns) {
138
+ if (!ACTIONABLE_NODE_STATUSES.has(nodeRun.status)) continue
139
+ const nodeSpec = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
140
+ if (!nodeSpec) continue
141
+ const visibleTarget = isVisibleAgentNode({ nodeSpec, spec })
142
+ if (!visibleTarget) continue
143
+ if (params.agentId && params.agentId !== visibleTarget.agentId) continue
144
+ actionable.push({
145
+ organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
146
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
147
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
148
+ nodeId: nodeSpec.nodeId,
149
+ agentId: visibleTarget.agentId,
150
+ status: nodeRun.status as 'ready' | 'running',
151
+ visibility: visibleTarget.visibility,
152
+ })
153
+ }
154
+ continue
155
+ }
156
+
157
+ const currentNodeId = run.currentNodeId
158
+ if (!currentNodeId) {
159
+ continue
160
+ }
161
+
162
+ const [nodeSpec, nodeRun] = yield* Effect.all([
163
+ queryServiceEffect('get-node-spec-by-node-id', planRunService.getNodeSpecByNodeId(spec.id, currentNodeId)),
164
+ queryServiceEffect('get-node-run-by-node-id', planRunService.getNodeRunByNodeId(run.id, currentNodeId)),
165
+ ])
166
+ if (!ACTIONABLE_NODE_STATUSES.has(nodeRun.status)) {
167
+ continue
168
+ }
169
+
170
+ const visibleTarget = isVisibleAgentNode({ nodeSpec, spec })
171
+ if (!visibleTarget) {
172
+ continue
173
+ }
174
+ if (params.agentId && params.agentId !== visibleTarget.agentId) {
175
+ continue
176
+ }
177
+
178
+ actionable.push({
179
+ organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
180
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
181
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
182
+ nodeId: nodeSpec.nodeId,
183
+ agentId: visibleTarget.agentId,
184
+ status: nodeRun.status as 'ready' | 'running',
185
+ visibility: visibleTarget.visibility,
186
+ })
187
+ }
188
+ return actionable
189
+ })
190
+
191
+ const getApproachingDeadlinesEffect = (params?: {
192
+ organizationId?: RecordIdInput
193
+ withinMinutes?: number
194
+ }): Effect.Effect<ApproachingDeadlineNode[], PlanAgentQueryError> =>
195
+ Effect.gen(function* () {
196
+ const now = nowDate()
197
+ const maxWindowMs = (params?.withinMinutes ?? 60) * 60_000
198
+ const runs = yield* listActiveRunsEffect(params?.organizationId)
199
+ const matches: ApproachingDeadlineNode[] = []
200
+
201
+ for (const run of runs) {
202
+ const spec = yield* queryServiceEffect('get-plan-spec-by-id', planRunService.getPlanSpecById(run.planSpecId))
203
+ const [nodeSpecs, nodeRuns] = yield* Effect.all([
204
+ queryServiceEffect('list-node-specs', planRunService.listNodeSpecs(spec.id)),
205
+ queryServiceEffect('list-node-runs', planRunService.listNodeRuns(run.id)),
206
+ ])
207
+ const nodeRunsById = new Map(nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
208
+
209
+ for (const nodeSpec of nodeSpecs) {
210
+ const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
211
+ if (!nodeRun || !nodeSpec.deadline || !DEADLINE_TRACKED_NODE_STATUSES.has(nodeRun.status)) {
212
+ continue
213
+ }
214
+
215
+ const evaluation = evaluateDeadline({
216
+ deadline: nodeSpec.deadline,
217
+ nodeStartedAt: unsafeDateFrom(nodeRun.startedAt ?? nodeRun.createdAt),
218
+ now,
219
+ })
220
+ if (evaluation.status === 'ok') {
221
+ continue
222
+ }
223
+
224
+ const nextTriggerTime = evaluation.nextTriggerAt?.getTime()
225
+ if (nextTriggerTime && nextTriggerTime - now.getTime() > maxWindowMs && evaluation.status !== 'missed') {
226
+ continue
227
+ }
228
+
229
+ const visibleTarget = isVisibleAgentNode({ nodeSpec, spec })
230
+ matches.push({
231
+ organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
232
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
233
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
234
+ nodeId: nodeSpec.nodeId,
235
+ ...(visibleTarget ? { agentId: visibleTarget.agentId, visibility: visibleTarget.visibility } : {}),
236
+ ...(nodeSpec.deadline.dueAt ? { dueAt: nodeSpec.deadline.dueAt } : {}),
237
+ status: evaluation.status,
238
+ nextTriggerAt: evaluation.nextTriggerAt?.toISOString() ?? null,
239
+ })
240
+ }
241
+ }
242
+
243
+ return matches
244
+ })
245
+
246
+ const getRecentlyUnblockedNodesEffect = (params?: {
247
+ organizationId?: RecordIdInput
248
+ sinceMinutes?: number
249
+ agentId?: string
250
+ }): Effect.Effect<RecentlyUnblockedNode[], PlanAgentQueryError> =>
251
+ Effect.gen(function* () {
252
+ const since = unsafeDateFrom(nowDate().getTime() - (params?.sinceMinutes ?? 30) * 60_000)
253
+ const runs = yield* listActiveRunsEffect(params?.organizationId)
254
+ const matches: RecentlyUnblockedNode[] = []
255
+
256
+ for (const run of runs) {
257
+ const spec = yield* queryServiceEffect('get-plan-spec-by-id', planRunService.getPlanSpecById(run.planSpecId))
258
+ const [nodeSpecs, events] = yield* Effect.all([
259
+ queryServiceEffect('list-node-specs', planRunService.listNodeSpecs(spec.id)),
260
+ queryServiceEffect('list-events', planRunService.listEvents(run.id, 200)),
261
+ ])
262
+ const nodeSpecsById = new Map(nodeSpecs.map((nodeSpec) => [nodeSpec.nodeId, nodeSpec]))
263
+
264
+ for (const event of events) {
265
+ if (
266
+ (event.eventType !== 'node-unblocked' && event.eventType !== 'approval-resolved') ||
267
+ !event.nodeId ||
268
+ unsafeDateFrom(event.createdAt).getTime() < since.getTime()
269
+ ) {
270
+ continue
271
+ }
272
+
273
+ const currentNodeId = run.currentNodeId ?? event.nodeId
274
+ const nodeSpec = nodeSpecsById.get(currentNodeId)
275
+ if (!nodeSpec) {
276
+ continue
277
+ }
278
+
279
+ const visibleTarget = isVisibleAgentNode({ nodeSpec, spec })
280
+ if (!visibleTarget) {
281
+ continue
282
+ }
283
+ if (params?.agentId && params.agentId !== visibleTarget.agentId) {
284
+ continue
285
+ }
286
+
287
+ matches.push({
288
+ organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
289
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
290
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
291
+ nodeId: currentNodeId,
292
+ agentId: visibleTarget.agentId,
293
+ visibility: visibleTarget.visibility,
294
+ unblockedAt: unsafeDateFrom(event.createdAt).toISOString(),
295
+ sourceEventType: event.eventType,
296
+ })
297
+ }
298
+ }
299
+
300
+ return matches
301
+ })
302
+
303
+ return {
304
+ getActionableNodesForAgent: getActionableNodesForAgentEffect,
305
+ getApproachingDeadlines: getApproachingDeadlinesEffect,
306
+ getRecentlyUnblockedNodes: getRecentlyUnblockedNodesEffect,
307
+ }
308
+ }
309
+
310
+ export class PlanAgentQueryServiceTag extends Context.Service<
311
+ PlanAgentQueryServiceTag,
312
+ ReturnType<typeof makePlanAgentQueryService>
313
+ >()('@lota-sdk/core/PlanAgentQueryService') {}
314
+
315
+ export const PlanAgentQueryServiceLive = Layer.effect(
316
+ PlanAgentQueryServiceTag,
317
+ Effect.gen(function* () {
318
+ const db = yield* DatabaseServiceTag
319
+ const planRunService = yield* PlanRunServiceTag
320
+ return makePlanAgentQueryService({ db, planRunService })
321
+ }),
322
+ )
@@ -1,24 +1,27 @@
1
1
  import { PlanApprovalSchema } from '@lota-sdk/shared'
2
2
  import type { PlanApprovalRecord, PlanApprovalStatus } from '@lota-sdk/shared'
3
+ import { Context, Effect, Layer } from 'effect'
3
4
  import { RecordId } from 'surrealdb'
4
5
 
5
- import type { RecordIdInput } from '../db/record-id'
6
- import { ensureRecordId } from '../db/record-id'
7
- import { databaseService } from '../db/service'
8
- import type { DatabaseTransaction } from '../db/service'
9
- import { TABLES } from '../db/tables'
6
+ import type { RecordIdInput } from '../../db/record-id'
7
+ import { ensureRecordId } from '../../db/record-id'
8
+ import type { DatabaseTransaction, SurrealDBService } from '../../db/service'
9
+ import { TABLES } from '../../db/tables'
10
+ import { DatabaseServiceTag } from '../../effect/services'
11
+ import { nowDate } from '../../utils/date-time'
12
+ import type { HumanNodeResponsePayload } from './plan-executor-helpers'
10
13
 
11
- class PlanApprovalService {
12
- async createPendingApproval(params: {
14
+ export function makePlanApprovalService(db: SurrealDBService) {
15
+ const createPendingApprovalEffect = Effect.fn('PlanApproval.createPendingApproval')(function* (params: {
13
16
  tx: DatabaseTransaction
14
17
  runId: RecordIdInput
15
18
  nodeRunId: RecordIdInput
16
19
  nodeId: string
17
20
  requestedBy: string
18
21
  presented: Record<string, unknown>
19
- }): Promise<PlanApprovalRecord> {
22
+ }) {
20
23
  const approvalId = new RecordId(TABLES.PLAN_APPROVAL, Bun.randomUUIDv7())
21
- const created = await params.tx
24
+ const created = yield* params.tx
22
25
  .create(approvalId)
23
26
  .content({
24
27
  runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
@@ -32,18 +35,20 @@ class PlanApprovalService {
32
35
  .output('after')
33
36
 
34
37
  return PlanApprovalSchema.parse(created)
35
- }
38
+ })
36
39
 
37
- async getApprovalById(approvalId: RecordIdInput): Promise<PlanApprovalRecord | null> {
38
- return databaseService.findOne(
40
+ const getApprovalByIdEffect = Effect.fn('PlanApproval.getApprovalById')(function* (approvalId: RecordIdInput) {
41
+ return yield* db.findOne(
39
42
  TABLES.PLAN_APPROVAL,
40
43
  { id: ensureRecordId(approvalId, TABLES.PLAN_APPROVAL) },
41
44
  PlanApprovalSchema,
42
45
  )
43
- }
46
+ })
44
47
 
45
- async getPendingApprovalForNodeRun(nodeRunId: RecordIdInput): Promise<PlanApprovalRecord | null> {
46
- const approvals = await databaseService.findMany(
48
+ const getPendingApprovalForNodeRunEffect = Effect.fn('PlanApproval.getPendingApprovalForNodeRun')(function* (
49
+ nodeRunId: RecordIdInput,
50
+ ) {
51
+ const approvals = yield* db.findMany(
47
52
  TABLES.PLAN_APPROVAL,
48
53
  { nodeRunId: ensureRecordId(nodeRunId, TABLES.PLAN_NODE_RUN), status: 'pending' },
49
54
  PlanApprovalSchema,
@@ -51,19 +56,19 @@ class PlanApprovalService {
51
56
  )
52
57
 
53
58
  return approvals.at(0) ?? null
54
- }
59
+ })
55
60
 
56
- async updateApprovalResponse(params: {
61
+ const updateApprovalResponseEffect = Effect.fn('PlanApproval.updateApprovalResponse')(function* (params: {
57
62
  tx: DatabaseTransaction
58
63
  approval: PlanApprovalRecord
59
64
  status: PlanApprovalStatus
60
- response: Record<string, unknown>
65
+ response: HumanNodeResponsePayload
61
66
  respondedBy: string
62
67
  approvalMessageId?: string
63
68
  comments?: string
64
69
  requiredEdits?: string[]
65
- }): Promise<PlanApprovalRecord> {
66
- const updated = await params.tx
70
+ }) {
71
+ const updated = yield* params.tx
67
72
  .update(ensureRecordId(params.approval.id, TABLES.PLAN_APPROVAL))
68
73
  .merge({
69
74
  status: params.status,
@@ -72,12 +77,30 @@ class PlanApprovalService {
72
77
  ...(params.approvalMessageId ? { approvalMessageId: params.approvalMessageId } : {}),
73
78
  ...(params.comments ? { comments: params.comments } : {}),
74
79
  ...(params.requiredEdits ? { requiredEdits: params.requiredEdits } : {}),
75
- respondedAt: new Date(),
80
+ respondedAt: nowDate(),
76
81
  })
77
82
  .output('after')
78
83
 
79
84
  return PlanApprovalSchema.parse(updated)
85
+ })
86
+
87
+ return {
88
+ createPendingApproval: createPendingApprovalEffect,
89
+ getApprovalById: getApprovalByIdEffect,
90
+ getPendingApprovalForNodeRun: getPendingApprovalForNodeRunEffect,
91
+ updateApprovalResponse: updateApprovalResponseEffect,
80
92
  }
81
93
  }
82
94
 
83
- export const planApprovalService = new PlanApprovalService()
95
+ export class PlanApprovalServiceTag extends Context.Service<
96
+ PlanApprovalServiceTag,
97
+ ReturnType<typeof makePlanApprovalService>
98
+ >()('@lota-sdk/core/PlanApprovalService') {}
99
+
100
+ export const PlanApprovalServiceLive = Layer.effect(
101
+ PlanApprovalServiceTag,
102
+ Effect.gen(function* () {
103
+ const db = yield* DatabaseServiceTag
104
+ return makePlanApprovalService(db)
105
+ }),
106
+ )