@lota-sdk/core 0.4.8 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/thread-defaults.ts +33 -21
  9. package/src/create-runtime.ts +725 -387
  10. package/src/db/base.service.ts +52 -28
  11. package/src/db/cursor-pagination.ts +71 -30
  12. package/src/db/memory-store.helpers.ts +4 -7
  13. package/src/db/memory-store.ts +856 -598
  14. package/src/db/memory.ts +398 -275
  15. package/src/db/record-id.ts +32 -10
  16. package/src/db/schema-fingerprint.ts +30 -12
  17. package/src/db/service-normalization.ts +255 -0
  18. package/src/db/service.ts +726 -761
  19. package/src/db/startup.ts +140 -66
  20. package/src/db/transaction-conflict.ts +15 -0
  21. package/src/effect/awaitable-effect.ts +87 -0
  22. package/src/effect/errors.ts +121 -0
  23. package/src/effect/helpers.ts +98 -0
  24. package/src/effect/index.ts +22 -0
  25. package/src/effect/layers.ts +228 -0
  26. package/src/effect/runtime-ref.ts +25 -0
  27. package/src/effect/runtime.ts +31 -0
  28. package/src/effect/services.ts +57 -0
  29. package/src/effect/zod.ts +43 -0
  30. package/src/embeddings/provider.ts +122 -76
  31. package/src/index.ts +46 -1
  32. package/src/openrouter/direct-provider.ts +11 -35
  33. package/src/queues/autonomous-job.queue.ts +130 -74
  34. package/src/queues/context-compaction.queue.ts +60 -15
  35. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  36. package/src/queues/document-processor.queue.ts +52 -77
  37. package/src/queues/memory-consolidation.queue.ts +47 -32
  38. package/src/queues/organization-learning.queue.ts +13 -4
  39. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  40. package/src/queues/plan-scheduler.queue.ts +107 -31
  41. package/src/queues/post-chat-memory.queue.ts +66 -24
  42. package/src/queues/queue-factory.ts +142 -52
  43. package/src/queues/standalone-worker.ts +39 -0
  44. package/src/queues/title-generation.queue.ts +54 -9
  45. package/src/redis/connection.ts +84 -32
  46. package/src/redis/index.ts +6 -8
  47. package/src/redis/org-memory-lock.ts +60 -27
  48. package/src/redis/redis-lease-lock.ts +200 -121
  49. package/src/redis/runtime-connection.ts +10 -0
  50. package/src/redis/stream-context.ts +84 -46
  51. package/src/runtime/agent-identity-overrides.ts +2 -2
  52. package/src/runtime/agent-runtime-policy.ts +4 -1
  53. package/src/runtime/agent-stream-helpers.ts +20 -9
  54. package/src/runtime/chat-run-orchestration.ts +102 -19
  55. package/src/runtime/chat-run-registry.ts +36 -2
  56. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  57. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  58. package/src/runtime/execution-plan-visibility.ts +2 -2
  59. package/src/runtime/execution-plan.ts +42 -15
  60. package/src/runtime/graph-designer.ts +11 -7
  61. package/src/runtime/helper-model.ts +135 -48
  62. package/src/runtime/index.ts +7 -7
  63. package/src/runtime/indexed-repositories-policy.ts +3 -3
  64. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  65. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  66. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  67. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  68. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  69. package/src/runtime/plugin-resolution.ts +144 -24
  70. package/src/runtime/plugin-types.ts +9 -1
  71. package/src/runtime/post-turn-side-effects.ts +197 -130
  72. package/src/runtime/retrieval-adapters.ts +38 -4
  73. package/src/runtime/runtime-config.ts +150 -61
  74. package/src/runtime/runtime-extensions.ts +21 -34
  75. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  76. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  77. package/src/runtime/social-chat/social-chat.ts +594 -0
  78. package/src/runtime/specialist-runner.ts +36 -10
  79. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  80. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  81. package/src/runtime/thread-chat-helpers.ts +2 -2
  82. package/src/runtime/thread-plan-turn.ts +2 -1
  83. package/src/runtime/thread-turn-context.ts +172 -94
  84. package/src/runtime/turn-lifecycle.ts +93 -27
  85. package/src/services/agent-activity.service.ts +287 -203
  86. package/src/services/agent-executor.service.ts +329 -217
  87. package/src/services/artifact.service.ts +225 -148
  88. package/src/services/attachment.service.ts +137 -115
  89. package/src/services/autonomous-job.service.ts +888 -491
  90. package/src/services/chat-run-registry.service.ts +11 -1
  91. package/src/services/context-compaction.service.ts +136 -86
  92. package/src/services/document-chunk.service.ts +162 -90
  93. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  94. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  95. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  96. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  97. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  98. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  99. package/src/services/feedback-loop.service.ts +132 -76
  100. package/src/services/global-orchestrator.service.ts +80 -170
  101. package/src/services/graph-full-routing.ts +182 -0
  102. package/src/services/index.ts +18 -21
  103. package/src/services/institutional-memory.service.ts +220 -123
  104. package/src/services/learned-skill.service.ts +364 -259
  105. package/src/services/memory/memory-conversation.ts +95 -0
  106. package/src/services/memory/memory-org-memory.ts +39 -0
  107. package/src/services/memory/memory-preseeded.ts +80 -0
  108. package/src/services/memory/memory-rerank.ts +297 -0
  109. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  110. package/src/services/memory/memory.service.ts +692 -0
  111. package/src/services/memory/rerank.service.ts +209 -0
  112. package/src/services/monitoring-window.service.ts +92 -70
  113. package/src/services/mutating-approval.service.ts +62 -53
  114. package/src/services/node-workspace.service.ts +141 -98
  115. package/src/services/notification.service.ts +17 -16
  116. package/src/services/organization-member.service.ts +120 -66
  117. package/src/services/organization.service.ts +144 -51
  118. package/src/services/ownership-dispatcher.service.ts +415 -264
  119. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  120. package/src/services/plan/plan-agent-query.service.ts +322 -0
  121. package/src/services/plan/plan-approval.service.ts +102 -0
  122. package/src/services/plan/plan-artifact.service.ts +60 -0
  123. package/src/services/plan/plan-builder.service.ts +76 -0
  124. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  125. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  126. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  127. package/src/services/plan/plan-coordination.service.ts +181 -0
  128. package/src/services/plan/plan-cycle.service.ts +398 -0
  129. package/src/services/plan/plan-deadline.service.ts +547 -0
  130. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  131. package/src/services/plan/plan-executor-context.ts +35 -0
  132. package/src/services/plan/plan-executor-graph.ts +475 -0
  133. package/src/services/plan/plan-executor-helpers.ts +322 -0
  134. package/src/services/plan/plan-executor-persistence.ts +209 -0
  135. package/src/services/plan/plan-executor.service.ts +1654 -0
  136. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  137. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  138. package/src/services/plan/plan-run-serialization.ts +15 -0
  139. package/src/services/plan/plan-run.service.ts +644 -0
  140. package/src/services/plan/plan-scheduler.service.ts +385 -0
  141. package/src/services/plan/plan-template.service.ts +224 -0
  142. package/src/services/plan/plan-transaction-events.ts +33 -0
  143. package/src/services/plan/plan-validator.service.ts +907 -0
  144. package/src/services/plan/plan-workspace.service.ts +125 -0
  145. package/src/services/plugin-executor.service.ts +97 -68
  146. package/src/services/quality-metrics.service.ts +112 -94
  147. package/src/services/queue-job.service.ts +296 -230
  148. package/src/services/recent-activity-title.service.ts +65 -36
  149. package/src/services/recent-activity.service.ts +274 -259
  150. package/src/services/skill-resolver.service.ts +38 -12
  151. package/src/services/social-chat-history.service.ts +176 -125
  152. package/src/services/system-executor.service.ts +91 -61
  153. package/src/services/thread/thread-active-run.ts +203 -0
  154. package/src/services/thread/thread-bootstrap.ts +369 -0
  155. package/src/services/thread/thread-listing.ts +198 -0
  156. package/src/services/thread/thread-memory-block.ts +117 -0
  157. package/src/services/thread/thread-message.service.ts +363 -0
  158. package/src/services/thread/thread-record-store.ts +155 -0
  159. package/src/services/thread/thread-title.service.ts +74 -0
  160. package/src/services/thread/thread-turn-execution.ts +280 -0
  161. package/src/services/thread/thread-turn-message-context.ts +73 -0
  162. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  163. package/src/services/thread/thread-turn-streaming.ts +402 -0
  164. package/src/services/thread/thread-turn-tracing.ts +35 -0
  165. package/src/services/thread/thread-turn.ts +343 -0
  166. package/src/services/thread/thread.service.ts +335 -0
  167. package/src/services/user.service.ts +82 -32
  168. package/src/services/write-intent-validator.service.ts +63 -51
  169. package/src/storage/attachment-parser.ts +69 -27
  170. package/src/storage/attachment-storage.service.ts +331 -275
  171. package/src/storage/generated-document-storage.service.ts +66 -34
  172. package/src/system-agents/agent-result.ts +3 -1
  173. package/src/system-agents/context-compaction.agent.ts +2 -2
  174. package/src/system-agents/delegated-agent-factory.ts +159 -90
  175. package/src/system-agents/memory-reranker.agent.ts +2 -2
  176. package/src/system-agents/memory.agent.ts +2 -2
  177. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  178. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  179. package/src/system-agents/skill-extractor.agent.ts +2 -2
  180. package/src/system-agents/skill-manager.agent.ts +2 -2
  181. package/src/system-agents/thread-router.agent.ts +157 -113
  182. package/src/system-agents/title-generator.agent.ts +2 -2
  183. package/src/tools/execution-plan.tool.ts +220 -161
  184. package/src/tools/fetch-webpage.tool.ts +21 -17
  185. package/src/tools/firecrawl-client.ts +16 -6
  186. package/src/tools/index.ts +1 -0
  187. package/src/tools/memory-block.tool.ts +14 -6
  188. package/src/tools/plan-approval.tool.ts +49 -47
  189. package/src/tools/read-file-parts.tool.ts +44 -33
  190. package/src/tools/remember-memory.tool.ts +65 -45
  191. package/src/tools/search-web.tool.ts +26 -22
  192. package/src/tools/search.tool.ts +41 -29
  193. package/src/tools/team-think.tool.ts +124 -83
  194. package/src/tools/user-questions.tool.ts +4 -3
  195. package/src/tools/web-tool-shared.ts +6 -0
  196. package/src/utils/async.ts +17 -23
  197. package/src/utils/crypto.ts +21 -0
  198. package/src/utils/date-time.ts +40 -1
  199. package/src/utils/errors.ts +95 -16
  200. package/src/utils/hono-error-handler.ts +24 -39
  201. package/src/utils/index.ts +2 -1
  202. package/src/utils/null-proto-record.ts +41 -0
  203. package/src/utils/sse-keepalive.ts +124 -21
  204. package/src/workers/bootstrap.ts +186 -51
  205. package/src/workers/memory-consolidation.worker.ts +325 -237
  206. package/src/workers/organization-learning.worker.ts +50 -16
  207. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  208. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  209. package/src/workers/skill-extraction.runner.ts +176 -93
  210. package/src/workers/utils/file-section-chunker.ts +8 -10
  211. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  212. package/src/workers/utils/repomix-file-sections.ts +2 -2
  213. package/src/workers/utils/thread-message-query.ts +97 -38
  214. package/src/workers/worker-utils.ts +56 -31
  215. package/src/config/debug-logger.ts +0 -47
  216. package/src/redis/connection-accessor.ts +0 -26
  217. package/src/runtime/context-compaction-runtime.ts +0 -87
  218. package/src/runtime/social-chat-agent-runner.ts +0 -118
  219. package/src/runtime/social-chat.ts +0 -516
  220. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  221. package/src/services/adaptive-playbook.service.ts +0 -152
  222. package/src/services/artifact-provenance.service.ts +0 -172
  223. package/src/services/chat-attachments.service.ts +0 -17
  224. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  225. package/src/services/execution-plan.service.ts +0 -1118
  226. package/src/services/memory.service.ts +0 -914
  227. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  228. package/src/services/plan-agent-query.service.ts +0 -267
  229. package/src/services/plan-approval.service.ts +0 -83
  230. package/src/services/plan-artifact.service.ts +0 -50
  231. package/src/services/plan-builder.service.ts +0 -67
  232. package/src/services/plan-checkpoint.service.ts +0 -81
  233. package/src/services/plan-completion-side-effects.ts +0 -80
  234. package/src/services/plan-coordination.service.ts +0 -157
  235. package/src/services/plan-cycle.service.ts +0 -284
  236. package/src/services/plan-deadline.service.ts +0 -430
  237. package/src/services/plan-event-delivery.service.ts +0 -166
  238. package/src/services/plan-executor.service.ts +0 -1950
  239. package/src/services/plan-run.service.ts +0 -515
  240. package/src/services/plan-scheduler.service.ts +0 -240
  241. package/src/services/plan-template.service.ts +0 -177
  242. package/src/services/plan-validator.service.ts +0 -818
  243. package/src/services/plan-workspace.service.ts +0 -83
  244. package/src/services/rerank.service.ts +0 -156
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /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
+ >()('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
+ >()('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
+ )
@@ -0,0 +1,102 @@
1
+ import { PlanApprovalSchema } from '@lota-sdk/shared'
2
+ import type { PlanApprovalRecord, PlanApprovalStatus } from '@lota-sdk/shared'
3
+ import { Context, Effect, Layer } from 'effect'
4
+ import { RecordId } from 'surrealdb'
5
+
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'
13
+
14
+ export function makePlanApprovalService(db: SurrealDBService) {
15
+ const createPendingApprovalEffect = (params: {
16
+ tx: DatabaseTransaction
17
+ runId: RecordIdInput
18
+ nodeRunId: RecordIdInput
19
+ nodeId: string
20
+ requestedBy: string
21
+ presented: Record<string, unknown>
22
+ }) =>
23
+ Effect.gen(function* () {
24
+ const approvalId = new RecordId(TABLES.PLAN_APPROVAL, Bun.randomUUIDv7())
25
+ const created = yield* params.tx
26
+ .create(approvalId)
27
+ .content({
28
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
29
+ nodeRunId: ensureRecordId(params.nodeRunId, TABLES.PLAN_NODE_RUN),
30
+ nodeId: params.nodeId,
31
+ status: 'pending',
32
+ requestedBy: params.requestedBy,
33
+ presented: params.presented,
34
+ requiredEdits: [],
35
+ })
36
+ .output('after')
37
+
38
+ return PlanApprovalSchema.parse(created)
39
+ })
40
+
41
+ const getApprovalByIdEffect = (approvalId: RecordIdInput) =>
42
+ db.findOne(TABLES.PLAN_APPROVAL, { id: ensureRecordId(approvalId, TABLES.PLAN_APPROVAL) }, PlanApprovalSchema)
43
+
44
+ const getPendingApprovalForNodeRunEffect = (nodeRunId: RecordIdInput) =>
45
+ Effect.gen(function* () {
46
+ const approvals = yield* db.findMany(
47
+ TABLES.PLAN_APPROVAL,
48
+ { nodeRunId: ensureRecordId(nodeRunId, TABLES.PLAN_NODE_RUN), status: 'pending' },
49
+ PlanApprovalSchema,
50
+ { orderBy: 'createdAt', orderDir: 'DESC', limit: 1 },
51
+ )
52
+
53
+ return approvals.at(0) ?? null
54
+ })
55
+
56
+ const updateApprovalResponseEffect = (params: {
57
+ tx: DatabaseTransaction
58
+ approval: PlanApprovalRecord
59
+ status: PlanApprovalStatus
60
+ response: HumanNodeResponsePayload
61
+ respondedBy: string
62
+ approvalMessageId?: string
63
+ comments?: string
64
+ requiredEdits?: string[]
65
+ }) =>
66
+ Effect.gen(function* () {
67
+ const updated = yield* params.tx
68
+ .update(ensureRecordId(params.approval.id, TABLES.PLAN_APPROVAL))
69
+ .merge({
70
+ status: params.status,
71
+ response: params.response,
72
+ respondedBy: params.respondedBy,
73
+ ...(params.approvalMessageId ? { approvalMessageId: params.approvalMessageId } : {}),
74
+ ...(params.comments ? { comments: params.comments } : {}),
75
+ ...(params.requiredEdits ? { requiredEdits: params.requiredEdits } : {}),
76
+ respondedAt: nowDate(),
77
+ })
78
+ .output('after')
79
+
80
+ return PlanApprovalSchema.parse(updated)
81
+ })
82
+
83
+ return {
84
+ createPendingApproval: createPendingApprovalEffect,
85
+ getApprovalById: getApprovalByIdEffect,
86
+ getPendingApprovalForNodeRun: getPendingApprovalForNodeRunEffect,
87
+ updateApprovalResponse: updateApprovalResponseEffect,
88
+ }
89
+ }
90
+
91
+ export class PlanApprovalServiceTag extends Context.Service<
92
+ PlanApprovalServiceTag,
93
+ ReturnType<typeof makePlanApprovalService>
94
+ >()('PlanApprovalService') {}
95
+
96
+ export const PlanApprovalServiceLive = Layer.effect(
97
+ PlanApprovalServiceTag,
98
+ Effect.gen(function* () {
99
+ const db = yield* DatabaseServiceTag
100
+ return makePlanApprovalService(db)
101
+ }),
102
+ )