@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,379 @@
1
+ import { PlanCycleRecordSchema, PlanScheduleRecordSchema } from '@lota-sdk/shared'
2
+ import type { PlanScheduleRecord, PlanScheduleSpec } from '@lota-sdk/shared'
3
+ import { Context, Cron, Schema, Effect, Layer, Result } 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 { NotFoundError } from '../../effect/errors'
11
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
12
+ import { DatabaseServiceTag } from '../../effect/services'
13
+ import { nowDate, nowEpochMillis, toDatabaseDateTime, unsafeDateFrom } from '../../utils/date-time'
14
+
15
+ interface PlanSchedulerRuntimeDeps {
16
+ promoteDelayedNode(params: { runId: string; nodeId: string; emittedBy: string }): Promise<void>
17
+ advanceCycle(cycleId: RecordIdInput): Promise<void>
18
+ recoverDeadlineChecks(): Promise<void>
19
+ }
20
+
21
+ class PlanSchedulerError extends Schema.TaggedErrorClass<PlanSchedulerError>()('PlanSchedulerError', {
22
+ message: Schema.String,
23
+ cause: Schema.optional(Schema.Defect),
24
+ }) {}
25
+
26
+ const effectTryPromise = makeEffectTryPromiseWithMessage((message, cause) => new PlanSchedulerError({ message, cause }))
27
+
28
+ function failPlanScheduler(message: string, cause?: unknown): Effect.Effect<never, PlanSchedulerError> {
29
+ return Effect.fail(new PlanSchedulerError({ message, ...(cause === undefined ? {} : { cause }) }))
30
+ }
31
+
32
+ function requireScheduleField<A>(value: A | undefined, message: string): Effect.Effect<A, PlanSchedulerError> {
33
+ return value === undefined ? failPlanScheduler(message) : Effect.succeed(value)
34
+ }
35
+
36
+ const computeNextCronDateEffect: (cronExpression: string, baseTime: Date) => Effect.Effect<Date, PlanSchedulerError> =
37
+ Effect.fn('PlanScheduler.computeNextCronDate')(function* (cronExpression: string, baseTime: Date) {
38
+ const parsedCron = Cron.parse(cronExpression)
39
+ const cron = Result.isSuccess(parsedCron)
40
+ ? parsedCron.success
41
+ : yield* failPlanScheduler(`Invalid cron expression: "${cronExpression}".`)
42
+
43
+ return yield* Effect.try({
44
+ try: () => Cron.next(cron, baseTime),
45
+ catch: (cause) =>
46
+ new PlanSchedulerError({
47
+ message: `Failed to compute the next fire time for cron expression "${cronExpression}".`,
48
+ cause,
49
+ }),
50
+ })
51
+ })
52
+
53
+ const computeNextFireAtEffect: (spec: PlanScheduleSpec, baseTime?: Date) => Effect.Effect<Date, PlanSchedulerError> =
54
+ Effect.fn('PlanScheduler.computeNextFireAt')(function* (spec: PlanScheduleSpec, baseTime: Date = nowDate()) {
55
+ switch (spec.type) {
56
+ case 'immediate':
57
+ return baseTime
58
+
59
+ case 'absolute': {
60
+ const at = yield* requireScheduleField(spec.at, 'Absolute schedules require an "at" timestamp.')
61
+ return unsafeDateFrom(at)
62
+ }
63
+
64
+ case 'relative': {
65
+ const delayMs = yield* requireScheduleField(spec.delayMs, 'Relative schedules require "delayMs".')
66
+ return unsafeDateFrom(baseTime.getTime() + delayMs)
67
+ }
68
+
69
+ case 'cron': {
70
+ const cronExpression = yield* requireScheduleField(spec.cron, 'Cron schedules require a "cron" expression.')
71
+ return yield* computeNextCronDateEffect(cronExpression, baseTime)
72
+ }
73
+
74
+ case 'monitoring': {
75
+ const intervalMs = yield* requireScheduleField(spec.intervalMs, 'Monitoring schedules require "intervalMs".')
76
+ return unsafeDateFrom(baseTime.getTime() + intervalMs)
77
+ }
78
+
79
+ default:
80
+ return yield* failPlanScheduler('Unsupported schedule type in schedule specification.')
81
+ }
82
+ })
83
+
84
+ export function makePlanSchedulerService(db: SurrealDBService) {
85
+ const loadPlanSchedulerQueue = () =>
86
+ effectTryPromise(() => import('../../queues/plan-scheduler.queue'), 'Failed to load plan scheduler queue module.')
87
+
88
+ const createScheduleEffect = (params: {
89
+ organizationId: RecordIdInput
90
+ threadId: RecordIdInput
91
+ planSpecId?: RecordIdInput
92
+ runId?: RecordIdInput
93
+ nodeId?: string
94
+ scheduleSpec: PlanScheduleSpec
95
+ }): Effect.Effect<PlanScheduleRecord, PlanSchedulerError> =>
96
+ Effect.gen(function* () {
97
+ const nextFireAt = yield* computeNextFireAtEffect(params.scheduleSpec)
98
+ const now = nowDate()
99
+ const record = yield* effectTryPromise(
100
+ () =>
101
+ db.create(
102
+ TABLES.PLAN_SCHEDULE,
103
+ {
104
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
105
+ threadId: ensureRecordId(params.threadId, TABLES.THREAD),
106
+ planSpecId: params.planSpecId ? ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC) : undefined,
107
+ runId: params.runId ? ensureRecordId(params.runId, TABLES.PLAN_RUN) : undefined,
108
+ nodeId: params.nodeId,
109
+ scheduleSpec: params.scheduleSpec,
110
+ status: 'active',
111
+ fireCount: 0,
112
+ nextFireAt: toDatabaseDateTime(nextFireAt),
113
+ createdAt: now,
114
+ },
115
+ PlanScheduleRecordSchema,
116
+ ),
117
+ 'Failed to create plan schedule.',
118
+ )
119
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
120
+ yield* effectTryPromise(
121
+ () =>
122
+ enqueueScheduleFire(
123
+ recordIdToString(record.id, TABLES.PLAN_SCHEDULE),
124
+ Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
125
+ ),
126
+ 'Failed to enqueue schedule fire job.',
127
+ )
128
+ return record
129
+ })
130
+
131
+ const fireScheduleEffect = (
132
+ schedule: PlanScheduleRecord,
133
+ runtimeDeps: PlanSchedulerRuntimeDeps,
134
+ ): Effect.Effect<void, PlanSchedulerError> =>
135
+ Effect.gen(function* () {
136
+ const now = nowDate()
137
+ const newFireCount = schedule.fireCount + 1
138
+ const isRecurring = schedule.scheduleSpec.type === 'cron' || schedule.scheduleSpec.type === 'monitoring'
139
+ const maxReached = schedule.scheduleSpec.maxFires !== undefined && newFireCount >= schedule.scheduleSpec.maxFires
140
+
141
+ const isActive = isRecurring && !maxReached
142
+ const nextFireAt: Date | null = isActive ? yield* computeNextFireAtEffect(schedule.scheduleSpec, now) : null
143
+ const newStatus: 'active' | 'completed' = isActive ? 'active' : 'completed'
144
+
145
+ yield* effectTryPromise(
146
+ () =>
147
+ db.update(
148
+ TABLES.PLAN_SCHEDULE,
149
+ schedule.id,
150
+ {
151
+ fireCount: newFireCount,
152
+ lastFiredAt: now,
153
+ nextFireAt: toDatabaseDateTime(nextFireAt),
154
+ status: newStatus,
155
+ },
156
+ PlanScheduleRecordSchema,
157
+ ),
158
+ 'Failed to update fired schedule.',
159
+ )
160
+
161
+ if (newStatus === 'active') {
162
+ if (!nextFireAt) {
163
+ return yield* new PlanSchedulerError({ message: 'Recurring schedules must resolve a next fire time.' })
164
+ }
165
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
166
+ yield* effectTryPromise(
167
+ () =>
168
+ enqueueScheduleFire(
169
+ recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
170
+ Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
171
+ ),
172
+ 'Failed to enqueue next schedule fire job.',
173
+ )
174
+ }
175
+
176
+ const runId = schedule.runId
177
+ const nodeId = schedule.nodeId
178
+ const promotedNode = Boolean(runId && nodeId)
179
+ if (runId && nodeId) {
180
+ yield* effectTryPromise(
181
+ () =>
182
+ runtimeDeps.promoteDelayedNode({
183
+ runId: recordIdToString(runId, TABLES.PLAN_RUN),
184
+ nodeId,
185
+ emittedBy: 'plan-scheduler',
186
+ }),
187
+ 'Failed to promote delayed plan node.',
188
+ )
189
+ }
190
+
191
+ const advancedCycle =
192
+ schedule.planSpecId && !schedule.runId
193
+ ? yield* effectTryPromise(
194
+ () =>
195
+ db.findOne(
196
+ TABLES.PLAN_CYCLE,
197
+ { scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
198
+ PlanCycleRecordSchema,
199
+ ),
200
+ 'Failed to load plan cycle for schedule.',
201
+ ).pipe(
202
+ Effect.tap((cycle) =>
203
+ cycle
204
+ ? effectTryPromise(() => runtimeDeps.advanceCycle(cycle.id), 'Failed to advance plan cycle.')
205
+ : Effect.void,
206
+ ),
207
+ Effect.map((cycle) => cycle !== null),
208
+ )
209
+ : false
210
+
211
+ if (promotedNode || advancedCycle) {
212
+ yield* effectTryPromise(() => runtimeDeps.recoverDeadlineChecks(), 'Failed to recover deadline checks.')
213
+ }
214
+ })
215
+
216
+ const fireScheduleByIdEffect = (scheduleId: string, runtimeDeps: PlanSchedulerRuntimeDeps) =>
217
+ Effect.gen(function* () {
218
+ const schedule = yield* effectTryPromise(
219
+ () =>
220
+ db.findOne(
221
+ TABLES.PLAN_SCHEDULE,
222
+ { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
223
+ PlanScheduleRecordSchema,
224
+ ),
225
+ 'Failed to load schedule by id.',
226
+ )
227
+ if (!schedule || schedule.status !== 'active') return
228
+
229
+ yield* fireScheduleEffect(schedule, runtimeDeps)
230
+ })
231
+
232
+ const recoverActiveSchedulesEffect = () =>
233
+ Effect.gen(function* () {
234
+ const activeSchedules = yield* effectTryPromise(
235
+ () =>
236
+ db.queryMany(
237
+ new BoundQuery(`SELECT * FROM ${TABLES.PLAN_SCHEDULE} WHERE status = $status ORDER BY nextFireAt ASC`, {
238
+ status: 'active',
239
+ }),
240
+ PlanScheduleRecordSchema,
241
+ ),
242
+ 'Failed to load active schedules.',
243
+ )
244
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
245
+ yield* Effect.forEach(
246
+ activeSchedules.filter(
247
+ (schedule): schedule is typeof schedule & { nextFireAt: Date } => schedule.nextFireAt !== undefined,
248
+ ),
249
+ (schedule) =>
250
+ effectTryPromise(
251
+ () =>
252
+ enqueueScheduleFire(
253
+ recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
254
+ Math.max(0, unsafeDateFrom(schedule.nextFireAt).getTime() - nowEpochMillis()),
255
+ ),
256
+ `Failed to re-enqueue schedule ${recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE)}.`,
257
+ ),
258
+ { concurrency: 5, discard: true },
259
+ )
260
+ })
261
+
262
+ const cancelScheduleEffect = (scheduleId: RecordIdInput) => {
263
+ const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
264
+ return Effect.gen(function* () {
265
+ const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
266
+ yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
267
+ yield* effectTryPromise(
268
+ () =>
269
+ db.update(
270
+ TABLES.PLAN_SCHEDULE,
271
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
272
+ { status: 'cancelled' },
273
+ PlanScheduleRecordSchema,
274
+ ),
275
+ 'Failed to cancel schedule.',
276
+ )
277
+ })
278
+ }
279
+
280
+ const pauseScheduleEffect = (scheduleId: RecordIdInput) => {
281
+ const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
282
+ return Effect.gen(function* () {
283
+ const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
284
+ yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
285
+ yield* effectTryPromise(
286
+ () =>
287
+ db.update(
288
+ TABLES.PLAN_SCHEDULE,
289
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
290
+ { status: 'paused' },
291
+ PlanScheduleRecordSchema,
292
+ ),
293
+ 'Failed to pause schedule.',
294
+ )
295
+ })
296
+ }
297
+
298
+ const resumeScheduleEffect = (scheduleId: RecordIdInput) =>
299
+ Effect.gen(function* () {
300
+ const schedule = yield* effectTryPromise(
301
+ () =>
302
+ db.findOne(
303
+ TABLES.PLAN_SCHEDULE,
304
+ { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
305
+ PlanScheduleRecordSchema,
306
+ ),
307
+ 'Failed to load schedule for resume.',
308
+ )
309
+ if (!schedule) {
310
+ return yield* new NotFoundError({
311
+ resource: TABLES.PLAN_SCHEDULE,
312
+ id: recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE),
313
+ message: `Schedule not found: ${recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)}`,
314
+ })
315
+ }
316
+
317
+ const nextFireAt = yield* computeNextFireAtEffect(schedule.scheduleSpec)
318
+ yield* effectTryPromise(
319
+ () =>
320
+ db.update(
321
+ TABLES.PLAN_SCHEDULE,
322
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
323
+ { status: 'active', nextFireAt: toDatabaseDateTime(nextFireAt) },
324
+ PlanScheduleRecordSchema,
325
+ ),
326
+ 'Failed to resume schedule.',
327
+ )
328
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
329
+ yield* effectTryPromise(
330
+ () =>
331
+ enqueueScheduleFire(
332
+ recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE),
333
+ Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
334
+ ),
335
+ 'Failed to enqueue resumed schedule.',
336
+ )
337
+ })
338
+
339
+ const listSchedulesEffect = (threadId: RecordIdInput) =>
340
+ effectTryPromise(
341
+ () =>
342
+ db.findMany(
343
+ TABLES.PLAN_SCHEDULE,
344
+ { threadId: ensureRecordId(threadId, TABLES.THREAD) },
345
+ PlanScheduleRecordSchema,
346
+ { orderBy: 'createdAt', orderDir: 'ASC' },
347
+ ),
348
+ 'Failed to list schedules.',
349
+ )
350
+
351
+ return {
352
+ createSchedule: createScheduleEffect,
353
+ computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = nowDate()): Date {
354
+ return Effect.runSync(computeNextFireAtEffect(spec, baseTime))
355
+ },
356
+ /** Called by the BullMQ worker when a fire-schedule job executes. */
357
+ fireScheduleById: fireScheduleByIdEffect,
358
+ fireSchedule: fireScheduleEffect,
359
+ /** Re-enqueue BullMQ jobs for all active schedules. Called once at worker startup. */
360
+ recoverActiveSchedules: recoverActiveSchedulesEffect,
361
+ cancelSchedule: cancelScheduleEffect,
362
+ pauseSchedule: pauseScheduleEffect,
363
+ resumeSchedule: resumeScheduleEffect,
364
+ listSchedules: listSchedulesEffect,
365
+ }
366
+ }
367
+
368
+ export class PlanSchedulerServiceTag extends Context.Service<
369
+ PlanSchedulerServiceTag,
370
+ ReturnType<typeof makePlanSchedulerService>
371
+ >()('@lota-sdk/core/PlanSchedulerService') {}
372
+
373
+ export const PlanSchedulerServiceLive = Layer.effect(
374
+ PlanSchedulerServiceTag,
375
+ Effect.gen(function* () {
376
+ const db = yield* DatabaseServiceTag
377
+ return makePlanSchedulerService(db)
378
+ }),
379
+ )
@@ -0,0 +1,224 @@
1
+ import { PlanTemplateRecordSchema } from '@lota-sdk/shared'
2
+ import type { PlanArtifactRecord, PlanDraft } from '@lota-sdk/shared'
3
+ import { Context, Schema, Effect, Layer } from 'effect'
4
+
5
+ import type { RecordIdInput } from '../../db/record-id'
6
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
7
+ import type { SurrealDBService } from '../../db/service'
8
+ import { TABLES } from '../../db/tables'
9
+ import { ValidationError } from '../../effect/errors'
10
+ import { DatabaseServiceTag } from '../../effect/services'
11
+ import { nowDate } from '../../utils/date-time'
12
+ import type { makeExecutionPlanService } from '../execution-plan/execution-plan.service'
13
+ import { ExecutionPlanServiceTag } from '../execution-plan/execution-plan.service'
14
+
15
+ interface PlanTemplateDeps {
16
+ db: SurrealDBService
17
+ executionPlanService: Pick<ReturnType<typeof makeExecutionPlanService>, 'createPlan'>
18
+ }
19
+
20
+ type CreateTemplateParams = {
21
+ organizationId: RecordIdInput
22
+ name: string
23
+ description?: string
24
+ draft: PlanDraft
25
+ tags?: string[]
26
+ source?: 'user' | 'playbook' | 'system'
27
+ sourceRef?: string
28
+ }
29
+
30
+ type GetTemplateBySourceRefParams = {
31
+ organizationId: RecordIdInput
32
+ source: 'user' | 'playbook' | 'system'
33
+ sourceRef: string
34
+ }
35
+
36
+ type ListTemplatesParams = { tags?: string[]; source?: string }
37
+
38
+ type UpdateTemplatePatch = Partial<{ name: string; description: string; draft: PlanDraft; tags: string[] }>
39
+
40
+ type InstantiateTemplateParams = {
41
+ templateId: RecordIdInput
42
+ organizationId: RecordIdInput
43
+ threadId: RecordIdInput
44
+ sourceThreadId?: RecordIdInput
45
+ leadAgentId: string
46
+ createdByAgentId?: string
47
+ requireApproval?: boolean
48
+ overrides?: Partial<PlanDraft>
49
+ carryForwardArtifacts?: PlanArtifactRecord[]
50
+ }
51
+
52
+ class PlanTemplateNotFoundError extends Schema.TaggedErrorClass<PlanTemplateNotFoundError>()(
53
+ 'PlanTemplateNotFoundError',
54
+ { templateId: Schema.String, message: Schema.String },
55
+ ) {}
56
+
57
+ function resolveSourceIdentityEffect(params: {
58
+ source?: 'user' | 'playbook' | 'system'
59
+ sourceRef?: string
60
+ }): Effect.Effect<{ source: 'user' | 'playbook' | 'system'; sourceRef?: string }, ValidationError> {
61
+ const source = params.source ?? 'user'
62
+ const sourceRef = params.sourceRef?.trim()
63
+
64
+ if (source !== 'user' && !sourceRef) {
65
+ return Effect.fail(new ValidationError({ message: `sourceRef is required when source is "${source}".` }))
66
+ }
67
+
68
+ return Effect.succeed({ source, sourceRef })
69
+ }
70
+
71
+ export function makePlanTemplateService(deps: PlanTemplateDeps) {
72
+ const { db } = deps
73
+
74
+ const createTemplateEffect = (params: CreateTemplateParams) =>
75
+ Effect.gen(function* () {
76
+ const now = nowDate()
77
+ const identity = yield* resolveSourceIdentityEffect({ source: params.source, sourceRef: params.sourceRef })
78
+ return yield* db.create(
79
+ TABLES.PLAN_TEMPLATE,
80
+ {
81
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
82
+ name: params.name,
83
+ ...(params.description ? { description: params.description } : {}),
84
+ draft: params.draft,
85
+ tags: params.tags ?? [],
86
+ source: identity.source,
87
+ ...(identity.sourceRef ? { sourceRef: identity.sourceRef } : {}),
88
+ createdAt: now,
89
+ },
90
+ PlanTemplateRecordSchema,
91
+ )
92
+ })
93
+
94
+ const getTemplateEffect = (templateId: RecordIdInput) =>
95
+ db.findOne(TABLES.PLAN_TEMPLATE, { id: ensureRecordId(templateId, TABLES.PLAN_TEMPLATE) }, PlanTemplateRecordSchema)
96
+
97
+ const getTemplateBySourceRefEffect = (params: GetTemplateBySourceRefParams) =>
98
+ db.findOne(
99
+ TABLES.PLAN_TEMPLATE,
100
+ {
101
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
102
+ source: params.source,
103
+ sourceRef: params.sourceRef,
104
+ },
105
+ PlanTemplateRecordSchema,
106
+ )
107
+
108
+ const listTemplatesEffect = (organizationId: RecordIdInput, params?: ListTemplatesParams) => {
109
+ const filter: Record<string, unknown> = { organizationId: ensureRecordId(organizationId, TABLES.ORGANIZATION) }
110
+ if (params?.source) {
111
+ filter.source = params.source
112
+ }
113
+
114
+ return Effect.gen(function* () {
115
+ const templates = yield* db.findMany(TABLES.PLAN_TEMPLATE, filter, PlanTemplateRecordSchema, {
116
+ orderBy: 'createdAt',
117
+ orderDir: 'ASC',
118
+ })
119
+
120
+ if (params?.tags && params.tags.length > 0) {
121
+ const tagSet = new Set(params.tags)
122
+ return templates.filter((t) => t.tags.some((tag) => tagSet.has(tag)))
123
+ }
124
+
125
+ return templates
126
+ })
127
+ }
128
+
129
+ const updateTemplateEffect = (templateId: RecordIdInput, patch: UpdateTemplatePatch) =>
130
+ Effect.gen(function* () {
131
+ const updated = yield* db.update(
132
+ TABLES.PLAN_TEMPLATE,
133
+ ensureRecordId(templateId, TABLES.PLAN_TEMPLATE),
134
+ patch,
135
+ PlanTemplateRecordSchema,
136
+ )
137
+ if (!updated) {
138
+ return yield* new PlanTemplateNotFoundError({
139
+ templateId: recordIdToString(templateId, TABLES.PLAN_TEMPLATE),
140
+ message: `Template not found: ${recordIdToString(templateId, TABLES.PLAN_TEMPLATE)}`,
141
+ })
142
+ }
143
+
144
+ return updated
145
+ })
146
+
147
+ const upsertTemplateBySourceRefEffect = (
148
+ params: CreateTemplateParams & { source: 'user' | 'playbook' | 'system'; sourceRef: string },
149
+ ) =>
150
+ Effect.gen(function* () {
151
+ const existing = yield* getTemplateBySourceRefEffect({
152
+ organizationId: params.organizationId,
153
+ source: params.source,
154
+ sourceRef: params.sourceRef,
155
+ })
156
+
157
+ if (!existing) {
158
+ return yield* createTemplateEffect(params)
159
+ }
160
+
161
+ return yield* updateTemplateEffect(existing.id, {
162
+ name: params.name,
163
+ description: params.description,
164
+ draft: params.draft,
165
+ tags: params.tags ?? [],
166
+ })
167
+ })
168
+
169
+ const deleteTemplateEffect = (templateId: RecordIdInput) =>
170
+ db.deleteById(TABLES.PLAN_TEMPLATE, ensureRecordId(templateId, TABLES.PLAN_TEMPLATE)).pipe(Effect.asVoid)
171
+
172
+ const instantiateEffect = (params: InstantiateTemplateParams) =>
173
+ Effect.gen(function* () {
174
+ const template = yield* getTemplateEffect(params.templateId)
175
+ if (!template) {
176
+ return yield* new PlanTemplateNotFoundError({
177
+ templateId: recordIdToString(params.templateId, TABLES.PLAN_TEMPLATE),
178
+ message: `Template not found: ${recordIdToString(params.templateId, TABLES.PLAN_TEMPLATE)}`,
179
+ })
180
+ }
181
+
182
+ const draft: PlanDraft = { ...template.draft, ...params.overrides }
183
+
184
+ if (params.carryForwardArtifacts && params.carryForwardArtifacts.length > 0) {
185
+ const carryContext = params.carryForwardArtifacts.map((a) => `[carry-forward] ${a.name}: ${a.pointer}`)
186
+ draft.objective = `${draft.objective}\n\nCarry-forward context:\n${carryContext.join('\n')}`
187
+ }
188
+
189
+ return yield* deps.executionPlanService.createPlan({
190
+ organizationId: params.organizationId,
191
+ threadId: params.threadId,
192
+ sourceThreadId: params.sourceThreadId,
193
+ leadAgentId: params.leadAgentId,
194
+ createdByAgentId: params.createdByAgentId,
195
+ requireApproval: params.requireApproval,
196
+ input: draft,
197
+ })
198
+ })
199
+
200
+ return {
201
+ createTemplate: createTemplateEffect,
202
+ getTemplate: getTemplateEffect,
203
+ getTemplateBySourceRef: getTemplateBySourceRefEffect,
204
+ listTemplates: listTemplatesEffect,
205
+ updateTemplate: updateTemplateEffect,
206
+ upsertTemplateBySourceRef: upsertTemplateBySourceRefEffect,
207
+ deleteTemplate: deleteTemplateEffect,
208
+ instantiate: instantiateEffect,
209
+ }
210
+ }
211
+
212
+ export class PlanTemplateServiceTag extends Context.Service<
213
+ PlanTemplateServiceTag,
214
+ ReturnType<typeof makePlanTemplateService>
215
+ >()('@lota-sdk/core/PlanTemplateService') {}
216
+
217
+ export const PlanTemplateServiceLive = Layer.effect(
218
+ PlanTemplateServiceTag,
219
+ Effect.gen(function* () {
220
+ const db = yield* DatabaseServiceTag
221
+ const executionPlanService = yield* ExecutionPlanServiceTag
222
+ return makePlanTemplateService({ db, executionPlanService })
223
+ }),
224
+ )
@@ -0,0 +1,36 @@
1
+ import type { PlanEventRecord } from '@lota-sdk/shared'
2
+ import { Schema, Effect } from 'effect'
3
+
4
+ import type { DatabaseTransaction, SurrealDBService } from '../../db/service'
5
+ import type { makePlanEventDeliveryService } from './plan-event-delivery.service'
6
+
7
+ class PlanTransactionEventsError extends Schema.TaggedErrorClass<PlanTransactionEventsError>()(
8
+ 'PlanTransactionEventsError',
9
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
10
+ ) {}
11
+
12
+ export function withTransactionAndEventsEffect<T, E, R>(params: {
13
+ db: SurrealDBService
14
+ planEventDeliveryService: ReturnType<typeof makePlanEventDeliveryService>
15
+ run: (tx: DatabaseTransaction, emittedEvents: PlanEventRecord[]) => Effect.Effect<T, E, R>
16
+ }) {
17
+ return Effect.gen(function* () {
18
+ const emittedEvents: PlanEventRecord[] = []
19
+ const result = yield* params.db
20
+ .withTransaction((tx) => params.run(tx, emittedEvents))
21
+ .pipe(
22
+ Effect.mapError(
23
+ (error) => new PlanTransactionEventsError({ message: 'Failed to run plan transaction.', cause: error }),
24
+ ),
25
+ )
26
+ yield* params.planEventDeliveryService
27
+ .dispatchEventsEffect(emittedEvents)
28
+ .pipe(
29
+ Effect.mapError(
30
+ (error) =>
31
+ new PlanTransactionEventsError({ message: 'Failed to dispatch plan transaction events.', cause: error }),
32
+ ),
33
+ )
34
+ return result
35
+ })
36
+ }