@lota-sdk/core 0.4.7 → 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/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  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,385 @@
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 { effectTryPromise as effectTryPromiseShared } 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
+ function effectTryPromise<A>(
27
+ evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
28
+ message: string,
29
+ ): Effect.Effect<A, PlanSchedulerError> {
30
+ return effectTryPromiseShared(evaluate, (cause) => new PlanSchedulerError({ message, cause }))
31
+ }
32
+
33
+ function failPlanScheduler(message: string, cause?: unknown): Effect.Effect<never, PlanSchedulerError> {
34
+ return Effect.fail(new PlanSchedulerError({ message, ...(cause === undefined ? {} : { cause }) }))
35
+ }
36
+
37
+ function requireScheduleField<A>(value: A | undefined, message: string): Effect.Effect<A, PlanSchedulerError> {
38
+ return value === undefined ? failPlanScheduler(message) : Effect.succeed(value)
39
+ }
40
+
41
+ const computeNextCronDateEffect: (cronExpression: string, baseTime: Date) => Effect.Effect<Date, PlanSchedulerError> =
42
+ Effect.fn('PlanScheduler.computeNextCronDate')(function* (cronExpression: string, baseTime: Date) {
43
+ const parsedCron = Cron.parse(cronExpression)
44
+ const cron = Result.isSuccess(parsedCron)
45
+ ? parsedCron.success
46
+ : yield* failPlanScheduler(`Invalid cron expression: "${cronExpression}".`)
47
+
48
+ return yield* Effect.try({
49
+ try: () => Cron.next(cron, baseTime),
50
+ catch: (cause) =>
51
+ new PlanSchedulerError({
52
+ message: `Failed to compute the next fire time for cron expression "${cronExpression}".`,
53
+ cause,
54
+ }),
55
+ })
56
+ })
57
+
58
+ const computeNextFireAtEffect: (spec: PlanScheduleSpec, baseTime?: Date) => Effect.Effect<Date, PlanSchedulerError> =
59
+ Effect.fn('PlanScheduler.computeNextFireAt')(function* (spec: PlanScheduleSpec, baseTime: Date = nowDate()) {
60
+ switch (spec.type) {
61
+ case 'immediate':
62
+ return baseTime
63
+
64
+ case 'absolute': {
65
+ const at = yield* requireScheduleField(spec.at, 'Absolute schedules require an "at" timestamp.')
66
+ return unsafeDateFrom(at)
67
+ }
68
+
69
+ case 'relative': {
70
+ const delayMs = yield* requireScheduleField(spec.delayMs, 'Relative schedules require "delayMs".')
71
+ return unsafeDateFrom(baseTime.getTime() + delayMs)
72
+ }
73
+
74
+ case 'cron': {
75
+ const cronExpression = yield* requireScheduleField(spec.cron, 'Cron schedules require a "cron" expression.')
76
+ return yield* computeNextCronDateEffect(cronExpression, baseTime)
77
+ }
78
+
79
+ case 'monitoring': {
80
+ const intervalMs = yield* requireScheduleField(spec.intervalMs, 'Monitoring schedules require "intervalMs".')
81
+ return unsafeDateFrom(baseTime.getTime() + intervalMs)
82
+ }
83
+
84
+ default:
85
+ return yield* failPlanScheduler('Unsupported schedule type in schedule specification.')
86
+ }
87
+ })
88
+
89
+ export function makePlanSchedulerService(db: SurrealDBService) {
90
+ const loadPlanSchedulerQueue = () =>
91
+ effectTryPromise(() => import('../../queues/plan-scheduler.queue'), 'Failed to load plan scheduler queue module.')
92
+
93
+ const createScheduleEffect = (params: {
94
+ organizationId: RecordIdInput
95
+ threadId: RecordIdInput
96
+ planSpecId?: RecordIdInput
97
+ runId?: RecordIdInput
98
+ nodeId?: string
99
+ scheduleSpec: PlanScheduleSpec
100
+ }): Effect.Effect<PlanScheduleRecord, PlanSchedulerError> =>
101
+ Effect.gen(function* () {
102
+ const nextFireAt = yield* computeNextFireAtEffect(params.scheduleSpec)
103
+ const now = nowDate()
104
+ const record = yield* effectTryPromise(
105
+ () =>
106
+ db.create(
107
+ TABLES.PLAN_SCHEDULE,
108
+ {
109
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
110
+ threadId: ensureRecordId(params.threadId, TABLES.THREAD),
111
+ planSpecId: params.planSpecId ? ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC) : undefined,
112
+ runId: params.runId ? ensureRecordId(params.runId, TABLES.PLAN_RUN) : undefined,
113
+ nodeId: params.nodeId,
114
+ scheduleSpec: params.scheduleSpec,
115
+ status: 'active',
116
+ fireCount: 0,
117
+ nextFireAt: toDatabaseDateTime(nextFireAt),
118
+ createdAt: now,
119
+ },
120
+ PlanScheduleRecordSchema,
121
+ ),
122
+ 'Failed to create plan schedule.',
123
+ )
124
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
125
+ yield* effectTryPromise(
126
+ () =>
127
+ enqueueScheduleFire(
128
+ recordIdToString(record.id, TABLES.PLAN_SCHEDULE),
129
+ Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
130
+ ),
131
+ 'Failed to enqueue schedule fire job.',
132
+ )
133
+ return record
134
+ })
135
+
136
+ const fireScheduleEffect = (
137
+ schedule: PlanScheduleRecord,
138
+ runtimeDeps: PlanSchedulerRuntimeDeps,
139
+ ): Effect.Effect<void, PlanSchedulerError> =>
140
+ Effect.gen(function* () {
141
+ const now = nowDate()
142
+ const newFireCount = schedule.fireCount + 1
143
+ const isRecurring = schedule.scheduleSpec.type === 'cron' || schedule.scheduleSpec.type === 'monitoring'
144
+ const maxReached = schedule.scheduleSpec.maxFires !== undefined && newFireCount >= schedule.scheduleSpec.maxFires
145
+
146
+ let nextFireAt: Date | null = null
147
+ let newStatus: 'active' | 'completed' = 'completed'
148
+
149
+ if (isRecurring && !maxReached) {
150
+ nextFireAt = yield* computeNextFireAtEffect(schedule.scheduleSpec, now)
151
+ newStatus = 'active'
152
+ }
153
+
154
+ yield* effectTryPromise(
155
+ () =>
156
+ db.update(
157
+ TABLES.PLAN_SCHEDULE,
158
+ schedule.id,
159
+ {
160
+ fireCount: newFireCount,
161
+ lastFiredAt: now,
162
+ nextFireAt: toDatabaseDateTime(nextFireAt),
163
+ status: newStatus,
164
+ },
165
+ PlanScheduleRecordSchema,
166
+ ),
167
+ 'Failed to update fired schedule.',
168
+ )
169
+
170
+ let shouldRecoverDeadlineChecks = false
171
+ if (newStatus === 'active') {
172
+ if (!nextFireAt) {
173
+ return yield* new PlanSchedulerError({ message: 'Recurring schedules must resolve a next fire time.' })
174
+ }
175
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
176
+ yield* effectTryPromise(
177
+ () =>
178
+ enqueueScheduleFire(
179
+ recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
180
+ Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
181
+ ),
182
+ 'Failed to enqueue next schedule fire job.',
183
+ )
184
+ }
185
+
186
+ const runId = schedule.runId
187
+ const nodeId = schedule.nodeId
188
+ if (runId && nodeId) {
189
+ yield* effectTryPromise(
190
+ () =>
191
+ runtimeDeps.promoteDelayedNode({
192
+ runId: recordIdToString(runId, TABLES.PLAN_RUN),
193
+ nodeId,
194
+ emittedBy: 'plan-scheduler',
195
+ }),
196
+ 'Failed to promote delayed plan node.',
197
+ )
198
+ shouldRecoverDeadlineChecks = true
199
+ }
200
+
201
+ if (schedule.planSpecId && !schedule.runId) {
202
+ const cycle = yield* effectTryPromise(
203
+ () =>
204
+ db.findOne(
205
+ TABLES.PLAN_CYCLE,
206
+ { scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
207
+ PlanCycleRecordSchema,
208
+ ),
209
+ 'Failed to load plan cycle for schedule.',
210
+ )
211
+ if (cycle) {
212
+ yield* effectTryPromise(() => runtimeDeps.advanceCycle(cycle.id), 'Failed to advance plan cycle.')
213
+ shouldRecoverDeadlineChecks = true
214
+ }
215
+ }
216
+
217
+ if (shouldRecoverDeadlineChecks) {
218
+ yield* effectTryPromise(() => runtimeDeps.recoverDeadlineChecks(), 'Failed to recover deadline checks.')
219
+ }
220
+ })
221
+
222
+ const fireScheduleByIdEffect = (scheduleId: string, runtimeDeps: PlanSchedulerRuntimeDeps) =>
223
+ Effect.gen(function* () {
224
+ const schedule = yield* effectTryPromise(
225
+ () =>
226
+ db.findOne(
227
+ TABLES.PLAN_SCHEDULE,
228
+ { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
229
+ PlanScheduleRecordSchema,
230
+ ),
231
+ 'Failed to load schedule by id.',
232
+ )
233
+ if (!schedule || schedule.status !== 'active') return
234
+
235
+ yield* fireScheduleEffect(schedule, runtimeDeps)
236
+ })
237
+
238
+ const recoverActiveSchedulesEffect = () =>
239
+ Effect.gen(function* () {
240
+ const activeSchedules = yield* effectTryPromise(
241
+ () =>
242
+ db.queryMany(
243
+ new BoundQuery(`SELECT * FROM ${TABLES.PLAN_SCHEDULE} WHERE status = $status ORDER BY nextFireAt ASC`, {
244
+ status: 'active',
245
+ }),
246
+ PlanScheduleRecordSchema,
247
+ ),
248
+ 'Failed to load active schedules.',
249
+ )
250
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
251
+ yield* Effect.forEach(
252
+ activeSchedules.filter(
253
+ (schedule): schedule is typeof schedule & { nextFireAt: Date } => schedule.nextFireAt !== undefined,
254
+ ),
255
+ (schedule) =>
256
+ effectTryPromise(
257
+ () =>
258
+ enqueueScheduleFire(
259
+ recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
260
+ Math.max(0, unsafeDateFrom(schedule.nextFireAt).getTime() - nowEpochMillis()),
261
+ ),
262
+ `Failed to re-enqueue schedule ${recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE)}.`,
263
+ ),
264
+ { concurrency: 5, discard: true },
265
+ )
266
+ })
267
+
268
+ const cancelScheduleEffect = (scheduleId: RecordIdInput) => {
269
+ const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
270
+ return Effect.gen(function* () {
271
+ const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
272
+ yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
273
+ yield* effectTryPromise(
274
+ () =>
275
+ db.update(
276
+ TABLES.PLAN_SCHEDULE,
277
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
278
+ { status: 'cancelled' },
279
+ PlanScheduleRecordSchema,
280
+ ),
281
+ 'Failed to cancel schedule.',
282
+ )
283
+ })
284
+ }
285
+
286
+ const pauseScheduleEffect = (scheduleId: RecordIdInput) => {
287
+ const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
288
+ return Effect.gen(function* () {
289
+ const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
290
+ yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
291
+ yield* effectTryPromise(
292
+ () =>
293
+ db.update(
294
+ TABLES.PLAN_SCHEDULE,
295
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
296
+ { status: 'paused' },
297
+ PlanScheduleRecordSchema,
298
+ ),
299
+ 'Failed to pause schedule.',
300
+ )
301
+ })
302
+ }
303
+
304
+ const resumeScheduleEffect = (scheduleId: RecordIdInput) =>
305
+ Effect.gen(function* () {
306
+ const schedule = yield* effectTryPromise(
307
+ () =>
308
+ db.findOne(
309
+ TABLES.PLAN_SCHEDULE,
310
+ { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
311
+ PlanScheduleRecordSchema,
312
+ ),
313
+ 'Failed to load schedule for resume.',
314
+ )
315
+ if (!schedule) {
316
+ return yield* new NotFoundError({
317
+ resource: TABLES.PLAN_SCHEDULE,
318
+ id: recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE),
319
+ message: `Schedule not found: ${recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)}`,
320
+ })
321
+ }
322
+
323
+ const nextFireAt = yield* computeNextFireAtEffect(schedule.scheduleSpec)
324
+ yield* effectTryPromise(
325
+ () =>
326
+ db.update(
327
+ TABLES.PLAN_SCHEDULE,
328
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
329
+ { status: 'active', nextFireAt: toDatabaseDateTime(nextFireAt) },
330
+ PlanScheduleRecordSchema,
331
+ ),
332
+ 'Failed to resume schedule.',
333
+ )
334
+ const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
335
+ yield* effectTryPromise(
336
+ () =>
337
+ enqueueScheduleFire(
338
+ recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE),
339
+ Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
340
+ ),
341
+ 'Failed to enqueue resumed schedule.',
342
+ )
343
+ })
344
+
345
+ const listSchedulesEffect = (threadId: RecordIdInput) =>
346
+ effectTryPromise(
347
+ () =>
348
+ db.findMany(
349
+ TABLES.PLAN_SCHEDULE,
350
+ { threadId: ensureRecordId(threadId, TABLES.THREAD) },
351
+ PlanScheduleRecordSchema,
352
+ { orderBy: 'createdAt', orderDir: 'ASC' },
353
+ ),
354
+ 'Failed to list schedules.',
355
+ )
356
+
357
+ return {
358
+ createSchedule: createScheduleEffect,
359
+ computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = nowDate()): Date {
360
+ return Effect.runSync(computeNextFireAtEffect(spec, baseTime))
361
+ },
362
+ /** Called by the BullMQ worker when a fire-schedule job executes. */
363
+ fireScheduleById: fireScheduleByIdEffect,
364
+ fireSchedule: fireScheduleEffect,
365
+ /** Re-enqueue BullMQ jobs for all active schedules. Called once at worker startup. */
366
+ recoverActiveSchedules: recoverActiveSchedulesEffect,
367
+ cancelSchedule: cancelScheduleEffect,
368
+ pauseSchedule: pauseScheduleEffect,
369
+ resumeSchedule: resumeScheduleEffect,
370
+ listSchedules: listSchedulesEffect,
371
+ }
372
+ }
373
+
374
+ export class PlanSchedulerServiceTag extends Context.Service<
375
+ PlanSchedulerServiceTag,
376
+ ReturnType<typeof makePlanSchedulerService>
377
+ >()('PlanSchedulerService') {}
378
+
379
+ export const PlanSchedulerServiceLive = Layer.effect(
380
+ PlanSchedulerServiceTag,
381
+ Effect.gen(function* () {
382
+ const db = yield* DatabaseServiceTag
383
+ return makePlanSchedulerService(db)
384
+ }),
385
+ )
@@ -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
+ >()('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,33 @@
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* Effect.tryPromise({
27
+ try: () => params.planEventDeliveryService.dispatchEvents(emittedEvents),
28
+ catch: (error) =>
29
+ new PlanTransactionEventsError({ message: 'Failed to dispatch plan transaction events.', cause: error }),
30
+ })
31
+ return result
32
+ })
33
+ }