@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
@@ -1,430 +0,0 @@
1
- import type {
2
- DeadlineAction,
3
- DeadlineReminder,
4
- DeadlineSpec,
5
- PlanEventType,
6
- PlanNodeRunRecord,
7
- PlanNodeSpecRecord,
8
- PlanRunRecord,
9
- } from '@lota-sdk/shared'
10
- import { PlanEventSchema, PlanNodeRunSchema, PlanNodeSpecRecordSchema, PlanRunSchema } from '@lota-sdk/shared'
11
- import { BoundQuery, RecordId } from 'surrealdb'
12
-
13
- import type { RecordIdInput } from '../db/record-id'
14
- import { ensureRecordId, recordIdToString } from '../db/record-id'
15
- import { databaseService } from '../db/service'
16
- import { TABLES } from '../db/tables'
17
- import { planEventDeliveryService } from './plan-event-delivery.service'
18
- import { planRunService } from './plan-run.service'
19
-
20
- export type DeadlineEvaluationStatus = 'ok' | 'warning' | 'escalated' | 'missed'
21
-
22
- export interface DeadlineEvaluationResult {
23
- status: DeadlineEvaluationStatus
24
- activeReminder?: DeadlineReminder
25
- nextTriggerAt?: Date | null
26
- }
27
-
28
- class PlanDeadlineService {
29
- evaluateDeadline(params: { deadline: DeadlineSpec; nodeStartedAt: Date; now?: Date }): DeadlineEvaluationResult {
30
- const now = params.now ?? new Date()
31
- const deadlineTime = this.resolveDeadlineTime(params.deadline, params.nodeStartedAt)
32
-
33
- if (!deadlineTime) {
34
- return { status: 'ok', nextTriggerAt: null }
35
- }
36
-
37
- const reminderEntries = [...params.deadline.reminders]
38
- .map((reminder) => ({ reminder, triggerAt: new Date(deadlineTime.getTime() - reminder.beforeMs) }))
39
- .sort((a, b) => a.triggerAt.getTime() - b.triggerAt.getTime())
40
-
41
- let activeReminder: DeadlineReminder | undefined
42
- let nextTriggerAt: Date | null = null
43
-
44
- for (const entry of reminderEntries) {
45
- if (now.getTime() < entry.triggerAt.getTime()) {
46
- nextTriggerAt = entry.triggerAt
47
- break
48
- }
49
-
50
- activeReminder = entry.reminder
51
- }
52
-
53
- if (now.getTime() >= deadlineTime.getTime()) {
54
- return { status: 'missed', nextTriggerAt: null }
55
- }
56
-
57
- if (activeReminder) {
58
- const status: DeadlineEvaluationStatus = activeReminder.action === 'escalate' ? 'escalated' : 'warning'
59
- return { status, activeReminder, nextTriggerAt: nextTriggerAt ?? deadlineTime }
60
- }
61
-
62
- return { status: 'ok', nextTriggerAt: nextTriggerAt ?? deadlineTime }
63
- }
64
-
65
- async checkDeadlines(now?: Date): Promise<void> {
66
- const currentTime = now ?? new Date()
67
-
68
- const sweep = await this.collectDeadlineSweep(currentTime)
69
- if (sweep.entries.length === 0) {
70
- return
71
- }
72
-
73
- // Query parent runs lazily only for node runs that need notification or escalation handling.
74
- const runCache = new Map<string, PlanRunRecord>()
75
-
76
- for (const entry of sweep.entries) {
77
- if (entry.evaluation.status === 'ok') continue
78
-
79
- const deadline = entry.nodeSpec.deadline
80
- if (!deadline) continue
81
-
82
- const runIdStr = recordIdToString(entry.nodeRun.runId, TABLES.PLAN_RUN)
83
- let run = runCache.get(runIdStr)
84
- if (!run) {
85
- const found = await databaseService.findOne(
86
- TABLES.PLAN_RUN,
87
- { id: ensureRecordId(entry.nodeRun.runId, TABLES.PLAN_RUN) },
88
- PlanRunSchema,
89
- )
90
- if (!found) continue
91
- run = found
92
- runCache.set(runIdStr, run)
93
- }
94
-
95
- const dedupeKeyBase = `plan-deadline:${runIdStr}:${entry.nodeRun.nodeId}`
96
-
97
- if (entry.evaluation.status === 'warning') {
98
- await this.emitDeadlineEvent({
99
- run,
100
- nodeRun: entry.nodeRun,
101
- eventType: 'deadline-warning',
102
- emittedBy: 'plan-deadline-checker',
103
- message:
104
- entry.evaluation.activeReminder?.message ?? `Node "${entry.nodeRun.nodeId}" is approaching its deadline.`,
105
- detail: {
106
- title: 'Deadline approaching',
107
- reminderBeforeMs: entry.evaluation.activeReminder?.beforeMs ?? null,
108
- dedupeKey: `${dedupeKeyBase}:warning:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
109
- },
110
- })
111
- } else if (entry.evaluation.status === 'escalated') {
112
- await this.emitDeadlineEvent({
113
- run,
114
- nodeRun: entry.nodeRun,
115
- eventType: 'escalation-triggered',
116
- emittedBy: 'plan-deadline-checker',
117
- message:
118
- entry.evaluation.activeReminder?.message ?? `Node "${entry.nodeRun.nodeId}" deadline requires escalation.`,
119
- detail: {
120
- title: 'Deadline escalation',
121
- reminderBeforeMs: entry.evaluation.activeReminder?.beforeMs ?? null,
122
- dedupeKey: `${dedupeKeyBase}:escalated:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
123
- },
124
- })
125
- } else {
126
- await this.applyDeadlineMissAction({
127
- runId: entry.nodeRun.runId,
128
- nodeId: entry.nodeRun.nodeId,
129
- threadId: recordIdToString(run.threadId, TABLES.THREAD),
130
- organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
131
- action: deadline.missAction,
132
- emittedBy: 'plan-deadline-checker',
133
- })
134
- }
135
-
136
- await this.maybeEmitEscalationPolicyEvent({
137
- run,
138
- nodeRun: entry.nodeRun,
139
- nodeSpec: entry.nodeSpec,
140
- now: currentTime,
141
- })
142
- }
143
-
144
- const nextTriggerAt =
145
- sweep.entries
146
- .map((entry) => entry.evaluation.nextTriggerAt)
147
- .filter((value): value is Date => {
148
- if (!value) return false
149
- return value.getTime() > currentTime.getTime()
150
- })
151
- .sort((a, b) => a.getTime() - b.getTime())
152
- .at(0) ?? null
153
-
154
- if (nextTriggerAt) {
155
- await this.enqueueDeadlineCheck(nextTriggerAt)
156
- }
157
- }
158
-
159
- async recoverDeadlineChecks(now = new Date()): Promise<void> {
160
- const sweep = await this.collectDeadlineSweep(now)
161
- const hasDueAction = sweep.entries.some((entry) => entry.evaluation.status !== 'ok')
162
- if (hasDueAction) {
163
- await this.enqueueDeadlineCheck(now)
164
- return
165
- }
166
-
167
- const nextTriggerAt =
168
- sweep.entries
169
- .map((entry) => entry.evaluation.nextTriggerAt)
170
- .filter((value): value is Date => {
171
- if (!value) return false
172
- return value.getTime() > now.getTime()
173
- })
174
- .sort((a, b) => a.getTime() - b.getTime())
175
- .at(0) ?? null
176
-
177
- if (nextTriggerAt) {
178
- await this.enqueueDeadlineCheck(nextTriggerAt)
179
- }
180
- }
181
-
182
- private async collectDeadlineSweep(
183
- now: Date,
184
- ): Promise<{
185
- entries: Array<{ nodeRun: PlanNodeRunRecord; nodeSpec: PlanNodeSpecRecord; evaluation: DeadlineEvaluationResult }>
186
- }> {
187
- const activeNodeRuns = await databaseService.queryMany(
188
- new BoundQuery(`SELECT * FROM ${TABLES.PLAN_NODE_RUN} WHERE status IN $statuses`, {
189
- statuses: ['running', 'awaiting-human'],
190
- }),
191
- PlanNodeRunSchema,
192
- )
193
-
194
- const entries = []
195
-
196
- for (const nodeRun of activeNodeRuns) {
197
- const nodeSpec = await this.loadNodeSpec(nodeRun)
198
- if (!nodeSpec?.deadline) continue
199
-
200
- const evaluation = this.evaluateDeadline({
201
- deadline: nodeSpec.deadline,
202
- nodeStartedAt: new Date(nodeRun.startedAt ?? nodeRun.createdAt),
203
- now,
204
- })
205
-
206
- entries.push({ nodeRun, nodeSpec, evaluation })
207
- }
208
-
209
- return { entries }
210
- }
211
-
212
- private resolveDeadlineTime(deadline: DeadlineSpec, nodeStartedAt: Date): Date | null {
213
- return (
214
- (deadline.dueAt ? new Date(deadline.dueAt) : null) ??
215
- (deadline.durationMs !== undefined ? new Date(nodeStartedAt.getTime() + deadline.durationMs) : null)
216
- )
217
- }
218
-
219
- private async loadNodeSpec(nodeRun: PlanNodeRunRecord): Promise<PlanNodeSpecRecord | null> {
220
- return databaseService.findOne(
221
- TABLES.PLAN_NODE_SPEC,
222
- { planSpecId: ensureRecordId(nodeRun.planSpecId, TABLES.PLAN_SPEC), nodeId: nodeRun.nodeId },
223
- PlanNodeSpecRecordSchema,
224
- )
225
- }
226
-
227
- async applyDeadlineMissAction(params: {
228
- runId: RecordIdInput
229
- nodeId: string
230
- threadId: string
231
- organizationId: string
232
- action: DeadlineAction
233
- emittedBy: string
234
- }): Promise<void> {
235
- const runIdStr = recordIdToString(params.runId, TABLES.PLAN_RUN)
236
- const run = await planRunService.getRunById(params.runId)
237
- const nodeRun = await planRunService.getNodeRunByNodeId(params.runId, params.nodeId)
238
-
239
- switch (params.action) {
240
- case 'notify':
241
- await this.emitDeadlineEvent({
242
- run,
243
- nodeRun,
244
- eventType: 'deadline-missed',
245
- emittedBy: params.emittedBy,
246
- message: `Node "${params.nodeId}" has missed its deadline.`,
247
- detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:notify` },
248
- })
249
- break
250
-
251
- case 'escalate':
252
- await this.emitDeadlineEvent({
253
- run,
254
- nodeRun,
255
- eventType: 'escalation-triggered',
256
- emittedBy: params.emittedBy,
257
- message: `Node "${params.nodeId}" has missed its deadline and requires escalation.`,
258
- detail: {
259
- title: 'Deadline escalation',
260
- dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:escalate`,
261
- missedDeadline: true,
262
- },
263
- })
264
- break
265
-
266
- case 'block': {
267
- await this.emitDeadlineEvent({
268
- run,
269
- nodeRun,
270
- eventType: 'deadline-missed',
271
- emittedBy: params.emittedBy,
272
- message: `Node "${params.nodeId}" has missed its deadline.`,
273
- detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:block` },
274
- })
275
- const { planExecutorService } = await import('./plan-executor.service')
276
- await planExecutorService.blockNodeOnDispatchFailure({
277
- threadId: params.threadId,
278
- runId: runIdStr,
279
- nodeId: params.nodeId,
280
- emittedBy: params.emittedBy,
281
- message: 'Deadline missed — node blocked',
282
- failureClass: 'timeout_exceeded',
283
- })
284
- break
285
- }
286
-
287
- case 'fail': {
288
- await this.emitDeadlineEvent({
289
- run,
290
- nodeRun,
291
- eventType: 'deadline-missed',
292
- emittedBy: params.emittedBy,
293
- message: `Node "${params.nodeId}" has missed its deadline.`,
294
- detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:fail` },
295
- })
296
- const { planExecutorService } = await import('./plan-executor.service')
297
- await planExecutorService.blockNodeOnDispatchFailure({
298
- threadId: params.threadId,
299
- runId: runIdStr,
300
- nodeId: params.nodeId,
301
- emittedBy: params.emittedBy,
302
- message: 'Deadline missed — node failed',
303
- failureClass: 'timeout_exceeded',
304
- })
305
- break
306
- }
307
- }
308
- }
309
-
310
- private async maybeEmitEscalationPolicyEvent(params: {
311
- run: PlanRunRecord
312
- nodeRun: PlanNodeRunRecord
313
- nodeSpec: PlanNodeSpecRecord
314
- now: Date
315
- }): Promise<void> {
316
- const escalation = params.nodeSpec.escalation
317
- if (!escalation) return
318
-
319
- const startedAt = new Date(params.nodeRun.startedAt ?? params.nodeRun.createdAt)
320
- const runIdStr = recordIdToString(params.run.id, TABLES.PLAN_RUN)
321
- const baseKey = `plan-escalation:${runIdStr}:${params.nodeRun.nodeId}`
322
-
323
- if (escalation.autoEscalateAfterMinutes) {
324
- const thresholdMs = escalation.autoEscalateAfterMinutes * 60_000
325
- if (params.now.getTime() - startedAt.getTime() >= thresholdMs) {
326
- await this.emitDeadlineEvent({
327
- run: params.run,
328
- nodeRun: params.nodeRun,
329
- eventType: 'escalation-triggered',
330
- emittedBy: 'plan-deadline-checker',
331
- message: `Node "${params.nodeRun.nodeId}" exceeded its auto-escalation threshold.`,
332
- detail: {
333
- title: 'Execution escalation',
334
- dedupeKey: `${baseKey}:auto:${escalation.autoEscalateAfterMinutes}`,
335
- autoEscalateAfterMinutes: escalation.autoEscalateAfterMinutes,
336
- ...(escalation.escalateToAgent ? { escalateToAgent: escalation.escalateToAgent } : {}),
337
- ...(escalation.escalateToUser ? { escalateToUser: escalation.escalateToUser } : {}),
338
- },
339
- })
340
- }
341
- }
342
-
343
- if (escalation.deadlineThresholdMinutes && params.nodeSpec.deadline) {
344
- const dueAt = this.resolveDeadlineTime(params.nodeSpec.deadline, startedAt)
345
- if (!dueAt) return
346
-
347
- const thresholdMs = escalation.deadlineThresholdMinutes * 60_000
348
- if (dueAt.getTime() - params.now.getTime() <= thresholdMs && dueAt.getTime() > params.now.getTime()) {
349
- await this.emitDeadlineEvent({
350
- run: params.run,
351
- nodeRun: params.nodeRun,
352
- eventType: 'escalation-triggered',
353
- emittedBy: 'plan-deadline-checker',
354
- message: `Node "${params.nodeRun.nodeId}" is inside its escalation threshold.`,
355
- detail: {
356
- title: 'Deadline escalation threshold',
357
- dedupeKey: `${baseKey}:threshold:${escalation.deadlineThresholdMinutes}`,
358
- deadlineThresholdMinutes: escalation.deadlineThresholdMinutes,
359
- ...(escalation.escalateToAgent ? { escalateToAgent: escalation.escalateToAgent } : {}),
360
- ...(escalation.escalateToUser ? { escalateToUser: escalation.escalateToUser } : {}),
361
- },
362
- })
363
- }
364
- }
365
- }
366
-
367
- private async emitDeadlineEvent(params: {
368
- run: PlanRunRecord
369
- nodeRun: PlanNodeRunRecord
370
- eventType: Extract<PlanEventType, 'deadline-warning' | 'deadline-missed' | 'escalation-triggered'>
371
- emittedBy: string
372
- message: string
373
- detail: Record<string, unknown>
374
- }): Promise<void> {
375
- const dedupeKey =
376
- typeof params.detail.dedupeKey === 'string' && params.detail.dedupeKey.trim().length > 0
377
- ? params.detail.dedupeKey.trim()
378
- : null
379
-
380
- if (dedupeKey) {
381
- const existing = await databaseService.queryMany(
382
- new BoundQuery(
383
- `SELECT * FROM ${TABLES.PLAN_EVENT}
384
- WHERE runId = $runId
385
- AND nodeId = $nodeId
386
- AND eventType = $eventType
387
- AND detail.dedupeKey = $dedupeKey
388
- LIMIT 1`,
389
- {
390
- runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
391
- nodeId: params.nodeRun.nodeId,
392
- eventType: params.eventType,
393
- dedupeKey,
394
- },
395
- ),
396
- PlanEventSchema,
397
- )
398
- if (existing.length > 0) {
399
- return
400
- }
401
- }
402
-
403
- const spec = await planRunService.getPlanSpecById(params.run.planSpecId)
404
- const event = PlanEventSchema.parse(
405
- await databaseService.create(
406
- TABLES.PLAN_EVENT,
407
- {
408
- id: new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()),
409
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
410
- runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
411
- nodeId: params.nodeRun.nodeId,
412
- eventType: params.eventType,
413
- message: params.message,
414
- emittedBy: params.emittedBy,
415
- detail: params.detail,
416
- },
417
- PlanEventSchema,
418
- ),
419
- )
420
-
421
- await planEventDeliveryService.dispatchEvent(event)
422
- }
423
-
424
- private async enqueueDeadlineCheck(scheduledFor: Date): Promise<void> {
425
- const { enqueueDeadlineCheck } = await import('../queues/plan-scheduler.queue')
426
- await enqueueDeadlineCheck(scheduledFor)
427
- }
428
- }
429
-
430
- export const planDeadlineService = new PlanDeadlineService()
@@ -1,166 +0,0 @@
1
- import { PlanEventSchema } from '@lota-sdk/shared'
2
- import type { PlanEventRecord } from '@lota-sdk/shared'
3
-
4
- import { ensureRecordId, recordIdToString } from '../db/record-id'
5
- import { databaseService } from '../db/service'
6
- import { TABLES } from '../db/tables'
7
- import { getRedisConnection } from '../redis'
8
- import { resolvePlanNodeExecutionVisibility } from '../runtime/execution-plan-visibility'
9
- import { getRuntimeAdapters } from '../runtime/runtime-extensions'
10
- import type { LotaRuntimePlanEventEnvelope } from '../runtime/runtime-extensions'
11
- import { planRunService } from './plan-run.service'
12
- import { ThreadSchema } from './thread.types'
13
- import { userService } from './user.service'
14
-
15
- const PLAN_EVENT_DELIVERED_TTL_MS = 7 * 24 * 60 * 60 * 1000
16
-
17
- function buildDeliveredKey(eventId: string): string {
18
- return `plan-event:delivered:${eventId}`
19
- }
20
-
21
- class PlanEventDeliveryService {
22
- async dispatchEvents(events: PlanEventRecord[]): Promise<void> {
23
- const runIds = new Set(events.map((event) => recordIdToString(event.runId, TABLES.PLAN_RUN)))
24
- for (const runId of runIds) {
25
- await this.dispatchUndeliveredEvents(runId)
26
- }
27
- }
28
-
29
- async dispatchEvent(rawEvent: PlanEventRecord): Promise<void> {
30
- const event = PlanEventSchema.parse(rawEvent)
31
- const eventId = recordIdToString(event.id, TABLES.PLAN_EVENT)
32
- const deliveredKey = buildDeliveredKey(eventId)
33
- if (await getRedisConnection().exists(deliveredKey)) {
34
- return
35
- }
36
-
37
- await this.deliverEvent(event)
38
- await getRedisConnection().set(deliveredKey, '1', 'PX', PLAN_EVENT_DELIVERED_TTL_MS)
39
- }
40
-
41
- private async deliverEvent(rawEvent: PlanEventRecord): Promise<void> {
42
- const event = PlanEventSchema.parse(rawEvent)
43
- const run = await planRunService.getRunById(event.runId)
44
- const [spec, nodeSpecs, nodeRuns, thread] = await Promise.all([
45
- planRunService.getPlanSpecById(run.planSpecId),
46
- planRunService.listNodeSpecs(run.planSpecId),
47
- planRunService.listNodeRuns(run.id),
48
- databaseService.findOne(TABLES.THREAD, { id: ensureRecordId(run.threadId, TABLES.THREAD) }, ThreadSchema),
49
- ])
50
- const nodeSpecsById = new Map(nodeSpecs.map((nodeSpec) => [nodeSpec.nodeId, nodeSpec]))
51
- const nodeRunsById = new Map(nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
52
- const organizationId = recordIdToString(run.organizationId, TABLES.ORGANIZATION)
53
- const threadId = recordIdToString(run.threadId, TABLES.THREAD)
54
- const runIdString = recordIdToString(run.id, TABLES.PLAN_RUN)
55
- const planSpecId = recordIdToString(spec.id, TABLES.PLAN_SPEC)
56
- const userId = thread?.userId
57
- ? recordIdToString(ensureRecordId(thread.userId, TABLES.USER), TABLES.USER)
58
- : undefined
59
- const userName =
60
- userId === undefined
61
- ? undefined
62
- : await userService
63
- .getUser(userId)
64
- .then((user) => user.name)
65
- .catch(() => undefined)
66
-
67
- const envelope: LotaRuntimePlanEventEnvelope = {
68
- event,
69
- spec,
70
- run,
71
- ...(event.nodeId ? { nodeSpec: nodeSpecsById.get(event.nodeId) } : {}),
72
- ...(event.nodeId ? { nodeRun: nodeRunsById.get(event.nodeId) } : {}),
73
- organizationId,
74
- threadId,
75
- runId: runIdString,
76
- planSpecId,
77
- ...(userId ? { userId } : {}),
78
- ...(userName ? { userName } : {}),
79
- }
80
-
81
- await this.dispatchAdapterEvent(envelope)
82
- await this.enqueueWakeIfNeeded(envelope, nodeSpecsById)
83
- }
84
-
85
- async dispatchUndeliveredEvents(runId: string, options?: { limit?: number }): Promise<void> {
86
- const run = await planRunService.getRunById(runId)
87
- const events = await planRunService.listEvents(run.id, options?.limit ?? 200)
88
-
89
- for (const rawEvent of events) {
90
- const event = PlanEventSchema.parse(rawEvent)
91
- const eventId = recordIdToString(event.id, TABLES.PLAN_EVENT)
92
- const deliveredKey = buildDeliveredKey(eventId)
93
- if (await getRedisConnection().exists(deliveredKey)) {
94
- continue
95
- }
96
-
97
- await this.dispatchEvent(event)
98
- }
99
- }
100
-
101
- private async dispatchAdapterEvent(envelope: LotaRuntimePlanEventEnvelope): Promise<void> {
102
- const adapter = getRuntimeAdapters().planEventAdapter
103
- if (!adapter) {
104
- return
105
- }
106
-
107
- await adapter.onPlanEvent(envelope)
108
- }
109
-
110
- private async enqueueWakeIfNeeded(
111
- envelope: LotaRuntimePlanEventEnvelope,
112
- nodeSpecsById: Map<string, NonNullable<LotaRuntimePlanEventEnvelope['nodeSpec']>>,
113
- ): Promise<void> {
114
- const wakeTarget = this.resolveWakeTarget(envelope, nodeSpecsById)
115
- if (!wakeTarget) {
116
- return
117
- }
118
-
119
- const { enqueuePlanAgentHeartbeatWake } = await import('../queues/plan-agent-heartbeat.queue')
120
- await enqueuePlanAgentHeartbeatWake({
121
- organizationId: envelope.organizationId,
122
- threadId: envelope.threadId,
123
- runId: envelope.runId,
124
- nodeId: wakeTarget.nodeId,
125
- agentId: wakeTarget.agentId,
126
- reason: wakeTarget.reason,
127
- })
128
- }
129
-
130
- private resolveWakeTarget(
131
- envelope: LotaRuntimePlanEventEnvelope,
132
- nodeSpecsById: Map<string, NonNullable<LotaRuntimePlanEventEnvelope['nodeSpec']>>,
133
- ): { nodeId: string; agentId: string; reason: string } | null {
134
- const currentNodeId = envelope.run.currentNodeId ?? envelope.event.nodeId
135
- if (!currentNodeId) {
136
- return null
137
- }
138
-
139
- const effectiveNodeSpec =
140
- (envelope.event.nodeId === currentNodeId ? envelope.nodeSpec : undefined) ?? nodeSpecsById.get(currentNodeId)
141
- if (!effectiveNodeSpec || effectiveNodeSpec.owner.executorType !== 'agent') {
142
- return null
143
- }
144
-
145
- const isVisible = resolvePlanNodeExecutionVisibility(envelope.spec, effectiveNodeSpec) === 'visible'
146
- if (!isVisible) {
147
- return null
148
- }
149
-
150
- const supportedEventTypes = new Set([
151
- 'node-ready',
152
- 'node-unblocked',
153
- 'approval-resolved',
154
- 'run-resumed',
155
- 'deadline-warning',
156
- 'escalation-triggered',
157
- ])
158
- if (!supportedEventTypes.has(envelope.event.eventType)) {
159
- return null
160
- }
161
-
162
- return { nodeId: currentNodeId, agentId: effectiveNodeSpec.owner.ref, reason: envelope.event.eventType }
163
- }
164
- }
165
-
166
- export const planEventDeliveryService = new PlanEventDeliveryService()